diff --git a/kubernetes/gitea/landingpage.css b/kubernetes/gitea/landingpage.css index 8855af6..def81ee 100644 --- a/kubernetes/gitea/landingpage.css +++ b/kubernetes/gitea/landingpage.css @@ -241,7 +241,7 @@ display: flex; align-items: flex-start; gap: var(--space-xs); - padding: var(--space-xs) var(--space-sm); + padding: var(--space-sm) var(--space-sm); background: var(--color-bg-card); border-top: 1px solid var(--color-border); font-size: var(--text-base); @@ -259,21 +259,36 @@ align-items: center; justify-content: center; font-size: var(--text-sm); - margin-top: 1px; + margin-top: 2px; } .activity-body { flex: 1; min-width: 0; } -.activity-headline { color: var(--color-text); line-height: 1.5; } +.activity-headline-row { + display: flex; + align-items: baseline; + gap: var(--space-xs); + min-width: 0; +} +.activity-headline { + color: var(--color-text); + line-height: 1.5; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} .activity-headline a { color: var(--color-accent); text-decoration: none; - font-weight: 600; + font-weight: 500; } .activity-headline a:hover { text-decoration: underline; } .activity-commits { - margin-top: 6px; + margin-top: 5px; display: flex; flex-direction: column; gap: 3px; + padding-left: 2px; } .activity-commit-line { display: flex; @@ -286,21 +301,24 @@ .activity-commit-sha { font-family: monospace; font-size: var(--text-xs); - color: var(--color-accent); + color: var(--color-text-subtle); + background: var(--color-border); + border-radius: var(--radius-sm); + padding: 1px 5px; flex-shrink: 0; text-decoration: none; } -.activity-commit-sha:hover { text-decoration: underline; } +.activity-commit-sha:hover { color: var(--color-accent); text-decoration: none; } .activity-commit-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + color: var(--color-text-muted); } .activity-time { flex-shrink: 0; - font-size: 0.78rem; + font-size: 0.75rem; color: var(--color-text-faint); - margin-left: auto; - padding-left: var(--space-xs); white-space: nowrap; + padding-top: 3px; } diff --git a/kubernetes/gitea/landingpage.html b/kubernetes/gitea/landingpage.html index 2dc32d6..473b246 100644 --- a/kubernetes/gitea/landingpage.html +++ b/kubernetes/gitea/landingpage.html @@ -1,5 +1,4 @@ {{template "base/head" .}} -
@@ -43,5 +42,6 @@ window.GITEA_APP_URL = "{{AppUrl}}"; window.GITEA_SUB_URL = "{{AppSubUrl}}"; - + + {{template "base/footer" .}} diff --git a/kubernetes/gitea/landingpage.js b/kubernetes/gitea/landingpage.js index dd8ece1..1bee8ab 100644 --- a/kubernetes/gitea/landingpage.js +++ b/kubernetes/gitea/landingpage.js @@ -1,10 +1,16 @@ -(async function loadRepos() { - const grid = document.getElementById('repo-grid'); - if (!grid) return; +const httpService = { + baseUrl: window.GITEA_SUB_URL || '', - const baseUrl = window.GITEA_SUB_URL || ''; + async fetchRss() { + const resp = await fetch(`${this.baseUrl}/alex.rss`); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const text = await resp.text(); + return new DOMParser().parseFromString(text, 'application/xml'); + }, +}; - function timeAgo(dateStr) { +const dataDomain = { + timeAgo(dateStr) { const diff = (Date.now() - new Date(dateStr)) / 1000; if (diff < 60) return 'just now'; if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; @@ -12,195 +18,193 @@ if (diff < 2592000) return Math.floor(diff / 86400) + 'd ago'; if (diff < 31536000) return Math.floor(diff / 2592000) + 'mo ago'; return Math.floor(diff / 31536000) + 'y ago'; - } + }, - function esc(str) { - return (str || '').replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); - } + esc(str) { + return (str || '') + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + }, - let doc; - try { - const resp = await fetch(`${baseUrl}/alex.rss`); - if (!resp.ok) { - grid.innerHTML = `
Could not load feed (HTTP ${resp.status}). Browse manually →
`; - return; - } - const text = await resp.text(); - doc = new DOMParser().parseFromString(text, 'application/xml'); - } catch (e) { - console.error('Gitea landing: RSS fetch failed', e); - grid.innerHTML = `
Could not load repositories. Browse manually →
`; - return; - } + safeTitleHtml(rawTitleText) { + const doc = new DOMParser().parseFromString(rawTitleText, 'text/html'); + doc.body.querySelectorAll('*:not(a)').forEach(el => el.replaceWith(el.textContent)); + return doc.body.innerHTML; + }, - const items = Array.from(doc.querySelectorAll('channel > item')); - if (items.length === 0) { - grid.innerHTML = `
No activity found.
`; - return; - } + titlePlainText(rawTitleText) { + const doc = new DOMParser().parseFromString(rawTitleText, 'text/html'); + return doc.body.textContent || rawTitleText; + }, - // Deduplicate: one card per repo (most recent entry wins) - const seen = new Map(); - for (const item of items) { - const titleHtml = item.querySelector('title')?.textContent || ''; - const titleDoc = new DOMParser().parseFromString(titleHtml, 'text/html'); - const anchors = titleDoc.querySelectorAll('a'); - if (anchors.length < 2) continue; - // last anchor in the title is the repo link - const repoAnchor = anchors[anchors.length - 1]; - const repoName = repoAnchor.textContent.trim(); - if (!repoName || seen.has(repoName)) continue; - seen.set(repoName, { repoName, repoUrl: repoAnchor.getAttribute('href') || '#', item }); - } + activityIcon(titleText) { + const t = titleText.toLowerCase(); + if (t.includes('push') || t.includes('commit')) return '📤'; + if (t.includes('creat') && t.includes('repo')) return '📁'; + if (t.includes('fork')) return '🍴'; + if (t.includes('open') && t.includes('issue')) return '🔴'; + if (t.includes('clos') && t.includes('issue')) return '🟢'; + if (t.includes('pull request') || t.includes('merge')) return '🔀'; + if (t.includes('tag')) return '🏷️'; + if (t.includes('branch')) return '🌿'; + if (t.includes('comment')) return '💬'; + if (t.includes('release')) return '🚀'; + return '⚡'; + }, - if (seen.size === 0) { - grid.innerHTML = `
No repositories found in feed.
`; - return; - } - - grid.innerHTML = ''; - for (const { repoName, repoUrl, item } of seen.values()) { - const pubDate = item.querySelector('pubDate')?.textContent || ''; - const description = item.querySelector('description')?.textContent || ''; - const when = pubDate ? timeAgo(pubDate) : ''; - - // Parse first commit from description: sha\ncommit message - const descDoc = new DOMParser().parseFromString(description, 'text/html'); - const firstAnchor = descDoc.querySelector('a'); - let commitMsg = ''; - let commitUrl = '#'; - if (firstAnchor) { - commitUrl = firstAnchor.getAttribute('href') || '#'; - let node = firstAnchor.nextSibling; + parseCommits(descriptionText) { + const doc = new DOMParser().parseFromString(descriptionText, 'text/html'); + return Array.from(doc.querySelectorAll('a')).map(anchor => { + const sha = anchor.textContent.trim().slice(0, 7); + const href = anchor.getAttribute('href') || '#'; + let msg = ''; + let node = anchor.nextSibling; while (node) { const t = (node.textContent || '').trim(); - if (t) { commitMsg = t; break; } + if (t) { msg = t; break; } node = node.nextSibling; } - if (!commitMsg) commitMsg = firstAnchor.textContent.trim().slice(0, 7); + return { sha, href, msg }; + }); + }, + + parseRepos(xmlDoc) { + const items = Array.from(xmlDoc.querySelectorAll('channel > item')); + const seen = new Map(); + for (const item of items) { + const titleHtml = item.querySelector('title')?.textContent || ''; + const titleDoc = new DOMParser().parseFromString(titleHtml, 'text/html'); + const anchors = titleDoc.querySelectorAll('a'); + if (anchors.length < 2) continue; + const repoAnchor = anchors[anchors.length - 1]; + const repoName = repoAnchor.textContent.trim(); + if (!repoName || seen.has(repoName)) continue; + seen.set(repoName, { + repoName, + repoUrl: repoAnchor.getAttribute('href') || '#', + shortName: repoName.includes('/') ? repoName.split('/').pop() : repoName, + pubDate: item.querySelector('pubDate')?.textContent || '', + firstCommit: this.parseCommits(item.querySelector('description')?.textContent || '')[0] || null, + }); } + return Array.from(seen.values()); + }, - const shortName = repoName.includes('/') ? repoName.split('/').pop() : repoName; + parseActivity(xmlDoc, limit = 20) { + return Array.from(xmlDoc.querySelectorAll('channel > item')) + .slice(0, limit) + .map(item => { + const rawTitle = item.querySelector('title')?.textContent || ''; + const titleText = this.titlePlainText(rawTitle); + return { + titleHtmlSafe: this.safeTitleHtml(rawTitle), + titleText, + link: item.querySelector('link')?.textContent || '#', + pubDate: item.querySelector('pubDate')?.textContent || '', + icon: this.activityIcon(titleText), + commits: this.parseCommits(item.querySelector('description')?.textContent || '').slice(0, 3), + }; + }); + }, +}; - const card = document.createElement('a'); - card.className = 'repo-card'; - card.href = esc(repoUrl); - card.innerHTML = ` -
- 📦 - ${esc(shortName)} -
-
${esc(repoName)}
-
- - ${esc(commitMsg)} - ${esc(when)} -
- `.trim(); - grid.appendChild(card); - } -})(); +const uiRendering = { + async renderRepos(xmlDoc) { + const grid = document.getElementById('repo-grid'); + if (!grid) return; -(async function loadActivity() { - const feed = document.getElementById('activity-feed'); - if (!feed) return; - const baseUrl = window.GITEA_SUB_URL || ''; - function timeAgo(dateStr) { - const diff = (Date.now() - new Date(dateStr)) / 1000; - if (diff < 60) return 'just now'; - if (diff < 3600) return Math.floor(diff / 60) + 'm ago'; - if (diff < 86400) return Math.floor(diff / 3600) + 'h ago'; - if (diff < 2592000) return Math.floor(diff / 86400) + 'd ago'; - if (diff < 31536000) return Math.floor(diff / 2592000) + 'mo ago'; - return Math.floor(diff / 31536000) + 'y ago'; - } - function esc(str) { - return (str || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); - } - - let doc; - try { - const resp = await fetch(`${baseUrl}/alex.rss`); - if (!resp.ok) { - feed.innerHTML = `
Activity unavailable (HTTP ${resp.status})
`; + const repos = dataDomain.parseRepos(xmlDoc); + if (repos.length === 0) { + grid.innerHTML = `
No repositories found in feed.
`; return; } - const text = await resp.text(); - doc = new DOMParser().parseFromString(text, 'application/xml'); - } catch (e) { - console.error('activity rss error', e); - feed.innerHTML = `
Could not load activity
`; - return; - } - const items = Array.from(doc.querySelectorAll('channel > item')).slice(0, 20); - if (items.length === 0) { - feed.innerHTML = `
No public activity yet.
`; - return; - } + grid.innerHTML = ''; + for (const { shortName, repoName, repoUrl, pubDate, firstCommit } of repos) { + const when = dataDomain.timeAgo(pubDate); + const commitMsg = firstCommit?.msg || firstCommit?.sha || ''; - feed.innerHTML = ''; - for (const item of items) { - const title = item.querySelector('title')?.textContent || ''; - const link = item.querySelector('link')?.textContent || '#'; - const pubDate = item.querySelector('pubDate')?.textContent || ''; - const description = item.querySelector('description')?.textContent || ''; - const when = pubDate ? timeAgo(pubDate) : ''; + const card = document.createElement('a'); + card.className = 'repo-card'; + card.href = dataDomain.esc(repoUrl); + card.innerHTML = ` +
+ 📦 + ${dataDomain.esc(shortName)} +
+
${dataDomain.esc(repoName)}
+
+ + ${dataDomain.esc(commitMsg)} + ${dataDomain.esc(when)} +
+ `.trim(); + grid.appendChild(card); + } + }, - // Parse title HTML — Gitea only puts tags in it, safe to use as innerHTML - const titleDoc = new DOMParser().parseFromString(title, 'text/html'); - const titleText = titleDoc.body.textContent || title; - // Preserve links but strip any unsafe tags (only expected from Gitea) - titleDoc.body.querySelectorAll('*:not(a)').forEach(el => el.replaceWith(el.textContent)); - const titleHtmlSafe = titleDoc.body.innerHTML; + async renderActivity(xmlDoc) { + const feed = document.getElementById('activity-feed'); + if (!feed) return; - let icon = '⚡'; - const t = titleText.toLowerCase(); - if (t.includes('push') || t.includes('commit')) icon = '📤'; - else if (t.includes('creat') && t.includes('repo')) icon = '📁'; - else if (t.includes('fork')) icon = '🍴'; - else if (t.includes('open') && t.includes('issue')) icon = '🔴'; - else if (t.includes('clos') && t.includes('issue')) icon = '🟢'; - else if (t.includes('pull request') || t.includes('merge')) icon = '🔀'; - else if (t.includes('tag')) icon = '🏷️'; - else if (t.includes('branch')) icon = '🌿'; - else if (t.includes('comment')) icon = '💬'; - else if (t.includes('release')) icon = '🚀'; - - // Parse commits from description: sha\ncommit message\n\n... - let commitsHtml = ''; - const descDoc = new DOMParser().parseFromString(description, 'text/html'); - const commitAnchors = Array.from(descDoc.querySelectorAll('a')).slice(0, 3); - if (commitAnchors.length > 0) { - const lines = commitAnchors.map(anchor => { - const sha = esc(anchor.textContent.trim().slice(0, 7)); - const shaHref = esc(anchor.getAttribute('href') || '#'); - let msg = ''; - let node = anchor.nextSibling; - while (node) { - const t = (node.textContent || '').trim(); - if (t) { msg = esc(t); break; } - node = node.nextSibling; - } - return `
- ${sha} - ${msg} -
`; - }).join(''); - commitsHtml = `
${lines}
`; + const items = dataDomain.parseActivity(xmlDoc); + if (items.length === 0) { + feed.innerHTML = `
No public activity yet.
`; + return; } - const el = document.createElement('div'); - el.className = 'activity-item'; - el.innerHTML = ` -
${icon}
-
-
${titleHtmlSafe}
- ${commitsHtml} -
- ${when} - `; - feed.appendChild(el); - } -})(); + feed.innerHTML = ''; + for (const { titleHtmlSafe, icon, pubDate, commits } of items) { + const when = dataDomain.timeAgo(pubDate); + + const commitsHtml = commits.length === 0 ? '' : + `
` + + commits.map(({ sha, href, msg }) => ` +
+ ${dataDomain.esc(sha)} + ${dataDomain.esc(msg)} +
`).join('') + + `
`; + + const el = document.createElement('div'); + el.className = 'activity-item'; + el.innerHTML = ` +
${icon}
+
+
+
${titleHtmlSafe}
+ ${when} +
+ ${commitsHtml} +
+ `; + feed.appendChild(el); + } + }, + + async render() { + const baseUrl = httpService.baseUrl; + + let xmlDoc; + try { + xmlDoc = await httpService.fetchRss(); + } catch (e) { + console.error('Gitea landing: RSS fetch failed', e); + const grid = document.getElementById('repo-grid'); + const feed = document.getElementById('activity-feed'); + if (grid) grid.innerHTML = `
Could not load feed (${e.message}). Browse manually →
`; + if (feed) feed.innerHTML = `
Could not load activity (${e.message})
`; + return; + } + + await Promise.all([ + this.renderRepos(xmlDoc), + this.renderActivity(xmlDoc), + ]); + }, +}; + +document.addEventListener('DOMContentLoaded', () => uiRendering.render());