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 = ``;
- 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 = ``;
- 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(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(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 ``;
- }).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 }) => `
+
`).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 = ``;
+ 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());