[object Object]

← back to Lifestyle Asset Intel

snapshot — gitify backup 2026-05-19

3c385b48aef9244af749c830787e2654c3984b21 · 2026-05-19 11:35:47 -0700 · Steve

Files touched

Diff

commit 3c385b48aef9244af749c830787e2654c3984b21
Author: Steve <steve@designerwallcoverings.com>
Date:   Tue May 19 11:35:47 2026 -0700

    snapshot — gitify backup 2026-05-19
---
 lib/audit.js        | 11 ++++++++++-
 lib/openapi.js      |  5 +++--
 routes/api.js       | 10 +++++++++-
 tests/audit.test.js | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 75 insertions(+), 4 deletions(-)

diff --git a/lib/audit.js b/lib/audit.js
index af42300..634fe40 100644
--- a/lib/audit.js
+++ b/lib/audit.js
@@ -91,7 +91,7 @@ function auditMiddleware(req, res, next) {
 
 async function recentCalls({
   limit = 100, asset_slug = null, route = null, method = null,
-  status = null, since = null
+  status = null, since = null, until = null
 } = {}) {
   const lim = Math.max(1, Math.min(1000, parseInt(limit, 10) || 100));
   const where = [];
@@ -133,6 +133,15 @@ async function recentCalls({
       where.push(`called_at >= $${params.length}`);
     }
   }
+  // until: ISO-8601 timestamp upper bound (called_at <= until). Mirrors
+  // since; composes with it to bracket a window. Unparseable input ignored.
+  if (until != null && until !== '') {
+    const d = new Date(String(until).trim());
+    if (!Number.isNaN(d.getTime())) {
+      params.push(d.toISOString());
+      where.push(`called_at <= $${params.length}`);
+    }
+  }
   params.push(lim);
   const whereSql = where.length ? `WHERE ${where.join(' AND ')}` : '';
   const r = await pool.query(
diff --git a/lib/openapi.js b/lib/openapi.js
index e4fc47c..6266d5e 100644
--- a/lib/openapi.js
+++ b/lib/openapi.js
@@ -557,7 +557,7 @@ function buildSpec({ version, gitSha }) {
         description:
           'Immutable audit log per METHODOLOGY § 14. Returns most recent N (default 100) ' +
           'calls in descending called_at order. Optional filters compose with AND: ' +
-          'asset_slug (exact match), route (case-insensitive substring), method (exact, case-insensitive), status (exact 3-digit code OR 1-digit class prefix "2"/"4"/"5" for status-class match), since (ISO-8601 lower bound on called_at).',
+          'asset_slug (exact match), route (case-insensitive substring), method (exact, case-insensitive), status (exact 3-digit code OR 1-digit class prefix "2"/"4"/"5" for status-class match), since (ISO-8601 lower bound on called_at), until (ISO-8601 upper bound on called_at).',
         tags: ['ops'],
         parameters: [
           { name: 'limit', in: 'query', schema: { type: 'integer', minimum: 1, maximum: 1000, default: 100 } },
@@ -565,7 +565,8 @@ function buildSpec({ version, gitSha }) {
           { name: 'route', in: 'query', description: 'Case-insensitive substring match against route template', schema: { type: 'string' } },
           { name: 'method', in: 'query', description: 'Exact HTTP method, case-insensitive', schema: { type: 'string', example: 'GET' } },
           { name: 'status', in: 'query', description: 'Exact 3-digit response status (200, 404, …) OR a 1-digit class prefix (2 → 2xx). Invalid input is silently ignored.', schema: { type: 'string', example: '404' } },
-          { name: 'since', in: 'query', description: 'ISO-8601 timestamp; only rows with called_at >= since are returned. Unparseable input is silently ignored.', schema: { type: 'string', format: 'date-time' } }
+          { name: 'since', in: 'query', description: 'ISO-8601 timestamp; only rows with called_at >= since are returned. Unparseable input is silently ignored.', schema: { type: 'string', format: 'date-time' } },
+          { name: 'until', in: 'query', description: 'ISO-8601 timestamp; only rows with called_at <= until are returned. Composes with since to bracket a window. Unparseable input is silently ignored.', schema: { type: 'string', format: 'date-time' } }
         ],
         responses: {
           200: { description: 'OK', content: { 'application/json': { schema: {
diff --git a/routes/api.js b/routes/api.js
index e03d99c..d316ea3 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -257,13 +257,20 @@ router.get('/audit/recent', setCache(NO_STORE), async (req, res, next) => {
       const d = new Date(sinceRaw);
       if (!Number.isNaN(d.getTime())) sinceEcho = d.toISOString();
     }
+    const untilRaw = typeof req.query.until === 'string' ? req.query.until.trim() : '';
+    let untilEcho = null;
+    if (untilRaw) {
+      const d = new Date(untilRaw);
+      if (!Number.isNaN(d.getTime())) untilEcho = d.toISOString();
+    }
     const rows = await recentCalls({
       limit,
       asset_slug,
       route: route || null,
       method: method || null,
       status: statusRaw || null,
-      since: sinceRaw || null
+      since: sinceRaw || null,
+      until: untilRaw || null
     });
     res.json({
       count: rows.length,
@@ -272,6 +279,7 @@ router.get('/audit/recent', setCache(NO_STORE), async (req, res, next) => {
       method: method ? method.toUpperCase() : null,
       status: statusEcho,
       since: sinceEcho,
+      until: untilEcho,
       calls: rows
     });
   } catch (e) { next(e); }
diff --git a/tests/audit.test.js b/tests/audit.test.js
index 8ce8e54..3257cfe 100644
--- a/tests/audit.test.js
+++ b/tests/audit.test.js
@@ -294,6 +294,59 @@ test('GET /api/audit/recent?since filters by ISO timestamp lower bound and compo
   assert.equal(rBlank.json.since, null);
 });
 
+test('GET /api/audit/recent?until filters by ISO timestamp upper bound and composes', async () => {
+  // Generate at least one call BEFORE the cutoff.
+  await getJson('/api/valuation/birkin-30-togo-gold-ghw-us');
+  await new Promise((res) => setTimeout(res, 250));
+
+  // Capture a cutoff AFTER those calls landed.
+  const cutoff = new Date().toISOString();
+  await new Promise((res) => setTimeout(res, 50));
+
+  // Generate at least one call AFTER the cutoff (must be excluded).
+  await getJson('/api/valuation/birkin-30-togo-gold-ghw-us');
+  await new Promise((res) => setTimeout(res, 250));
+
+  const r = await getJson(`/api/audit/recent?until=${encodeURIComponent(cutoff)}&limit=200`);
+  assert.equal(r.status, 200);
+  assert.equal(r.json.until, cutoff);
+  assert.ok(r.json.calls.length >= 1, 'at least one row before cutoff');
+  for (const c of r.json.calls) {
+    assert.ok(new Date(c.called_at).getTime() <= new Date(cutoff).getTime(),
+      `expected called_at <= cutoff, got ${c.called_at}`);
+  }
+
+  // Composes with since to bracket a window.
+  const windowLo = new Date(Date.now() - 1000 * 60 * 60).toISOString(); // 1h ago
+  const r2 = await getJson(
+    `/api/audit/recent?since=${encodeURIComponent(windowLo)}&until=${encodeURIComponent(cutoff)}&limit=50`
+  );
+  assert.equal(r2.status, 200);
+  assert.equal(r2.json.since, windowLo);
+  assert.equal(r2.json.until, cutoff);
+  for (const c of r2.json.calls) {
+    const t = new Date(c.called_at).getTime();
+    assert.ok(t >= new Date(windowLo).getTime() && t <= new Date(cutoff).getTime(),
+      `expected called_at within window, got ${c.called_at}`);
+  }
+
+  // Far-past cutoff returns zero rows.
+  const ancient = new Date(Date.now() - 1000 * 60 * 60 * 24 * 365).toISOString();
+  const rEmpty = await getJson(`/api/audit/recent?until=${encodeURIComponent(ancient)}&limit=10`);
+  assert.equal(rEmpty.status, 200);
+  assert.equal(rEmpty.json.calls.length, 0);
+
+  // Unparseable until is silently ignored → echoes null and behaves like no filter.
+  const rJunk = await getJson('/api/audit/recent?until=not-a-date&limit=5');
+  assert.equal(rJunk.status, 200);
+  assert.equal(rJunk.json.until, null);
+
+  // Empty until is silently ignored.
+  const rBlank = await getJson('/api/audit/recent?until=&limit=5');
+  assert.equal(rBlank.status, 200);
+  assert.equal(rBlank.json.until, null);
+});
+
 test('valuation_calls has no UPDATE or DELETE called by app code', async () => {
   // Sanity: app should never mutate the table. A grep would verify
   // this at code-review time, but the fastest check at runtime is that

← 9b2ec0e yolo tick #34: /api/audit/recent ?since= ISO-8601 lower boun  ·  back to Lifestyle Asset Intel  ·  gitignore snapshot files + 404-guard .bak/.pre-/.orig paths f402188 →