← back to build-pages
csv: /fleet.csv — every commit across every project in one file + 3 smoke
f2d736354ed832302618dbd746672e62e5c684d1 · 2026-05-13 14:48:54 -0700 · SteveStudio2
Files touched
M server.jsM test/smoke.js
Diff
commit f2d736354ed832302618dbd746672e62e5c684d1
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 14:48:54 2026 -0700
csv: /fleet.csv — every commit across every project in one file + 3 smoke
---
server.js | 25 ++++++++++++++++++++++++-
test/smoke.js | 8 +++++++-
2 files changed, 31 insertions(+), 2 deletions(-)
diff --git a/server.js b/server.js
index c9d2065..5d3ef07 100644
--- a/server.js
+++ b/server.js
@@ -648,6 +648,28 @@ app.get('/api/projects/:slug/commits', async (req, res, next) => {
} catch (e) { next(e); }
});
+// Fleet-wide CSV — every commit across every project in one file, sorted by
+// date desc. Adds a `project` column so the consumer can slice/filter. Use:
+// `curl https://projects.agentabrams.com/fleet.csv > fleet.csv && open fleet.csv`.
+app.get('/fleet.csv', async (_req, res, next) => {
+ try {
+ const q = (s) => `"${String(s ?? '').replace(/"/g, '""')}"`;
+ const all = [];
+ 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) {
+ all.push([q(slug), q(c.shortHash), q(c.dateISO), q(c.author), q(c.subject), c.body.length]);
+ }
+ }
+ all.sort((a, b) => (b[2] < a[2] ? -1 : b[2] > a[2] ? 1 : 0));
+ const rows = ['project,hash,date,author,subject,body_len'].concat(all.map(r => r.join(',')));
+ res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=120');
+ res.setHeader('Content-Disposition', 'attachment; filename="fleet-commits.csv"');
+ res.type('text/csv').send(rows.join('\n') + '\n');
+ } catch (e) { next(e); }
+});
+
// CSV export — every commit in spreadsheet form. Useful for the "drop this
// into Numbers and slice by date / author" workflow. Header row + RFC-4180
// quoting (double-quote inside double-quoted field).
@@ -744,7 +766,8 @@ app.get('/api', (_req, res) => {
'GET /p/:slug': 'Project journal HTML page (?q= deep-link supported)',
'GET /p/:slug/c/:hash': 'Commit detail page — full diff + body',
'GET /p/:slug/feed.atom': 'Atom 1.0 feed — up to 50 recent commits, autodiscovered on project page',
- 'GET /p/:slug/commits.csv': 'Spreadsheet export — every commit as CSV (hash, date, author, subject, body_len)',
+ 'GET /p/:slug/commits.csv': 'Per-project commits as CSV (hash, date, author, subject, body_len)',
+ 'GET /fleet.csv': 'Fleet-wide commits CSV — every commit across every project, date-sorted desc',
'GET /p/:slug/file?path=<path>': 'View any tracked file at HEAD',
'GET /sitemap.xml': 'XML sitemap — home + every /p/:slug',
'GET /robots.txt': 'Crawler policy (allow all, sitemap advertised)',
diff --git a/test/smoke.js b/test/smoke.js
index 9ecd240..5596085 100644
--- a/test/smoke.js
+++ b/test/smoke.js
@@ -167,13 +167,19 @@ function check(name, cond, hint = '') {
r = await fetch('/p/butlr');
check('butlr: feed autodiscover',/rel="alternate" type="application\/atom\+xml"/.test(r.body));
- // 11b. CSV export
+ // 11b. CSV export (per project)
r = await fetch('/p/butlr/commits.csv');
check('csv: 200', r.status === 200);
check('csv: text/csv', /text\/csv/.test(r.headers['content-type'] || ''));
check('csv: header row', /^hash,date,author,subject,body_len/.test(r.body));
check('csv: attachment', /attachment;.*butlr-commits/.test(r.headers['content-disposition'] || ''));
+ // 11c. CSV export (fleet-wide)
+ r = await fetch('/fleet.csv');
+ check('fleet csv: 200', r.status === 200);
+ check('fleet csv: project col', /^project,hash,date,author,subject,body_len/.test(r.body));
+ check('fleet csv: butlr in mix', /"butlr",/.test(r.body));
+
// 12. /api/projects/:slug/commits — paginated raw JSON
r = await fetch('/api/projects/butlr/commits?limit=3');
check('commits api: 200', r.status === 200);
← fedcbfa csv: /p/:slug/commits.csv spreadsheet export (RFC-4180 quoti
·
back to build-pages
·
ui: visible export affordances — fleet.csv in footer + commi 29d618d →