← back to build-pages
csv: /p/:slug/commits.csv spreadsheet export (RFC-4180 quoting) + 4 smoke
fedcbface5cc4a05f7459bf1f0e733ab2d83ba08 · 2026-05-13 14:44:53 -0700 · SteveStudio2
Files touched
M server.jsM test/smoke.js
Diff
commit fedcbface5cc4a05f7459bf1f0e733ab2d83ba08
Author: SteveStudio2 <stevestudio2@SteveStacStudio.lan>
Date: Wed May 13 14:44:53 2026 -0700
csv: /p/:slug/commits.csv spreadsheet export (RFC-4180 quoting) + 4 smoke
---
server.js | 20 ++++++++++++++++++++
test/smoke.js | 7 +++++++
2 files changed, 27 insertions(+)
diff --git a/server.js b/server.js
index e496ba4..c9d2065 100644
--- a/server.js
+++ b/server.js
@@ -648,6 +648,25 @@ app.get('/api/projects/:slug/commits', async (req, res, next) => {
} 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).
+app.get('/p/:slug/commits.csv', async (req, res, next) => {
+ const p = PROJECTS[req.params.slug];
+ if (!p) return res.status(404).type('text/plain').send('no-such-project\n');
+ try {
+ const { commits } = await getProjectBundle(req.params.slug, p);
+ const q = (s) => `"${String(s ?? '').replace(/"/g, '""')}"`;
+ const rows = ['hash,date,author,subject,body_len'];
+ for (const c of commits) {
+ rows.push([q(c.shortHash), q(c.dateISO), q(c.author), q(c.subject), c.body.length].join(','));
+ }
+ res.setHeader('Cache-Control', 'public, max-age=60, stale-while-revalidate=120');
+ res.setHeader('Content-Disposition', `attachment; filename="${req.params.slug}-commits.csv"`);
+ res.type('text/csv').send(rows.join('\n') + '\n');
+ } catch (e) { next(e); }
+});
+
// Atom feed per project — recent N commits in standards-compliant Atom 1.0.
// Lets external tools subscribe to a project's build journal (e.g. Feedly,
// NetNewsWire). Pulls from the same bundle cache as the HTML page.
@@ -725,6 +744,7 @@ 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/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 7a598b8..9ecd240 100644
--- a/test/smoke.js
+++ b/test/smoke.js
@@ -167,6 +167,13 @@ 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
+ 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'] || ''));
+
// 12. /api/projects/:slug/commits — paginated raw JSON
r = await fetch('/api/projects/butlr/commits?limit=3');
check('commits api: 200', r.status === 200);
← 0cb94e6 api: per-project facet endpoints (/agents, /skills, /ideas)
·
back to build-pages
·
csv: /fleet.csv — every commit across every project in one f f2d7363 →