[object Object]

← back to build-pages

api: per-project facet endpoints (/agents, /skills, /ideas) + 6 smoke

0cb94e663b18f8da4f7b062f165f66cd6050d828 · 2026-05-13 14:40:48 -0700 · SteveStudio2

Files touched

Diff

commit 0cb94e663b18f8da4f7b062f165f66cd6050d828
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date:   Wed May 13 14:40:48 2026 -0700

    api: per-project facet endpoints (/agents, /skills, /ideas) + 6 smoke
---
 server.js     | 18 ++++++++++++++++++
 test/smoke.js |  7 +++++++
 2 files changed, 25 insertions(+)

diff --git a/server.js b/server.js
index aa0814d..e496ba4 100644
--- a/server.js
+++ b/server.js
@@ -612,6 +612,21 @@ app.get('/api/search', async (req, res, next) => {
   } catch (e) { next(e); }
 });
 
+// Per-project facet endpoints — agents / skills / ideas as machine-readable
+// JSON, for external dashboards that want one facet without parsing the whole
+// project detail blob. Same bundle cache, sub-1ms when warm.
+for (const facet of ['agents', 'skills', 'ideas']) {
+  app.get(`/api/projects/:slug/${facet}`, 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);
+      res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=120');
+      res.json({ slug: req.params.slug, [facet]: bundle.facets[facet] });
+    } catch (e) { next(e); }
+  });
+}
+
 // Raw commits JSON — paginated. Supports ?limit=N (default 100, max 500) and
 // ?offset=N. The bundle has the full list cached; this just slices it.
 app.get('/api/projects/:slug/commits', async (req, res, next) => {
@@ -702,6 +717,9 @@ app.get('/api', (_req, res) => {
       'GET /api/projects':                     'List every project journal (slug, name, repo, blurb)',
       'GET /api/projects/:slug':               'Per-project stats — head SHA, commit_count, top agents/skills, first/last commit',
       'GET /api/projects/:slug/commits':       'Paginated raw commits — ?limit=N&offset=N (1-500 default 100)',
+      'GET /api/projects/:slug/agents':        'Raw agents facet — sub-agent names auto-extracted from commit bodies',
+      'GET /api/projects/:slug/skills':        'Raw skills facet — /skill-name slugs auto-extracted from commit bodies',
+      'GET /api/projects/:slug/ideas':         'Raw ideas — commit bodies ≥120 chars (design-rationale + scaffolds)',
       'GET /api/search':                       'Cross-project commit search — ?q=<term>&limit=N, round-robin + by_project breakdown',
       'GET /api/recent':                       'Most recent N commits across the whole fleet — date-sorted, ?limit=20',
       'GET /p/:slug':                          'Project journal HTML page (?q= deep-link supported)',
diff --git a/test/smoke.js b/test/smoke.js
index a2ed74b..7a598b8 100644
--- a/test/smoke.js
+++ b/test/smoke.js
@@ -186,6 +186,13 @@ function check(name, cond, hint = '') {
   check('recent: results array',   /"results":\[/.test(r.body));
   check('recent: hash field',      /"hash":"[a-f0-9]{4,}/.test(r.body));
 
+  // 13c. per-project facet endpoints (agents / skills / ideas)
+  for (const facet of ['agents', 'skills', 'ideas']) {
+    r = await fetch(`/api/projects/butlr/${facet}`);
+    check(`facet ${facet}: 200`,    r.status === 200);
+    check(`facet ${facet}: field`,  new RegExp(`"${facet}":`).test(r.body));
+  }
+
   // 14. cross-project search (round-robin merge + per-project breakdown)
   r = await fetch('/api/search?q=whisper&limit=10');
   check('search: 200',             r.status === 200);

← afcaaa3 home: 'Recent across the fleet' rail — server-rendered top 1  ·  back to build-pages  ·  csv: /p/:slug/commits.csv spreadsheet export (RFC-4180 quoti fedcbfa →