From bc53380d4af0d5acfb463adaf84e307423c58711 Mon Sep 17 00:00:00 2001 From: Christopher Mayor Date: Wed, 4 Mar 2026 18:16:52 -0800 Subject: [PATCH] feat: add weekly swarm status report generation (#11) --- gitea-routes.js | 139 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/gitea-routes.js b/gitea-routes.js index 3292169..bff5c85 100644 --- a/gitea-routes.js +++ b/gitea-routes.js @@ -229,3 +229,142 @@ function setupGiteaRoutes(app) { } 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* +`; +} +