← 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
M server.jsM test/smoke.js
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 →