diff --git a/.gitea/workflows/apply-kubernetes.yml b/.gitea/workflows/apply-kubernetes.yml index 68db561..a1b9493 100644 --- a/.gitea/workflows/apply-kubernetes.yml +++ b/.gitea/workflows/apply-kubernetes.yml @@ -54,10 +54,20 @@ jobs: env: CLOUDFLARED_GITEA_TOKEN: ${{ secrets.CLOUDFLARED_GITEA_TOKEN }} run: | + kubectl apply -f kubernetes/gitea/namespace.yml + kubectl create configmap gitea-landing-page \ + -n gitea \ + --from-file=home.tmpl=kubernetes/gitea/landingpage.html \ + --from-file=custom-landing.css=kubernetes/gitea/landingpage.css \ + --from-file=custom-landing.js=kubernetes/gitea/landingpage.js \ + --dry-run=client -o yaml | kubectl apply -f - + for file in kubernetes/gitea/*.yml; do cat "$file" | envsubst | kubectl apply -f - done + kubectl rollout restart deployment/gitea-web -n gitea + notify-on-failure: runs-on: home-server needs: update-infrastructure diff --git a/kubernetes/gitea/landingpage.css b/kubernetes/gitea/landingpage.css new file mode 100644 index 0000000..1462bf6 --- /dev/null +++ b/kubernetes/gitea/landingpage.css @@ -0,0 +1,296 @@ +#alex-landing { + min-height: 100vh; + background: #0d1117; + color: #e6edf3; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; +} + +/* Hero */ +.hero { + display: flex; + justify-content: center; + align-items: center; + padding: 80px 24px 60px; + text-align: center; +} +.hero-inner { + max-width: 640px; +} +.hero h1 { + font-size: 3rem; + font-weight: 800; + margin: 0 0 12px; + background: linear-gradient(135deg, #e6edf3 0%, #58a6ff 100%); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; +} + +.hero-links { + display: flex; + gap: 12px; + justify-content: center; + flex-wrap: wrap; +} +.btn-primary { + padding: 10px 24px; + border-radius: 8px; + background: #238636; + color: #fff; + font-weight: 600; + font-size: 0.95rem; + text-decoration: none; + transition: background 0.2s; +} +.btn-primary:hover { background: #2ea043; } +.btn-ghost { + padding: 10px 24px; + border-radius: 8px; + border: 1px solid #30363d; + color: #e6edf3; + font-weight: 600; + font-size: 0.95rem; + text-decoration: none; + transition: border-color 0.2s, background 0.2s; +} +.btn-ghost:hover { border-color: #58a6ff; background: #58a6ff11; } + +/* Projects section */ +.projects-section { + max-width: 1100px; + margin: 0 auto; + padding: 0 24px 80px; +} +.section-header { + margin-bottom: 24px; + display: flex; + align-items: baseline; + gap: 12px; + flex-wrap: wrap; +} +.section-header h2 { + font-size: 1.5rem; + font-weight: 700; + margin: 0; + color: #e6edf3; +} +.subtitle { + font-size: 0.9rem; + color: #8b949e; +} + +/* Grid */ +.repo-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 16px; +} + +/* Skeleton loaders */ +.skeleton-card { + height: 160px; + border-radius: 12px; + background: linear-gradient(90deg, #161b22 25%, #21262d 50%, #161b22 75%); + background-size: 200% 100%; + animation: shimmer 1.4s infinite; +} +@keyframes shimmer { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } +} + +/* Repo cards */ +.repo-card { + background: #161b22; + border: 1px solid #21262d; + border-radius: 12px; + padding: 20px; + text-decoration: none; + color: inherit; + display: flex; + flex-direction: column; + gap: 10px; + transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; + cursor: pointer; +} +.repo-card:hover { + border-color: #58a6ff; + transform: translateY(-2px); + box-shadow: 0 8px 24px #58a6ff1a; +} +.repo-card-header { + display: flex; + align-items: center; + gap: 10px; +} +.repo-icon { + font-size: 1.1rem; + flex-shrink: 0; +} +.repo-name { + font-size: 1rem; + font-weight: 600; + color: #58a6ff; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.repo-private { + font-size: 0.7rem; + background: #21262d; + border: 1px solid #30363d; + border-radius: 4px; + padding: 1px 6px; + color: #8b949e; + flex-shrink: 0; +} +.repo-desc { + font-size: 0.875rem; + color: #8b949e; + line-height: 1.5; + flex: 1; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} +.repo-commit { + font-size: 0.8rem; + color: #6e7681; + border-top: 1px solid #21262d; + padding-top: 10px; + display: flex; + align-items: center; + gap: 6px; + overflow: hidden; +} +.commit-dot { + width: 6px; + height: 6px; + border-radius: 50%; + background: #238636; + flex-shrink: 0; +} +.commit-msg { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + flex: 1; +} +.commit-time { + color: #484f58; + flex-shrink: 0; +} +.repo-meta { + display: flex; + gap: 14px; + font-size: 0.8rem; + color: #6e7681; +} +.repo-meta span { display: flex; align-items: center; gap: 4px; } + +/* Error state */ +.error-msg { + grid-column: 1 / -1; + text-align: center; + padding: 40px; + color: #8b949e; + font-size: 0.95rem; +} + +/* Activity section */ +.activity-section { + max-width: 1100px; + margin: 0 auto; + padding: 0 24px 80px; +} +.view-all-link { + font-size: 0.85rem; + color: #58a6ff; + text-decoration: none; + margin-left: auto; +} +.view-all-link:hover { text-decoration: underline; } +.activity-feed { + display: flex; + flex-direction: column; + gap: 0; + border: 1px solid #21262d; + border-radius: 12px; + overflow: hidden; +} +.skeleton-activity { + height: 52px; + background: linear-gradient(90deg, #161b22 25%, #21262d 50%, #161b22 75%); + background-size: 200% 100%; + animation: shimmer 1.4s infinite; + border-top: 1px solid #0d1117; +} +.skeleton-activity:first-child { border-top: none; } +.activity-item { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 12px 16px; + background: #161b22; + border-top: 1px solid #21262d; + font-size: 0.875rem; + transition: background 0.15s; +} +.activity-item:first-child { border-top: none; } +.activity-item:hover { background: #1c2128; } +.activity-op-icon { + flex-shrink: 0; + width: 28px; + height: 28px; + border-radius: 50%; + background: #21262d; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.8rem; + margin-top: 1px; +} +.activity-body { flex: 1; min-width: 0; } +.activity-headline { color: #e6edf3; line-height: 1.5; } +.activity-headline a { + color: #58a6ff; + text-decoration: none; + font-weight: 600; +} +.activity-headline a:hover { text-decoration: underline; } +.activity-commits { + margin-top: 6px; + display: flex; + flex-direction: column; + gap: 3px; +} +.activity-commit-line { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.8rem; + color: #8b949e; + overflow: hidden; +} +.activity-commit-sha { + font-family: monospace; + font-size: 0.75rem; + color: #58a6ff; + flex-shrink: 0; + text-decoration: none; +} +.activity-commit-sha:hover { text-decoration: underline; } +.activity-commit-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.activity-time { + flex-shrink: 0; + font-size: 0.78rem; + color: #484f58; + margin-left: auto; + padding-left: 12px; + white-space: nowrap; +} diff --git a/kubernetes/gitea/landingpage.html b/kubernetes/gitea/landingpage.html new file mode 100644 index 0000000..b54e3c6 --- /dev/null +++ b/kubernetes/gitea/landingpage.html @@ -0,0 +1,51 @@ +{{template "base/head" .}} + +
+ +
+
+

Alex Mickelson

+ +
+
+ +
+
+

Recent Projects

+ Latest commits across all repositories +
+
+
+
+
+
+
+
+
+
+ +
+
+

Recent Activity

+ What Alex has been up to + View full profile β†’ +
+
+
+
+
+
+
+
+
+ +
+ + +{{template "base/footer" .}} diff --git a/kubernetes/gitea/landingpage.js b/kubernetes/gitea/landingpage.js new file mode 100644 index 0000000..ec4d84a --- /dev/null +++ b/kubernetes/gitea/landingpage.js @@ -0,0 +1,217 @@ +(async function () { + 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 escapeHtml(str) { + return (str || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } + + function langDot(lang) { + const colors = { + Go:'#00ADD8', Python:'#3572A5', JavaScript:'#f1e05a', TypeScript:'#2b7489', + Rust:'#dea584', Java:'#b07219', 'C#':'#178600', Nix:'#7e7eff', + Shell:'#89e051', HTML:'#e34c26', CSS:'#563d7c', Elixir:'#6e4a7e', + }; + return colors[lang] + ? `` + : 'πŸ“¦'; + } + + async function fetchJson(url) { + const resp = await fetch(url, { credentials: 'include' }); + if (!resp.ok) throw new Error(resp.status); + return resp.json(); + } + + async function getLatestCommit(repo) { + try { + const commits = await fetchJson( + `${baseUrl}/api/v1/repos/${encodeURIComponent(repo.full_name)}/commits?limit=1&page=1` + ); + if (commits && commits.length > 0) return commits[0]; + } catch (_) {} + return null; + } + + async function loadRepos() { + let repos; + try { + const resp = await fetch(`${baseUrl}/api/v1/repos/search?sort=updated&order=desc&limit=12`, { + credentials: 'include', + }); + if (!resp.ok) { + const msg = resp.status === 401 || resp.status === 403 + ? `Sign in to see repositories (HTTP ${resp.status})` + : `API error: HTTP ${resp.status}`; + grid.innerHTML = `
${msg}. Browse manually β†’
`; + return; + } + const json = await resp.json(); + repos = json.data || json; + } catch (e) { + console.error('Gitea landing: repo fetch failed', e); + grid.innerHTML = `
+ Could not load repositories (${e.message}). Browse manually β†’ +
`; + return; + } + + if (!repos || repos.length === 0) { + grid.innerHTML = `
No public repositories found.
`; + return; + } + + repos.sort((a, b) => new Date(b.updated) - new Date(a.updated)); + + grid.innerHTML = ''; + for (const repo of repos) { + const card = document.createElement('a'); + card.className = 'repo-card'; + card.href = `${baseUrl}/${escapeHtml(repo.full_name)}`; + card.dataset.repoName = repo.full_name; + card.innerHTML = ` +
+ ${langDot(repo.language)} + ${escapeHtml(repo.name)} + ${repo.private ? 'private' : ''} +
+
${escapeHtml(repo.description) || 'No description'}
+
+ ${repo.language ? `${langDot(repo.language)} ${escapeHtml(repo.language)}` : ''} + ⭐ ${repo.stars_count || 0} + 🍴 ${repo.forks_count || 0} + πŸ•’ ${timeAgo(repo.updated)} +
+
+ + loading commit… +
+ `.trim(); + grid.appendChild(card); + } + + await Promise.all(repos.map(async (repo) => { + const commit = await getLatestCommit(repo); + const el = document.getElementById(`commit-${CSS.escape(repo.full_name)}`); + if (!el) return; + if (commit) { + const msg = commit.commit?.message?.split('\n')[0] || ''; + const when = timeAgo(commit.commit?.committer?.date || commit.created); + el.innerHTML = ` + + ${escapeHtml(msg)} + ${when} + `; + } else { + el.innerHTML = `no commits visible`; + } + })); + } + + loadRepos(); +})(); + +(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) : ''; + + let icon = '⚑'; + const t = title.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 = 'πŸš€'; + + let commitsHtml = ''; + const descDoc = new DOMParser().parseFromString(description, 'text/html'); + const commitEls = descDoc.querySelectorAll('li'); + if (commitEls.length > 0) { + commitsHtml = '
' + + Array.from(commitEls).slice(0, 3).map(li => { + const anchor = li.querySelector('a'); + const sha = anchor ? esc(anchor.textContent.trim().slice(0, 7)) : ''; + const shaHref = anchor ? esc(anchor.getAttribute('href') || '#') : '#'; + const msg = esc(li.textContent.replace(anchor?.textContent || '', '').trim().replace(/^[-–:]\s*/, '')); + return `
+ ${sha ? `${sha}` : ''} + ${msg} +
`; + }).join('') + + (commitEls.length > 3 ? `
+${commitEls.length - 3} more
` : '') + + '
'; + } + + const el = document.createElement('div'); + el.className = 'activity-item'; + el.innerHTML = ` +
${icon}
+
+
${esc(title)}
+ ${commitsHtml} +
+ ${when} + `; + feed.appendChild(el); + } +})(); diff --git a/kubernetes/gitea/landingpage.yml b/kubernetes/gitea/landingpage.yml deleted file mode 100644 index eb505b3..0000000 --- a/kubernetes/gitea/landingpage.yml +++ /dev/null @@ -1,580 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: gitea-landing-page - namespace: gitea -data: - home.tmpl: | - {{template "base/head" .}} - -
- -
-
-

Alex Mickelson

- -
-
- -
-
-

Recent Projects

- Latest commits across all repositories -
-
-
-
-
-
-
-
-
-
- -
-
-

Recent Activity

- What Alex has been up to - View full profile β†’ -
-
-
-
-
-
-
-
-
- -
- - - {{template "base/footer" .}} - - custom-landing.css: | - #alex-landing { - min-height: 100vh; - background: #0d1117; - color: #e6edf3; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; - } - - /* Hero */ - .hero { - display: flex; - justify-content: center; - align-items: center; - padding: 80px 24px 60px; - text-align: center; - } - .hero-inner { - max-width: 640px; - } - .hero h1 { - font-size: 3rem; - font-weight: 800; - margin: 0 0 12px; - background: linear-gradient(135deg, #e6edf3 0%, #58a6ff 100%); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; - } - - .hero-links { - display: flex; - gap: 12px; - justify-content: center; - flex-wrap: wrap; - } - .btn-primary { - padding: 10px 24px; - border-radius: 8px; - background: #238636; - color: #fff; - font-weight: 600; - font-size: 0.95rem; - text-decoration: none; - transition: background 0.2s; - } - .btn-primary:hover { background: #2ea043; } - .btn-ghost { - padding: 10px 24px; - border-radius: 8px; - border: 1px solid #30363d; - color: #e6edf3; - font-weight: 600; - font-size: 0.95rem; - text-decoration: none; - transition: border-color 0.2s, background 0.2s; - } - .btn-ghost:hover { border-color: #58a6ff; background: #58a6ff11; } - - /* Projects section */ - .projects-section { - max-width: 1100px; - margin: 0 auto; - padding: 0 24px 80px; - } - .section-header { - margin-bottom: 24px; - display: flex; - align-items: baseline; - gap: 12px; - flex-wrap: wrap; - } - .section-header h2 { - font-size: 1.5rem; - font-weight: 700; - margin: 0; - color: #e6edf3; - } - .subtitle { - font-size: 0.9rem; - color: #8b949e; - } - - /* Grid */ - .repo-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); - gap: 16px; - } - - /* Skeleton loaders */ - .skeleton-card { - height: 160px; - border-radius: 12px; - background: linear-gradient(90deg, #161b22 25%, #21262d 50%, #161b22 75%); - background-size: 200% 100%; - animation: shimmer 1.4s infinite; - } - @keyframes shimmer { - 0% { background-position: 200% 0; } - 100% { background-position: -200% 0; } - } - - /* Repo cards */ - .repo-card { - background: #161b22; - border: 1px solid #21262d; - border-radius: 12px; - padding: 20px; - text-decoration: none; - color: inherit; - display: flex; - flex-direction: column; - gap: 10px; - transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s; - cursor: pointer; - } - .repo-card:hover { - border-color: #58a6ff; - transform: translateY(-2px); - box-shadow: 0 8px 24px #58a6ff1a; - } - .repo-card-header { - display: flex; - align-items: center; - gap: 10px; - } - .repo-icon { - font-size: 1.1rem; - flex-shrink: 0; - } - .repo-name { - font-size: 1rem; - font-weight: 600; - color: #58a6ff; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .repo-private { - font-size: 0.7rem; - background: #21262d; - border: 1px solid #30363d; - border-radius: 4px; - padding: 1px 6px; - color: #8b949e; - flex-shrink: 0; - } - .repo-desc { - font-size: 0.875rem; - color: #8b949e; - line-height: 1.5; - flex: 1; - overflow: hidden; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - } - .repo-commit { - font-size: 0.8rem; - color: #6e7681; - border-top: 1px solid #21262d; - padding-top: 10px; - display: flex; - align-items: center; - gap: 6px; - overflow: hidden; - } - .commit-dot { - width: 6px; - height: 6px; - border-radius: 50%; - background: #238636; - flex-shrink: 0; - } - .commit-msg { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - flex: 1; - } - .commit-time { - color: #484f58; - flex-shrink: 0; - } - .repo-meta { - display: flex; - gap: 14px; - font-size: 0.8rem; - color: #6e7681; - } - .repo-meta span { display: flex; align-items: center; gap: 4px; } - - /* Error state */ - .error-msg { - grid-column: 1 / -1; - text-align: center; - padding: 40px; - color: #8b949e; - font-size: 0.95rem; - } - - /* Activity section */ - .activity-section { - max-width: 1100px; - margin: 0 auto; - padding: 0 24px 80px; - } - .view-all-link { - font-size: 0.85rem; - color: #58a6ff; - text-decoration: none; - margin-left: auto; - } - .view-all-link:hover { text-decoration: underline; } - .activity-feed { - display: flex; - flex-direction: column; - gap: 0; - border: 1px solid #21262d; - border-radius: 12px; - overflow: hidden; - } - .skeleton-activity { - height: 52px; - background: linear-gradient(90deg, #161b22 25%, #21262d 50%, #161b22 75%); - background-size: 200% 100%; - animation: shimmer 1.4s infinite; - border-top: 1px solid #0d1117; - } - .skeleton-activity:first-child { border-top: none; } - .activity-item { - display: flex; - align-items: flex-start; - gap: 12px; - padding: 12px 16px; - background: #161b22; - border-top: 1px solid #21262d; - font-size: 0.875rem; - transition: background 0.15s; - } - .activity-item:first-child { border-top: none; } - .activity-item:hover { background: #1c2128; } - .activity-op-icon { - flex-shrink: 0; - width: 28px; - height: 28px; - border-radius: 50%; - background: #21262d; - display: flex; - align-items: center; - justify-content: center; - font-size: 0.8rem; - margin-top: 1px; - } - .activity-body { flex: 1; min-width: 0; } - .activity-headline { color: #e6edf3; line-height: 1.5; } - .activity-headline a { - color: #58a6ff; - text-decoration: none; - font-weight: 600; - } - .activity-headline a:hover { text-decoration: underline; } - .activity-commits { - margin-top: 6px; - display: flex; - flex-direction: column; - gap: 3px; - } - .activity-commit-line { - display: flex; - align-items: center; - gap: 8px; - font-size: 0.8rem; - color: #8b949e; - overflow: hidden; - } - .activity-commit-sha { - font-family: monospace; - font-size: 0.75rem; - color: #58a6ff; - flex-shrink: 0; - text-decoration: none; - } - .activity-commit-sha:hover { text-decoration: underline; } - .activity-commit-text { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - .activity-time { - flex-shrink: 0; - font-size: 0.78rem; - color: #484f58; - margin-left: auto; - padding-left: 12px; - white-space: nowrap; - } - - custom-landing.js: | - (async function () { - 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 escapeHtml(str) { - return (str || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); - } - - function langDot(lang) { - const colors = { - Go:'#00ADD8', Python:'#3572A5', JavaScript:'#f1e05a', TypeScript:'#2b7489', - Rust:'#dea584', Java:'#b07219', 'C#':'#178600', Nix:'#7e7eff', - Shell:'#89e051', HTML:'#e34c26', CSS:'#563d7c', Elixir:'#6e4a7e', - }; - return colors[lang] - ? `` - : 'πŸ“¦'; - } - - async function fetchJson(url) { - const resp = await fetch(url, { credentials: 'include' }); - if (!resp.ok) throw new Error(resp.status); - return resp.json(); - } - - async function getLatestCommit(repo) { - try { - const commits = await fetchJson( - `${baseUrl}/api/v1/repos/${encodeURIComponent(repo.full_name)}/commits?limit=1&page=1` - ); - if (commits && commits.length > 0) return commits[0]; - } catch (_) {} - return null; - } - - async function loadRepos() { - let repos; - try { - const resp = await fetch(`${baseUrl}/api/v1/repos/search?sort=updated&order=desc&limit=12`, { - credentials: 'include', - }); - if (!resp.ok) { - const msg = resp.status === 401 || resp.status === 403 - ? `Sign in to see repositories (HTTP ${resp.status})` - : `API error: HTTP ${resp.status}`; - grid.innerHTML = `
${msg}. Browse manually β†’
`; - return; - } - const json = await resp.json(); - repos = json.data || json; - } catch (e) { - console.error('Gitea landing: repo fetch failed', e); - grid.innerHTML = `
- Could not load repositories (${e.message}). Browse manually β†’ -
`; - return; - } - - if (!repos || repos.length === 0) { - grid.innerHTML = `
No public repositories found.
`; - return; - } - - // Sort by updated desc (API may already do this) - repos.sort((a, b) => new Date(b.updated) - new Date(a.updated)); - - // Render cards immediately, then fill in commits async - grid.innerHTML = ''; - for (const repo of repos) { - const card = document.createElement('a'); - card.className = 'repo-card'; - card.href = `${baseUrl}/${escapeHtml(repo.full_name)}`; - card.dataset.repoName = repo.full_name; - card.innerHTML = ` -
- ${langDot(repo.language)} - ${escapeHtml(repo.name)} - ${repo.private ? 'private' : ''} -
-
${escapeHtml(repo.description) || 'No description'}
-
- ${repo.language ? `${langDot(repo.language)} ${escapeHtml(repo.language)}` : ''} - ⭐ ${repo.stars_count || 0} - 🍴 ${repo.forks_count || 0} - πŸ•’ ${timeAgo(repo.updated)} -
-
- - loading commit… -
- `.trim(); - grid.appendChild(card); - } - - // Fetch commits in parallel - await Promise.all(repos.map(async (repo) => { - const commit = await getLatestCommit(repo); - const el = document.getElementById(`commit-${CSS.escape(repo.full_name)}`); - if (!el) return; - if (commit) { - const msg = commit.commit?.message?.split('\n')[0] || ''; - const when = timeAgo(commit.commit?.committer?.date || commit.created); - el.innerHTML = ` - - ${escapeHtml(msg)} - ${when} - `; - } else { - el.innerHTML = `no commits visible`; - } - })); - } - - loadRepos(); - })(); - - (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) : ''; - - // Guess icon from title - let icon = '⚑'; - const t = title.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 = 'πŸš€'; - - // Extract commit lines from description HTML - let commitsHtml = ''; - const descDoc = new DOMParser().parseFromString(description, 'text/html'); - const commitEls = descDoc.querySelectorAll('li'); - if (commitEls.length > 0) { - commitsHtml = '
' + - Array.from(commitEls).slice(0, 3).map(li => { - const anchor = li.querySelector('a'); - const sha = anchor ? esc(anchor.textContent.trim().slice(0, 7)) : ''; - const shaHref = anchor ? esc(anchor.getAttribute('href') || '#') : '#'; - const msg = esc(li.textContent.replace(anchor?.textContent || '', '').trim().replace(/^[-–:]\s*/, '')); - return `
- ${sha ? `${sha}` : ''} - ${msg} -
`; - }).join('') + - (commitEls.length > 3 ? `
+${commitEls.length - 3} more
` : '') + - '
'; - } - - const el = document.createElement('div'); - el.className = 'activity-item'; - el.innerHTML = ` -
${icon}
-
-
${esc(title)}
- ${commitsHtml} -
- ${when} - `; - feed.appendChild(el); - } - })(); diff --git a/kubernetes/gitea/0-namespace.yml b/kubernetes/gitea/namespace.yml similarity index 100% rename from kubernetes/gitea/0-namespace.yml rename to kubernetes/gitea/namespace.yml