[object Object]

← 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

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 →