[object Object]

← back to Lifestyle Asset Intel

yolo tick #6: /api/version introspection endpoint

0bd3a76e71f4cc284462aa38239eae49fe975bf1 · 2026-05-10 00:02:15 -0700 · Steve Abrams

Add the standard ops-hygiene introspection endpoint paying customers
expect: package_version, methodology_version, git SHA + dirty flag (so
ops can answer "is prod the same as my local?"), full runtime block
(node + platform + arch + pid + boot_time + uptime_seconds), schema
state (every applied migration with timestamp), and data snapshot
(asset / transaction / snapshot / index / holding / image counts +
latest snapshot as_of date).

Git SHA + dirty flag resolved at boot via execSync('git rev-parse'); if
git isn't installed (or the deploy is from a tarball not a checkout)
the field returns null without crashing the route.

Tightened /api/health to read PKG_VERSION from package.json (was
hardcoded). Added a /api/version smoke test asserting all the top-level
keys are present and that ≥3 migrations have applied.

30/30 tests green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit 0bd3a76e71f4cc284462aa38239eae49fe975bf1
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Sun May 10 00:02:15 2026 -0700

    yolo tick #6: /api/version introspection endpoint
    
    Add the standard ops-hygiene introspection endpoint paying customers
    expect: package_version, methodology_version, git SHA + dirty flag (so
    ops can answer "is prod the same as my local?"), full runtime block
    (node + platform + arch + pid + boot_time + uptime_seconds), schema
    state (every applied migration with timestamp), and data snapshot
    (asset / transaction / snapshot / index / holding / image counts +
    latest snapshot as_of date).
    
    Git SHA + dirty flag resolved at boot via execSync('git rev-parse'); if
    git isn't installed (or the deploy is from a tarball not a checkout)
    the field returns null without crashing the route.
    
    Tightened /api/health to read PKG_VERSION from package.json (was
    hardcoded). Added a /api/version smoke test asserting all the top-level
    keys are present and that ≥3 migrations have applied.
    
    30/30 tests green.
    
    Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 routes/api.js       | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++-
 tests/smoke.test.js | 13 ++++++++++
 2 files changed, 82 insertions(+), 1 deletion(-)

diff --git a/routes/api.js b/routes/api.js
index da4e9b7..5e30417 100644
--- a/routes/api.js
+++ b/routes/api.js
@@ -1,4 +1,7 @@
 const express = require('express');
+const fs = require('fs');
+const path = require('path');
+const { execSync } = require('child_process');
 const { pool } = require('../lib/db');
 const { valueAsset, listAssets, getIndex } = require('../lib/valuation');
 const {
@@ -11,6 +14,29 @@ const {
 
 const router = express.Router();
 
+// Resolve build metadata at boot (cheap; doesn't touch the DB).
+const PKG_VERSION = (() => {
+  try { return require('../package.json').version; } catch { return '0.0.0'; }
+})();
+const GIT_SHA = (() => {
+  try {
+    return execSync('git rev-parse --short=12 HEAD', {
+      cwd: path.join(__dirname, '..'),
+      stdio: ['ignore', 'pipe', 'ignore']
+    }).toString().trim();
+  } catch { return null; }
+})();
+const GIT_DIRTY = (() => {
+  try {
+    const out = execSync('git status --porcelain', {
+      cwd: path.join(__dirname, '..'),
+      stdio: ['ignore', 'pipe', 'ignore']
+    }).toString().trim();
+    return out.length > 0;
+  } catch { return null; }
+})();
+const BUILD_TIME = new Date().toISOString();
+
 router.get('/health', async (req, res) => {
   let dbOk = false;
   try {
@@ -19,7 +45,49 @@ router.get('/health', async (req, res) => {
   } catch (err) {
     console.error('[health] db check failed:', err.message);
   }
-  res.json({ ok: true, version: '0.0.1', db_ok: dbOk });
+  res.json({ ok: true, version: PKG_VERSION, db_ok: dbOk });
+});
+
+// /api/version — full build + data introspection. Used by deploy
+// scripts, monitoring dashboards, and "is the prod the same as my
+// local?" pre-flight checks.
+router.get('/version', async (req, res, next) => {
+  try {
+    const [migs, counts] = await Promise.all([
+      pool.query(`SELECT filename, applied_at FROM schema_migrations ORDER BY filename`),
+      pool.query(`SELECT
+        (SELECT COUNT(*) FROM canonical_assets)::int   AS assets,
+        (SELECT COUNT(*) FROM transactions)::int       AS transactions,
+        (SELECT COUNT(*) FROM valuation_snapshots)::int AS snapshots,
+        (SELECT COUNT(*) FROM indices)::int            AS indices,
+        (SELECT COUNT(*) FROM index_points)::int       AS index_points,
+        (SELECT COUNT(*) FROM portfolio_holdings)::int AS holdings,
+        (SELECT COUNT(*) FROM image_assets)::int       AS images,
+        (SELECT MAX(as_of) FROM valuation_snapshots)   AS latest_snapshot_as_of`)
+    ]);
+    res.json({
+      service: 'lifestyle-asset-intel',
+      package_version: PKG_VERSION,
+      methodology_version: 'v0-stub',
+      git: GIT_SHA ? { sha: GIT_SHA, dirty: GIT_DIRTY } : null,
+      runtime: {
+        node: process.version,
+        platform: process.platform,
+        arch: process.arch,
+        pid: process.pid,
+        boot_time: BUILD_TIME,
+        uptime_seconds: Math.round(process.uptime())
+      },
+      schema: {
+        migrations_applied: migs.rows.length,
+        migrations: migs.rows.map((r) => ({
+          filename: r.filename,
+          applied_at: r.applied_at
+        }))
+      },
+      data: counts.rows[0]
+    });
+  } catch (e) { next(e); }
 });
 
 router.get('/sources', async (req, res, next) => {
diff --git a/tests/smoke.test.js b/tests/smoke.test.js
index 1bb8d5e..178e9fd 100644
--- a/tests/smoke.test.js
+++ b/tests/smoke.test.js
@@ -46,6 +46,19 @@ test('boots and returns 200 on /api/health with db_ok true', async () => {
   assert.equal(r.json.version, '0.0.1');
 });
 
+test('GET /api/version returns build + schema + data introspection', async () => {
+  const r = await get('/api/version');
+  assert.equal(r.status, 200);
+  assert.equal(r.json.service, 'lifestyle-asset-intel');
+  assert.equal(r.json.methodology_version, 'v0-stub');
+  assert.ok(r.json.runtime && r.json.runtime.node, 'runtime.node present');
+  assert.ok(typeof r.json.runtime.uptime_seconds === 'number');
+  assert.ok(Array.isArray(r.json.schema.migrations));
+  assert.ok(r.json.schema.migrations_applied >= 3, 'at least 3 migrations');
+  assert.ok(r.json.data.assets >= 1);
+  assert.ok(r.json.data.snapshots >= 1);
+});
+
 test('GET /api/sources returns the seeded source registry', async () => {
   const r = await get('/api/sources');
   assert.equal(r.status, 200);

← 63e0563 yolo tick #5: METHODOLOGY.md + /methodology route  ·  back to Lifestyle Asset Intel  ·  yolo tick #7: immutable valuation_calls audit log + middlewa 44cae3a →