← back to build-pages
api: /api/projects/:slug detail endpoint (commit count, top agents/skills, head SHA) + 4 smoke
5273463673a8f33da74fe70ff738966d2b115c4c · 2026-05-13 13:05:02 -0700 · SteveStudio2
Files touched
M server.jsM test/smoke.js
Diff
commit 5273463673a8f33da74fe70ff738966d2b115c4c
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 13:05:02 2026 -0700
api: /api/projects/:slug detail endpoint (commit count, top agents/skills, head SHA) + 4 smoke
---
server.js | 27 +++++++++++++++++++++++++++
test/smoke.js | 8 ++++++++
2 files changed, 35 insertions(+)
diff --git a/server.js b/server.js
index 10cb2fe..9b59f67 100644
--- a/server.js
+++ b/server.js
@@ -395,6 +395,33 @@ app.get('/api/projects', (_req, res) => {
});
});
+// Per-project stats: commit count, last commit, top agents/skills, file count.
+// Reads from the per-repo bundle cache so it shares git work with the HTML page.
+app.get('/api/projects/:slug', async (req, res, next) => {
+ const p = PROJECTS[req.params.slug];
+ if (!p) return res.status(404).json({ error: 'no-such-project' });
+ try {
+ const bundle = await getProjectBundle(req.params.slug, p);
+ const first = bundle.commits[bundle.commits.length - 1];
+ const last = bundle.commits[0];
+ res.setHeader('Cache-Control', 'public, max-age=30, stale-while-revalidate=60');
+ res.json({
+ slug: req.params.slug,
+ name: p.name,
+ blurb: p.blurb,
+ head: bundle.head.slice(0, 7),
+ commit_count: bundle.commits.length,
+ file_count: bundle.files.length,
+ first_commit: first ? { hash: first.shortHash, date: first.dateISO.slice(0, 10), subject: first.subject } : null,
+ last_commit: last ? { hash: last.shortHash, date: last.dateISO.slice(0, 10), subject: last.subject } : null,
+ agents_top: bundle.facets.agents.slice(0, 10),
+ skills_top: bundle.facets.skills.slice(0, 10),
+ ideas_count: bundle.facets.ideas.length,
+ as_of: new Date(bundle.at).toISOString(),
+ });
+ } catch (e) { next(e); }
+});
+
app.use((err, _req, res, _next) => {
console.error('[build-pages]', err.message);
res.status(500).send(shell('Error', `<h1>500</h1><pre>${esc(err.message)}</pre>`));
diff --git a/test/smoke.js b/test/smoke.js
index 3eb58bc..b436439 100644
--- a/test/smoke.js
+++ b/test/smoke.js
@@ -63,6 +63,14 @@ function check(name, cond, hint = '') {
check('api/projects: 200', r.status === 200);
check('api/projects: butlr in list', /"slug":"butlr"/.test(r.body));
+ // 4b. /api/projects/:slug detail
+ r = await fetch('/api/projects/butlr');
+ check('api/projects/butlr: 200', r.status === 200);
+ check('api/projects/butlr: commit_count', /"commit_count":\d+/.test(r.body));
+ check('api/projects/butlr: head sha', /"head":"[a-f0-9]{4,}/.test(r.body));
+ r = await fetch('/api/projects/nope-not-real');
+ check('api/projects/?: 404', r.status === 404);
+
// 5. index page
r = await fetch('/');
check('home: 200', r.status === 200);
← 91732aa smoke: warm /p/butlr render <250ms perf budget (cache regres
·
back to build-pages
·
home: pull counts from shared bundle cache + show fleet tota 822388f →