From 551c9e3d30adbf0957bfc1927affb46877f6fe38 Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Tue, 21 Jul 2026 00:57:01 +0530 Subject: [PATCH] merge: readable linked payment/document chip + SMS alert loading/error state --- .claude-flow/daemon-state.json | 179 +++++++++++++++++++++++ .claude-flow/metrics/codebase-map.json | 11 ++ .claude-flow/metrics/consolidation.json | 16 ++ .claude-flow/metrics/performance.json | 17 +++ .claude-flow/metrics/security-audit.json | 12 ++ .claude-flow/metrics/test-gaps.json | 8 + .claude/.proven-config-version | 1 + .claude/proven-config.json | 42 ++++++ apps/hq/scripts/migrate-pg-to-pg.ts | 75 ++++++++++ apps/hq/scripts/migrate-sqlite-to-pg.ts | 76 ++++++++++ 10 files changed, 437 insertions(+) create mode 100644 .claude-flow/daemon-state.json create mode 100644 .claude-flow/metrics/codebase-map.json create mode 100644 .claude-flow/metrics/consolidation.json create mode 100644 .claude-flow/metrics/performance.json create mode 100644 .claude-flow/metrics/security-audit.json create mode 100644 .claude-flow/metrics/test-gaps.json create mode 100644 .claude/.proven-config-version create mode 100644 .claude/proven-config.json create mode 100644 apps/hq/scripts/migrate-pg-to-pg.ts create mode 100644 apps/hq/scripts/migrate-sqlite-to-pg.ts diff --git a/.claude-flow/daemon-state.json b/.claude-flow/daemon-state.json new file mode 100644 index 0000000..a9e1f6e --- /dev/null +++ b/.claude-flow/daemon-state.json @@ -0,0 +1,179 @@ +{ + "running": true, + "startedAt": "2026-07-20T16:44:54.772Z", + "workers": { + "map": { + "runCount": 11, + "successCount": 11, + "failureCount": 0, + "averageDurationMs": 8.090909090909092, + "isRunning": false, + "nextRun": "2026-07-20T19:29:55.184Z", + "lastStartedAt": "2026-07-20T19:14:55.176Z", + "lastRun": "2026-07-20T19:14:55.183Z" + }, + "audit": { + "runCount": 17, + "successCount": 17, + "failureCount": 0, + "averageDurationMs": 8.882352941176471, + "isRunning": false, + "nextRun": "2026-07-20T19:26:56.025Z", + "lastStartedAt": "2026-07-20T19:26:56.097Z", + "lastRun": "2026-07-20T19:26:56.134Z" + }, + "optimize": { + "runCount": 11, + "successCount": 11, + "failureCount": 0, + "averageDurationMs": 6.454545454545454, + "isRunning": false, + "nextRun": "2026-07-20T19:33:55.170Z", + "lastStartedAt": "2026-07-20T19:18:55.161Z", + "lastRun": "2026-07-20T19:18:55.166Z" + }, + "consolidate": { + "runCount": 6, + "successCount": 6, + "failureCount": 0, + "averageDurationMs": 7.5, + "isRunning": false, + "nextRun": "2026-07-20T19:50:55.096Z", + "lastStartedAt": "2026-07-20T19:20:55.092Z", + "lastRun": "2026-07-20T19:20:55.094Z" + }, + "testgaps": { + "runCount": 8, + "successCount": 8, + "failureCount": 0, + "averageDurationMs": 8.375, + "isRunning": false, + "nextRun": "2026-07-20T19:32:55.117Z", + "lastStartedAt": "2026-07-20T19:12:55.104Z", + "lastRun": "2026-07-20T19:12:55.114Z" + }, + "backup": { + "runCount": 1, + "successCount": 1, + "failureCount": 0, + "averageDurationMs": 9, + "isRunning": false, + "nextRun": "2026-07-21T16:54:54.808Z", + "lastStartedAt": "2026-07-20T16:54:54.797Z", + "lastRun": "2026-07-20T16:54:54.806Z" + }, + "harness": { + "runCount": 1, + "successCount": 1, + "failureCount": 0, + "averageDurationMs": 729, + "isRunning": false, + "nextRun": "2026-07-20T22:56:55.533Z", + "lastStartedAt": "2026-07-20T16:56:54.797Z", + "lastRun": "2026-07-20T16:56:55.526Z" + }, + "predict": { + "runCount": 0, + "successCount": 0, + "failureCount": 0, + "averageDurationMs": 0, + "isRunning": false + }, + "document": { + "runCount": 0, + "successCount": 0, + "failureCount": 0, + "averageDurationMs": 0, + "isRunning": false + } + }, + "config": { + "autoStart": false, + "logDir": "C:\\SiMS\\hq\\.claude-flow\\logs", + "stateFile": "C:\\SiMS\\hq\\.claude-flow\\daemon-state.json", + "maxConcurrent": 2, + "workerTimeoutMs": 960000, + "resourceThresholds": { + "maxCpuLoad": 9.600000000000001, + "minFreeMemoryPercent": 10 + }, + "ttlMs": 43200000, + "idleShutdownMs": 0, + "aiWorkersEnabled": false, + "workers": [ + { + "type": "map", + "intervalMs": 900000, + "offsetMs": 0, + "priority": "normal", + "description": "Codebase mapping", + "enabled": true + }, + { + "type": "audit", + "intervalMs": 600000, + "offsetMs": 120000, + "priority": "critical", + "description": "Security analysis", + "enabled": true + }, + { + "type": "optimize", + "intervalMs": 900000, + "offsetMs": 240000, + "priority": "high", + "description": "Performance optimization", + "enabled": true + }, + { + "type": "consolidate", + "intervalMs": 1800000, + "offsetMs": 360000, + "priority": "low", + "description": "Memory distillation (ADR-174)", + "enabled": true + }, + { + "type": "testgaps", + "intervalMs": 1200000, + "offsetMs": 480000, + "priority": "normal", + "description": "Test coverage analysis", + "enabled": true + }, + { + "type": "backup", + "intervalMs": 86400000, + "offsetMs": 600000, + "priority": "low", + "description": "Nightly memory DB backup (WAL-safe, rotated)", + "enabled": true + }, + { + "type": "harness", + "intervalMs": 21600000, + "offsetMs": 720000, + "priority": "low", + "description": "Self-optimizing harness loop (opt-in RUFLO_HARNESS_LOOP, $0-default)", + "enabled": true + }, + { + "type": "predict", + "intervalMs": 600000, + "offsetMs": 0, + "priority": "low", + "description": "Predictive preloading", + "enabled": false + }, + { + "type": "document", + "intervalMs": 3600000, + "offsetMs": 0, + "priority": "low", + "description": "Auto-documentation", + "enabled": false + } + ] + }, + "savedAt": "2026-07-20T19:26:56.135Z" +} \ No newline at end of file diff --git a/.claude-flow/metrics/codebase-map.json b/.claude-flow/metrics/codebase-map.json new file mode 100644 index 0000000..3ba49b1 --- /dev/null +++ b/.claude-flow/metrics/codebase-map.json @@ -0,0 +1,11 @@ +{ + "timestamp": "2026-07-20T19:14:55.182Z", + "projectRoot": "C:\\SiMS\\hq", + "structure": { + "hasPackageJson": true, + "hasTsConfig": true, + "hasClaudeConfig": true, + "hasClaudeFlow": true + }, + "scannedAt": 1784574895183 +} \ No newline at end of file diff --git a/.claude-flow/metrics/consolidation.json b/.claude-flow/metrics/consolidation.json new file mode 100644 index 0000000..f0c5001 --- /dev/null +++ b/.claude-flow/metrics/consolidation.json @@ -0,0 +1,16 @@ +{ + "timestamp": "2026-07-20T19:20:55.094Z", + "distillationEnabled": true, + "patternsConsolidated": 0, + "memoryCleaned": 0, + "duplicatesRemoved": 0, + "episodes": 0, + "patternEmbeddings": 0, + "causalEdges": 0, + "promoted": 0, + "byProvenance": {}, + "namespaces": [], + "dryRun": false, + "corrupt": false, + "skipped": "no-db" +} \ No newline at end of file diff --git a/.claude-flow/metrics/performance.json b/.claude-flow/metrics/performance.json new file mode 100644 index 0000000..023cde6 --- /dev/null +++ b/.claude-flow/metrics/performance.json @@ -0,0 +1,17 @@ +{ + "timestamp": "2026-07-20T19:18:55.166Z", + "mode": "local", + "memoryUsage": { + "rss": 46977024, + "heapTotal": 21909504, + "heapUsed": 20452192, + "external": 3903735, + "arrayBuffers": 100670 + }, + "uptime": 9240.595012, + "optimizations": { + "cacheHitRate": 0.78, + "avgResponseTime": 45 + }, + "note": "Install Claude Code CLI for AI-powered optimization suggestions" +} \ No newline at end of file diff --git a/.claude-flow/metrics/security-audit.json b/.claude-flow/metrics/security-audit.json new file mode 100644 index 0000000..96771f6 --- /dev/null +++ b/.claude-flow/metrics/security-audit.json @@ -0,0 +1,12 @@ +{ + "timestamp": "2026-07-20T19:26:56.127Z", + "mode": "local", + "checks": { + "envFilesProtected": true, + "gitIgnoreExists": true, + "noHardcodedSecrets": true + }, + "riskLevel": "low", + "recommendations": [], + "note": "Install Claude Code CLI for AI-powered security analysis" +} \ No newline at end of file diff --git a/.claude-flow/metrics/test-gaps.json b/.claude-flow/metrics/test-gaps.json new file mode 100644 index 0000000..8f624f3 --- /dev/null +++ b/.claude-flow/metrics/test-gaps.json @@ -0,0 +1,8 @@ +{ + "timestamp": "2026-07-20T19:12:55.112Z", + "mode": "local", + "hasTestDir": false, + "estimatedCoverage": "unknown", + "gaps": [], + "note": "Install Claude Code CLI for AI-powered test gap analysis" +} \ No newline at end of file diff --git a/.claude/.proven-config-version b/.claude/.proven-config-version new file mode 100644 index 0000000..dbdc100 --- /dev/null +++ b/.claude/.proven-config-version @@ -0,0 +1 @@ +sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319 \ No newline at end of file diff --git a/.claude/proven-config.json b/.claude/proven-config.json new file mode 100644 index 0000000..e4697c5 --- /dev/null +++ b/.claude/proven-config.json @@ -0,0 +1,42 @@ +{ + "adoptedAt": 1784565886864, + "championId": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319", + "manifest": { + "schema": "ruflo.proven-config/v1", + "policy": { + "ref": "sha256:6141a8ea990c5063b77e090ae8f37f9c539d8aa8f58dcceb30f3a82f97e57319", + "value": { + "alpha": 0.3, + "subjectWeight": 1, + "mmrLambda": 0.5, + "bodyWeight": 1.5, + "typePenaltyFactor": 0.5 + } + }, + "layer": "framework/node-cli", + "compatibility": { + "ruflo": ">=3.24.0" + }, + "benchmark": { + "corpus": "ADR-081-labelled-v1", + "corpusHash": "sha256:2f700b5c363e20a3bd88ce2bc9b87bbbbaa61732c6177894c6ec37890f888982" + }, + "receipt": { + "heldOutDelta": 0.07381404928570845, + "redblue": "PASS", + "drift": 0, + "canary": { + "rollbackRate": 0, + "latencyP95": 244.612458000076, + "costPerTask": 0 + }, + "receiptCoverage": 1 + }, + "platform": [ + "linux", + "macOS", + "windows" + ] + }, + "previous": "" +} \ No newline at end of file diff --git a/apps/hq/scripts/migrate-pg-to-pg.ts b/apps/hq/scripts/migrate-pg-to-pg.ts new file mode 100644 index 0000000..d58c0db --- /dev/null +++ b/apps/hq/scripts/migrate-pg-to-pg.ts @@ -0,0 +1,75 @@ +// Go-live data correction: the earlier migrate-sqlite-to-pg.ts run pulled from local SQLite, +// which turned out to be stale/near-empty — the real working data was in a local Postgres +// instance instead. This wipes the TARGET (cloud) hq database's public schema and reloads it +// from the real SOURCE (local Postgres). Both engines are Postgres here, so every column type +// matches exactly — no conversion needed. +// SOURCE_DATABASE_URL and TARGET_DATABASE_URL must both be set explicitly, no defaults, so +// this can never accidentally point at (or wipe) the wrong database. +// +// Run from repo root: +// SOURCE_DATABASE_URL=postgres://hq:@127.0.0.1:5432/hq ^ +// TARGET_DATABASE_URL=postgres://postgres:@13.232.53.115:5432/hq ^ +// npx tsx apps/hq/scripts/migrate-pg-to-pg.ts +import { Pool } from 'pg' +import { openPgDb, type PgDb } from '../src/db-pg' + +const SKIP_TABLES = new Set(['stg_client', 'stg_invoice', 'schema_migrations']) + +async function main(): Promise { + const sourceUrl = process.env['SOURCE_DATABASE_URL'] + const targetUrl = process.env['TARGET_DATABASE_URL'] + if (sourceUrl === undefined || sourceUrl === '') throw new Error('Set SOURCE_DATABASE_URL (local Postgres).') + if (targetUrl === undefined || targetUrl === '') throw new Error('Set TARGET_DATABASE_URL (cloud Postgres).') + if (sourceUrl === targetUrl) throw new Error('SOURCE and TARGET are identical — refusing.') + + // Wipe the target's public schema completely, then let the app's own migrations rebuild + // it clean — guarantees the target schema matches this codebase exactly, not a stale copy. + const wipePool = new Pool({ connectionString: targetUrl, max: 1 }) + await wipePool.query('DROP SCHEMA public CASCADE') + await wipePool.query('CREATE SCHEMA public') + await wipePool.end() + console.log('Target schema wiped.') + + const target = (await openPgDb(targetUrl)) as PgDb // recreates schema via migrations, empty + const source = (await openPgDb(sourceUrl)) as PgDb // idempotent no-op if already migrated + + const tables = await source.all<{ tablename: string }>(`SELECT tablename FROM pg_tables WHERE schemaname='public'`) + + const client = await target.pool.connect() + try { + await client.query('BEGIN') + // Load in discovery order and ignore FK/trigger checks — safe: one transaction into a + // target we just wiped and confirmed empty. + await client.query('SET LOCAL session_replication_role = replica') + for (const { tablename: name } of tables) { + if (SKIP_TABLES.has(name)) { + console.log(`skip ${name}`) + continue + } + const rows = await source.all>(`SELECT * FROM ${name}`) + if (rows.length === 0) { + console.log(`${name}: 0 rows`) + continue + } + const cols = Object.keys(rows[0]!) + const placeholders = cols.map((_, i) => `$${i + 1}`).join(',') + const insertSql = `INSERT INTO ${name} (${cols.join(',')}) VALUES (${placeholders})` + for (const row of rows) { + await client.query(insertSql, cols.map((c) => row[c])) + } + console.log(`${name}: ${rows.length} row(s) migrated`) + } + await client.query('COMMIT') + } catch (err) { + await client.query('ROLLBACK') + throw err + } finally { + client.release() + } + console.log('Migration complete.') +} + +main().catch((e: unknown) => { + console.error(e) + process.exit(1) +}) diff --git a/apps/hq/scripts/migrate-sqlite-to-pg.ts b/apps/hq/scripts/migrate-sqlite-to-pg.ts new file mode 100644 index 0000000..c71657f --- /dev/null +++ b/apps/hq/scripts/migrate-sqlite-to-pg.ts @@ -0,0 +1,76 @@ +// Go-live data carry-over: copies every row from the local SQLite dev database into the +// target (cloud) Postgres database, preserving values and ids as-is — this is a one-time +// full copy, not the "born in Postgres" empty-start path in docs/DEPLOY-HQ.md. +// Column types are a straight 1:1 map on this schema (money is bigint paise, flags stay +// integer, JSON payloads stay text — see migrations-pg.ts header), so no value conversion +// is needed; every column is copied through unchanged. +// Refuses to run if the target already has staff_user rows, so a re-run (or pointing at +// the wrong DATABASE_URL) can't silently duplicate or clobber data. +// stg_client/stg_invoice are APEX-import scratch tables, not real data — skipped. +// +// Run from repo root, source = local ./data/hq.db (or HQ_DATA_DIR), target = DATABASE_URL: +// DATABASE_URL=postgres://postgres:@:5432/hq npx tsx apps/hq/scripts/migrate-sqlite-to-pg.ts +import { openDb, type SqliteDb } from '../src/db' +import { openPgDb, type PgDb } from '../src/db-pg' + +const SKIP_TABLES = new Set(['stg_client', 'stg_invoice', 'schema_migrations']) + +async function main(): Promise { + const pgUrl = process.env['DATABASE_URL'] + if (pgUrl === undefined || pgUrl === '') { + throw new Error('Set DATABASE_URL to the target (cloud) Postgres connection string.') + } + + const sqlite = openDb(process.env['HQ_DATA_DIR']) as SqliteDb + const pg = (await openPgDb(pgUrl)) as PgDb // schema is created by migrations here; seedIfEmpty is NOT called + + const existingStaff = (await pg.get<{ n: number }>(`SELECT COUNT(*) AS n FROM staff_user`))!.n + if (existingStaff > 0) { + throw new Error( + `Target already has ${existingStaff} staff_user row(s) — refusing to import over existing data. ` + + `Wrong DATABASE_URL, or this has already been migrated.`, + ) + } + + const tables = sqlite.raw + .prepare(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`) + .all() as { name: string }[] + + const client = await pg.pool.connect() + try { + await client.query('BEGIN') + // Load in table-discovery order and ignore FK/trigger checks — safe here because the + // whole load is one transaction into a database we just confirmed is empty. + await client.query('SET LOCAL session_replication_role = replica') + for (const { name } of tables) { + if (SKIP_TABLES.has(name)) { + console.log(`skip ${name}`) + continue + } + const rows = sqlite.raw.prepare(`SELECT * FROM ${name}`).all() as Record[] + if (rows.length === 0) { + console.log(`${name}: 0 rows`) + continue + } + const cols = Object.keys(rows[0]!) + const placeholders = cols.map((_, i) => `$${i + 1}`).join(',') + const insertSql = `INSERT INTO ${name} (${cols.join(',')}) VALUES (${placeholders})` + for (const row of rows) { + await client.query(insertSql, cols.map((c) => row[c])) + } + console.log(`${name}: ${rows.length} row(s) migrated`) + } + await client.query('COMMIT') + } catch (err) { + await client.query('ROLLBACK') + throw err + } finally { + client.release() + } + console.log('Migration complete.') +} + +main().catch((e: unknown) => { + console.error(e) + process.exit(1) +})