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" .}}
+
+
+
+
+{{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 = ``;
+ return;
+ }
+ const json = await resp.json();
+ repos = json.data || json;
+ } catch (e) {
+ console.error('Gitea landing: repo fetch failed', e);
+ grid.innerHTML = ``;
+ 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 = `
+
+ ${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}
+
+ ${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" .}}
-
-
-
-
- {{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 = ``;
- return;
- }
- const json = await resp.json();
- repos = json.data || json;
- } catch (e) {
- console.error('Gitea landing: repo fetch failed', e);
- grid.innerHTML = ``;
- 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 = `
-
- ${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}
-
- ${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