diff --git a/public/app.js b/public/app.js index 31a58e9..3b2883e 100644 --- a/public/app.js +++ b/public/app.js @@ -934,6 +934,7 @@ document.addEventListener("DOMContentLoaded", () => { if (CURRENT_PAGE === 'wiki') initWikiPage(); if (CURRENT_PAGE === 'agents') initAgentsPage(); if (CURRENT_PAGE === 'usage') initUsagePage(); + if (CURRENT_PAGE === 'gitea') initGiteaPage(); }); // ============ GITEA DASHBOARD ============ @@ -1140,3 +1141,130 @@ if (document.querySelector('.gitea-dashboard')) { } }, 30000); } + +// ============ GITEA PAGE ============ +let giteaData = { repos: [], prs: [], activity: [] }; + +function initGiteaPage() { + loadGiteaData(); + + document.getElementById('refresh-gitea')?.addEventListener('click', loadGiteaData); + + // Tab switching + document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', (e) => { + const tab = e.target.dataset.tab; + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); + e.target.classList.add('active'); + document.getElementById(`tab-${tab}`)?.classList.add('active'); + }); + }); +} + +async function loadGiteaData() { + try { + const [swarmRes, reviewsRes, activityRes] = await Promise.all([ + fetch('/api/gitea/swarm'), + fetch('/api/gitea/reviews'), + fetch('/api/gitea/activity') + ]); + + giteaData.repos = await swarmRes.json(); + giteaData.prs = await reviewsRes.json(); + giteaData.activity = await activityRes.json(); + + renderGiteaStats(); + renderGiteaRepos(); + renderGiteaPRs(); + renderGiteaActivity(); + } catch (err) { + console.error('Failed to load Gitea data:', err); + } +} + +function renderGiteaStats() { + document.getElementById('stat-repos').textContent = giteaData.repos.length; + + const totalPRs = giteaData.repos.reduce((sum, r) => sum + (r.open_prs || 0), 0); + document.getElementById('stat-prs').textContent = totalPRs; + + const pendingReviews = giteaData.prs.filter(p => p.review_required).length; + document.getElementById('stat-reviews').textContent = pendingReviews; +} + +function renderGiteaRepos() { + const grid = document.getElementById('repos-grid'); + if (!grid) return; + + if (!giteaData.repos.length) { + grid.innerHTML = '

No repositories found

'; + return; + } + + grid.innerHTML = giteaData.repos.map(repo => ` +
+

${escapeHtml(repo.name)}

+

${escapeHtml(repo.full_name || '')}

+
+ ⭐ ${repo.stars || 0} + 🔀 ${repo.open_prs || 0} + 🐛 ${repo.open_issues || 0} +
+
Updated: ${repo.updated_at ? new Date(repo.updated_at).toLocaleDateString() : 'N/A'}
+ View → +
+ `).join(''); +} + +function renderGiteaPRs() { + const list = document.getElementById('prs-list'); + if (!list) return; + + // Collect all PRs from all repos + const allPRs = []; + giteaData.repos.forEach(repo => { + if (repo.prs) { + repo.prs.forEach(pr => { + allPRs.push({ ...pr, repo: repo.name }); + }); + } + }); + + if (!allPRs.length) { + list.innerHTML = '

No open pull requests

'; + return; + } + + list.innerHTML = allPRs.map(pr => ` +
+
+ #${pr.number} + ${escapeHtml(pr.title)} + ${pr.state} +
+
+ 📦 ${pr.repo} + 👤 ${pr.user?.login || 'Unknown'} +
+
+ `).join(''); +} + +function renderGiteaActivity() { + const list = document.getElementById('activity-list'); + if (!list) return; + + if (!giteaData.activity || !giteaData.activity.length) { + list.innerHTML = '

No recent activity

'; + return; + } + + list.innerHTML = giteaData.activity.slice(0, 20).map(activity => ` +
+ ${activity.type || '📝'} + ${escapeHtml(activity.message || activity.repo?.name || 'Activity')} + ${activity.created_at ? new Date(activity.created_at).toLocaleString() : ''} +
+ `).join(''); +} diff --git a/public/styles.css b/public/styles.css index 09575bd..1c15ae8 100644 --- a/public/styles.css +++ b/public/styles.css @@ -783,3 +783,158 @@ label { page-break-inside: avoid; } } + +/* ============ GITEA PAGE ============ */ +.gitea-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; +} + +.gitea-tabs { + display: flex; + gap: 0.5rem; + margin-bottom: 1.5rem; +} + +.tab-btn { + padding: 0.5rem 1rem; + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + color: var(--text-secondary); + cursor: pointer; + transition: all 0.2s ease; +} + +.tab-btn:hover { + background: var(--hover-bg); +} + +.tab-btn.active { + background: var(--primary); + color: white; + border-color: var(--primary); +} + +.tab-content { + display: none; +} + +.tab-content.active { + display: block; +} + +.repos-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1rem; +} + +.repo-card { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 12px; + padding: 1.25rem; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.repo-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15); +} + +.repo-card h4 { + margin: 0 0 0.5rem; + color: var(--text-primary); +} + +.repo-fullname { + font-size: 0.85rem; + color: var(--text-muted); + margin-bottom: 0.75rem; +} + +.repo-meta { + display: flex; + gap: 1rem; + margin-bottom: 0.75rem; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.repo-updated { + font-size: 0.8rem; + color: var(--text-muted); + margin-bottom: 1rem; +} + +.prs-list, .activity-list { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.pr-item, .activity-item { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.pr-header { + display: flex; + align-items: center; + gap: 0.75rem; +} + +.pr-number { + font-weight: 600; + color: var(--primary); +} + +.pr-title { + flex: 1; + color: var(--text-primary); +} + +.pr-status { + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + text-transform: uppercase; +} + +.pr-status.open { + background: #238636; + color: white; +} + +.pr-status.merged { + background: #8957e5; + color: white; +} + +.pr-meta, .activity-time { + font-size: 0.85rem; + color: var(--text-muted); + display: flex; + gap: 1rem; +} + +.activity-item { + flex-direction: row; + align-items: center; +} + +.activity-type { + font-size: 1.25rem; +} + +.activity-desc { + flex: 1; +} diff --git a/server.js b/server.js index 636a902..67facb4 100644 --- a/server.js +++ b/server.js @@ -80,6 +80,7 @@ function renderPage(viewName, activeTab, pageTitle) { wikiActive: activeTab === 'wiki' ? 'active' : '', agentsActive: activeTab === 'agents' ? 'active' : '', usageActive: activeTab === 'usage' ? 'active' : '', + giteaActive: activeTab === 'gitea' ? 'active' : '', markedScript: viewName === 'wiki' ? '' : '', @@ -108,6 +109,9 @@ app.get('/agents', (req, res) => { }); app.get('/usage', (req, res) => { +app.get('/gitea', (req, res) => { + res.send(renderPage('gitea', 'gitea', 'OpenClaw Agent Fleet Dashboard - Gitea')); +}); res.send(renderPage('usage', 'usage', 'OpenClaw Agent Fleet Dashboard - Usage')); }); diff --git a/views/gitea.html b/views/gitea.html index cb1c263..c0fdb7b 100644 --- a/views/gitea.html +++ b/views/gitea.html @@ -1,54 +1,41 @@ -
+
-

🔧 Gitea Integration

-

Real-time repository and PR tracking

+

🔗 Gitea Swarm Coordination

+
+ +
+
+ +
+
+

Repositories

+
0
+
+
+

Open PRs

+
0
+
+
+

Pending Reviews

+
0
+
- - - + + +
-
- -
-
-
-

Total Repos

-
-
-
-
-

Open PRs

-
-
-
-
-

Open Issues

-
-
-
-
-

Total Branches

-
-
-
-
- -
-

Loading repositories...

-
+
+
+
- - -
-
-

Loading pending reviews...

-
+
+
- - -
-
-

Loading recent activity...

-
+
+
-
+