← back to build-pages
api: /api/search?q= — cross-project commit search (min 2 chars, limit 1-200) + 4 smoke
eb2ab403954a2f165f42dc8faf399111817dfd4f · 2026-05-13 13:41:43 -0700 · SteveStudio2
Files touched
M server.jsM test/smoke.js
Diff
commit eb2ab403954a2f165f42dc8faf399111817dfd4f
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 13:41:43 2026 -0700
api: /api/search?q= — cross-project commit search (min 2 chars, limit 1-200) + 4 smoke
---
server.js | 29 +++++++++++++++++++++++++++++
test/smoke.js | 8 ++++++++
2 files changed, 37 insertions(+)
diff --git a/server.js b/server.js
index 2e39f87..d436ffa 100644
--- a/server.js
+++ b/server.js
@@ -434,6 +434,35 @@ app.get('/p/:slug/c/:hash', async (req, res, next) => {
} catch (e) { next(e); }
});
+// Cross-project search — case-insensitive substring match on commit
+// subject + body across every registered project. Used to power a "search
+// the whole fleet" experience on the home page. ?q= required (min 2 chars),
+// ?limit= clamps results (default 50, max 200).
+app.get('/api/search', async (req, res, next) => {
+ const q = (typeof req.query.q === 'string' ? req.query.q : '').trim().toLowerCase();
+ const limit = Math.min(Math.max(parseInt(req.query.limit, 10) || 50, 1), 200);
+ if (q.length < 2) return res.json({ q, results: [], note: 'min 2 chars' });
+ try {
+ const hits = [];
+ for (const [slug, p] of Object.entries(PROJECTS)) {
+ const bundle = await getProjectBundle(slug, p).catch(() => null);
+ if (!bundle) continue;
+ for (const c of bundle.commits) {
+ const blob = (c.subject + ' ' + c.body).toLowerCase();
+ if (blob.includes(q)) {
+ hits.push({
+ slug, project: p.name, hash: c.shortHash, date: c.dateISO.slice(0, 10), subject: c.subject,
+ });
+ if (hits.length >= limit) break;
+ }
+ }
+ if (hits.length >= limit) break;
+ }
+ res.setHeader('Cache-Control', 'public, max-age=30, stale-while-revalidate=60');
+ res.json({ q, total: hits.length, limit, results: hits });
+ } 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) => {
diff --git a/test/smoke.js b/test/smoke.js
index 59f7ceb..897bb83 100644
--- a/test/smoke.js
+++ b/test/smoke.js
@@ -146,6 +146,14 @@ function check(name, cond, hint = '') {
check('404: branded HTML', /build-pages/.test(r.body));
check('404: back-link', /back to all projects/.test(r.body));
+ // 14. cross-project search
+ r = await fetch('/api/search?q=whisper&limit=10');
+ check('search: 200', r.status === 200);
+ check('search: results array', /"results":\[/.test(r.body));
+ check('search: total field', /"total":\d+/.test(r.body));
+ r = await fetch('/api/search?q=a');
+ check('search: <2 chars → empty',/"results":\[\]/.test(r.body));
+
// Report
if (process.argv.includes('--json')) {
console.log(JSON.stringify({ base: BASE, pass, fail, total: pass + fail, checks, as_of: new Date().toISOString() }));
← 0158ff5 404: branded page on unknown routes (fleet parity) + 3 smoke
·
back to build-pages
·
ui: fleet-wide search input on home (wired to /api/search, 1 9494934 →