← 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
M routes/api.jsM tests/smoke.test.js
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 →