(async function loadRepos() { const grid = document.getElementById('repo-grid'); if (!grid) 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) { 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; } const items = Array.from(doc.querySelectorAll('channel > item')); if (items.length === 0) { grid.innerHTML = `
No activity found.
`; return; } // 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 }); } 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; while (node) { const t = (node.textContent || '').trim(); if (t) { commitMsg = t; break; } node = node.nextSibling; } if (!commitMsg) commitMsg = firstAnchor.textContent.trim().slice(0, 7); } const shortName = repoName.includes('/') ? repoName.split('/').pop() : repoName; 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); } })(); (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})
`; 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; } 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) : ''; // Strip HTML from title for plain text display const titleDoc = new DOMParser().parseFromString(title, 'text/html'); const titleText = titleDoc.body.textContent || title; 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 el = document.createElement('div'); el.className = 'activity-item'; el.innerHTML = `
${icon}
${esc(titleText)}
${commitsHtml}
${when} `; feed.appendChild(el); } })();