371 lines
11 KiB
JavaScript
371 lines
11 KiB
JavaScript
/**
|
|
* Gitea Routes Module
|
|
* Adds Gitea integration routes to Express app
|
|
*/
|
|
|
|
const GiteaIntegration = require('./gitea-integration.js');
|
|
|
|
function setupGiteaRoutes(app) {
|
|
// Initialize Gitea client
|
|
const giteaConfig = {
|
|
baseUrl: process.env.GITEA_URL || 'https://gitea.tophermayor.com',
|
|
token: process.env.GITEA_TOKEN,
|
|
owner: 'TopherMayor',
|
|
cacheTimeout: 30000
|
|
};
|
|
|
|
const gitea = new GiteaIntegration(giteaConfig);
|
|
|
|
|
|
// Gitea API routes
|
|
app.get('/api/gitea/swarm', async (req, res) => {
|
|
try {
|
|
const summary = await gitea.getSwarmSummary();
|
|
res.json(summary);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/reviews', async (req, res) => {
|
|
try {
|
|
const reviews = await gitea.getPendingReviews();
|
|
res.json(reviews);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/activity', async (req, res) => {
|
|
try {
|
|
const activity = await gitea.getRecentActivity();
|
|
res.json(activity);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/user', async (req, res) => {
|
|
try {
|
|
const user = await gitea.getUser();
|
|
res.json(user);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/repos/:repo', async (req, res) => {
|
|
try {
|
|
const repo = await gitea.getRepo(req.params.repo);
|
|
res.json(repo);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/repos/:repo/pulls', async (req, res) => {
|
|
try {
|
|
const state = req.query.state || 'open';
|
|
const prs = await gitea.getPullRequests(req.params.repo, state);
|
|
res.json(prs);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/repos/:repo/issues', async (req, res) => {
|
|
try {
|
|
const state = req.query.state || 'open';
|
|
const issues = await gitea.getIssues(req.params.repo, state);
|
|
res.json(issues);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/repos/:repo/commits', async (req, res) => {
|
|
try {
|
|
const branch = req.query.branch || 'main';
|
|
const limit = parseInt(req.query.limit) || 10;
|
|
const commits = await gitea.getCommits(req.params.repo, branch, limit);
|
|
res.json(commits);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.get('/api/gitea/repos/:repo/branches', async (req, res) => {
|
|
try {
|
|
const branches = await gitea.getBranches(req.params.repo);
|
|
res.json(branches);
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
app.post('/api/gitea/cache/clear', (req, res) => {
|
|
gitea.clearCache();
|
|
res.json({ success: true, message: 'Cache cleared' });
|
|
});
|
|
|
|
|
|
// Repository health check
|
|
app.get('/api/gitea/health', async (req, res) => {
|
|
try {
|
|
const repos = await gitea.getRepos();
|
|
const health = [];
|
|
|
|
for (const repo of repos) {
|
|
const updated = new Date(repo.updated_at);
|
|
const daysSinceUpdate = Math.floor((Date.now() - updated.getTime()) / (1000 * 60 * 60 * 24));
|
|
|
|
let status = 'healthy';
|
|
if (daysSinceUpdate > 30) status = 'stale';
|
|
else if (daysSinceUpdate > 7) status = 'inactive';
|
|
|
|
let openPRs = 0;
|
|
try {
|
|
const prs = await gitea.getPullRequests(repo.name, 'open');
|
|
openPRs = prs.length;
|
|
} catch {}
|
|
|
|
health.push({
|
|
name: repo.name,
|
|
status,
|
|
daysSinceUpdate,
|
|
updated_at: repo.updated_at,
|
|
openPRs,
|
|
html_url: repo.html_url
|
|
});
|
|
}
|
|
|
|
res.json({
|
|
repos: health,
|
|
summary: {
|
|
total: health.length,
|
|
healthy: health.filter(r => r.status === 'healthy').length,
|
|
inactive: health.filter(r => r.status === 'inactive').length,
|
|
stale: health.filter(r => r.status === 'stale').length
|
|
}
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
|
|
// GitOps deployment status
|
|
app.get('/api/gitops/deployments', async (req, res) => {
|
|
try {
|
|
const repos = await gitea.getRepos();
|
|
const deployments = [];
|
|
|
|
for (const repo of repos) {
|
|
try {
|
|
// Get latest release if exists
|
|
const releases = await gitea.request(`/api/v1/repos/${giteaConfig.owner}/${repo.name}/releases?limit=1`);
|
|
const latestRelease = releases[0];
|
|
|
|
deployments.push({
|
|
repo: repo.name,
|
|
full_name: repo.full_name,
|
|
updated_at: repo.updated_at,
|
|
latest_release: latestRelease ? {
|
|
tag: latestRelease.tag_name,
|
|
created: latestRelease.created_at,
|
|
url: latestRelease.html_url
|
|
} : null,
|
|
html_url: repo.html_url
|
|
});
|
|
} catch {
|
|
deployments.push({
|
|
repo: repo.name,
|
|
full_name: repo.full_name,
|
|
updated_at: repo.updated_at,
|
|
latest_release: null,
|
|
html_url: repo.html_url
|
|
});
|
|
}
|
|
}
|
|
|
|
res.json({
|
|
deployments,
|
|
lastUpdated: new Date().toISOString()
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Get deployment status for a specific repo
|
|
app.get('/api/gitops/deployments/:repo', async (req, res) => {
|
|
const { repo } = req.params;
|
|
|
|
try {
|
|
const releases = await gitea.request(`/api/v1/repos/${giteaConfig.owner}/${repo}/releases?limit=5`);
|
|
const commits = await gitea.request(`/api/v1/repos/${giteaConfig.owner}/${repo}/commits?limit=5`);
|
|
|
|
res.json({
|
|
repo,
|
|
releases: releases.map(r => ({
|
|
tag: r.tag_name,
|
|
name: r.name,
|
|
created: r.created_at,
|
|
url: r.html_url
|
|
})),
|
|
commits: commits.map(c => ({
|
|
sha: c.sha?.substring(0, 7),
|
|
message: c.commit?.message?.split('\n')[0],
|
|
author: c.commit?.author?.name,
|
|
date: c.commit?.author?.date
|
|
}))
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
console.log('✅ Gitea integration loaded');
|
|
}
|
|
|
|
module.exports = { setupGiteaRoutes };
|
|
|
|
// Weekly swarm status report
|
|
app.get('/api/swarm/weekly-report', async (req, res) => {
|
|
try {
|
|
const repos = await gitea.getRepos();
|
|
const tasks = await fetch(`${process.env.TASKBOARD_URL || 'http://localhost:8395'}/api/tasks`).then(r => r.json());
|
|
const agents = await fetch(`${process.env.TASKBOARD_URL || 'http://localhost:8395'}/api/agents`).then(r => r.json());
|
|
|
|
// Get usage data
|
|
const usageRes = await fetch(`${process.env.TASKBOARD_URL || 'http://localhost:8395'}/api/usage/real`);
|
|
const usage = await usageRes.json();
|
|
|
|
// Generate report
|
|
const report = {
|
|
generated: new Date().toISOString(),
|
|
weekOf: new Date().toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }),
|
|
repositories: {
|
|
total: repos.length,
|
|
healthy: repos.filter(r => {
|
|
const days = Math.floor((Date.now() - new Date(r.updated_at).getTime()) / 86400000);
|
|
return days <= 7;
|
|
}).length,
|
|
stale: repos.filter(r => {
|
|
const days = Math.floor((Date.now() - new Date(r.updated_at).getTime()) / 86400000);
|
|
return days > 30;
|
|
}).length
|
|
},
|
|
tasks: {
|
|
total: tasks.length,
|
|
done: tasks.filter(t => t.status === 'Done').length,
|
|
inProgress: tasks.filter(t => t.status === 'In Progress').length,
|
|
backlog: tasks.filter(t => t.status === 'Backlog').length,
|
|
byPriority: {
|
|
Critical: tasks.filter(t => t.priority === 'Critical').length,
|
|
High: tasks.filter(t => t.priority === 'High').length,
|
|
Medium: tasks.filter(t => t.priority === 'Medium').length,
|
|
Low: tasks.filter(t => t.priority === 'Low').length
|
|
}
|
|
},
|
|
agents: {
|
|
total: agents.length,
|
|
byStatus: {
|
|
active: agents.filter(a => a.status === 'active').length,
|
|
idle: agents.filter(a => a.status === 'idle').length,
|
|
offline: agents.filter(a => a.status === 'offline').length
|
|
}
|
|
},
|
|
usage: {
|
|
totalTokens: usage.totals?.total || 0,
|
|
totalCost: usage.totals?.cost || 0,
|
|
topAgents: Object.entries(usage.agents || {})
|
|
.map(([k, v]) => ({ agent: k, tokens: v.total }))
|
|
.sort((a, b) => b.tokens - a.tokens)
|
|
.slice(0, 5)
|
|
},
|
|
summary: `Swarm weekly report for ${new Date().toLocaleDateString('en-US', { weekday: 'long' })}.\n\n📊 **Repositories:** ${repos.length} total (${repos.filter(r => r.updated_at && new Date(r.updated_at) > new Date(Date.now() - 7 * 86400000)).length} active this week)\n\n📋 **Tasks:** ${tasks.length} total (${tasks.filter(t => t.status === 'Done').length} done, ${tasks.filter(t => t.status === 'In Progress').length} in progress)\n\n🤖 **Agents:** ${agents.length} total\n\n💰 **Usage:** ${new Intl.NumberFormat('en-US').format(usage.totals?.total || 0)} tokens this week`
|
|
};
|
|
|
|
// Generate markdown
|
|
const markdown = generateWeeklyReportMarkdown(report);
|
|
|
|
// Save to wiki
|
|
const wikiPath = path.join(process.env.WIKI_DIR || '/app/wiki', `weekly-report-${new Date().toISOString().slice(0, 10)}.md`);
|
|
fs.writeFileSync(wikiPath, JSON.stringify({
|
|
title: `Weekly Report - ${new Date().toLocaleDateString()}`,
|
|
content: markdown,
|
|
tags: ['report', 'weekly', 'swarm', 'auto-generated'],
|
|
created: new Date().toISOString(),
|
|
modified: new Date().toISOString()
|
|
}, 'utf8');
|
|
|
|
res.json({
|
|
success: true,
|
|
report,
|
|
wikiEntry: {
|
|
title: `Weekly Report - ${new Date().toLocaleDateString()}`,
|
|
filename: path.basename(wikiPath)
|
|
}
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
function generateWeeklyReportMarkdown(report) {
|
|
return `# Weekly Swarm Status Report
|
|
|
|
**Generated:** ${new Date(report.generated).toLocaleString()}
|
|
|
|
## 📊 Repositories
|
|
|
|
| Metric | Count |
|
|
|--------|-------|
|
|
| Total | ${report.repositories.total} |
|
|
| Healthy (≤7d) | ${report.repositories.healthy} |
|
|
| Stale (>30d) | ${report.repositories.stale} |
|
|
|
|
## 📋 Tasks
|
|
|
|
| Priority | Count |
|
|
|----------|-------|
|
|
| Critical | ${tasks.byPriority.Critical} |
|
|
| High | ${tasks.byPriority.High} |
|
|
| Medium | ${tasks.byPriority.Medium} |
|
|
| Low | ${tasks.byPriority.Low} |
|
|
|
|
**Summary:**
|
|
- Total: ${report.tasks.total}
|
|
- Done: ${report.tasks.done}
|
|
- In Progress: ${report.tasks.inProgress}
|
|
- Backlog: ${report.tasks.backlog}
|
|
|
|
## 🤖 Agents
|
|
|
|
**Total Agents:** ${report.agents.total}
|
|
|
|
| Status | Count |
|
|
|--------|-------|
|
|
| Active | ${report.agents.byStatus.active} |
|
|
| Idle | ${report.agents.byStatus.idle} |
|
|
| Offline | ${report.agents.byStatus.offline} |
|
|
|
|
## 💰 Usage
|
|
|
|
**Total Tokens:** ${new Intl.NumberFormat('en-US').format(report.usage.totalTokens)}
|
|
**Total Cost:** $${report.usage.totalCost.toFixed(2)}
|
|
|
|
### Top 5 Agents by Usage
|
|
|
|
| Rank | Agent | Tokens |
|
|
|------|-------|--------|
|
|
${report.usage.topAgents.map((a, i) => `| ${i + 1} | \`${a.agent}\` | ${new Intl.NumberFormat('en-US').format(a.tokens)} |`).join('\n')}
|
|
|
|
---
|
|
|
|
*Report auto-generated by OpenClaw Swarm Orchestrator*
|
|
`;
|
|
}
|
|
|