From 0bbecb76f9b98a8d6e7d8b9a06f9de16200611ea Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 14 Apr 2026 04:00:13 +0200 Subject: [PATCH] Maintenance CLI + unpin postgres digest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `npm run maint:check` (health report) and `npm run maint:reindex` (REINDEX DATABASE + REFRESH COLLATION VERSION + ANALYZE) for post- upgrade maintenance, modelled after Nextcloud's occ maintenance. Documented in README. Also relaxes postgres image from digest pin back to tag-pin (pgvector/pgvector:pg16) — the auto-REINDEX-on-drift check in database.js and the COLLATE "C" protection on critical indexes make the digest pin redundant while blocking ordinary `compose pull` updates. --- README.md | 27 ++++++++++ docker-compose.yml | 7 +-- package.json | 4 +- scripts/maintenance.js | 116 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 scripts/maintenance.js diff --git a/README.md b/README.md index 1cbb1e8..55c8323 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,33 @@ SMTP_FROM=noreply@yourdomain.com --- +## Maintenance CLI + +After a Postgres image upgrade (major version bump or silent base-layer change), +btree indexes on text columns can become inconsistent with the new ICU/glibc +library. The app auto-detects this at startup and reindexes on drift, but you +can also trigger it manually: + +```bash +# Health check — no writes +docker exec pediatric-ai-scribe npm run maint:check + +# Rebuild all indexes + refresh collation + ANALYZE +docker exec pediatric-ai-scribe npm run maint:reindex +``` + +Run `maint:reindex` any time after: + +- Upgrading the Postgres image (major or minor) +- Restoring from a dump created on a different Linux distro +- Seeing "invalid credentials" on credentials you know are correct +- Seeing `0 rows` returned from a lookup that should match + +The reindex takes seconds on a small DB and a minute or two on larger ones. +Safe to run while the app is serving traffic, though queries may slow briefly. + +--- + ## Docker Hub ```bash diff --git a/docker-compose.yml b/docker-compose.yml index 6baaa59..4b1ae41 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -21,9 +21,10 @@ services: start_period: 20s postgres: - # Pinned by digest — changing the pgvector image can change the bundled - # ICU library and corrupt existing text indexes. REINDEX after any bump. - image: pgvector/pgvector:pg16@sha256:7d400e340efb42f4d8c9c12c6427adb253f726881a9985d2a471bf0eed824dff + # Tag-pinned. If a newer pg16 image ships a different ICU library, the + # startup drift check in src/db/database.js auto-REINDEXes and + # refreshes the collation version. For stricter control, pin by digest. + image: pgvector/pgvector:pg16 environment: POSTGRES_DB: pedscribe POSTGRES_USER: pedscribe diff --git a/package.json b/package.json index 7deb5bf..4a2833c 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "AI-powered pediatric clinical documentation platform", "main": "server.js", "scripts": { - "start": "node server.js" + "start": "node server.js", + "maint:check": "node scripts/maintenance.js check", + "maint:reindex": "node scripts/maintenance.js reindex" }, "dependencies": { "@marp-team/marp-cli": "^4.3.1", diff --git a/scripts/maintenance.js b/scripts/maintenance.js new file mode 100644 index 0000000..c6dab6a --- /dev/null +++ b/scripts/maintenance.js @@ -0,0 +1,116 @@ +#!/usr/bin/env node +// ============================================================ +// PedScribe maintenance CLI +// Usage: node scripts/maintenance.js +// +// Commands: +// reindex Rebuild all indexes + refresh collation version +// (run this after any Postgres image upgrade) +// check Report collation/index health, no writes +// help Show this message +// ============================================================ + +require('dotenv').config(); +var { Pool } = require('pg'); + +var pool = new Pool({ + connectionString: process.env.DATABASE_URL + || 'postgres://' + (process.env.DB_USER || 'pedscribe') + + ':' + (process.env.DB_PASSWORD || process.env.POSTGRES_PASSWORD || '') + + '@' + (process.env.DB_HOST || 'postgres') + + ':' + (process.env.DB_PORT || 5432) + + '/' + (process.env.DB_NAME || 'pedscribe') +}); + +function log(msg) { process.stdout.write(msg + '\n'); } +function warn(msg) { process.stderr.write('⚠️ ' + msg + '\n'); } +function ok(msg) { process.stdout.write('✅ ' + msg + '\n'); } +function heading(m) { process.stdout.write('\n── ' + m + ' ──\n'); } + +async function check() { + var client = await pool.connect(); + try { + heading('Postgres version'); + var v = await client.query('SELECT version()'); + log(v.rows[0].version); + + heading('Collation version'); + var d = await client.query( + "SELECT datcollversion AS recorded, " + + "pg_database_collation_actual_version(oid) AS actual " + + "FROM pg_database WHERE datname = current_database()" + ); + var row = d.rows[0] || {}; + log('Recorded: ' + row.recorded); + log('Library: ' + row.actual); + if (row.recorded && row.actual && row.recorded !== row.actual) { + warn('Drift detected. Run `node scripts/maintenance.js reindex`'); + } else { + ok('Aligned — no drift'); + } + + heading('Row counts'); + var tables = ['users', 'user_sessions', 'audit_log', 'saved_encounters', 'audio_backups']; + for (var i = 0; i < tables.length; i++) { + try { + var c = await client.query('SELECT COUNT(*)::int AS n FROM ' + tables[i]); + log(tables[i].padEnd(20) + c.rows[0].n); + } catch (e) { log(tables[i].padEnd(20) + '(missing)'); } + } + + heading('Text indexes'); + var idx = await client.query( + "SELECT indexname, indexdef FROM pg_indexes " + + "WHERE schemaname='public' AND (indexdef ILIKE '%email%' OR indexdef ILIKE '%token%' OR indexdef ILIKE '%slug%' OR indexdef ILIKE '%COLLATE%')" + ); + idx.rows.forEach(function(r) { + var hasC = /COLLATE "C"/.test(r.indexdef); + log((hasC ? '[C] ' : '[ ] ') + r.indexname); + }); + } finally { client.release(); } +} + +async function reindex() { + var client = await pool.connect(); + try { + heading('REINDEX DATABASE'); + var start = Date.now(); + log('Running REINDEX DATABASE pedscribe (this can take a few seconds on small DBs, longer on large)...'); + await client.query('REINDEX DATABASE ' + (process.env.DB_NAME || 'pedscribe')); + ok('REINDEX complete (' + ((Date.now() - start) / 1000).toFixed(1) + 's)'); + + heading('REFRESH COLLATION VERSION'); + await client.query('ALTER DATABASE ' + (process.env.DB_NAME || 'pedscribe') + ' REFRESH COLLATION VERSION'); + ok('Collation version refreshed'); + + heading('ANALYZE'); + await client.query('ANALYZE'); + ok('Statistics updated'); + + log('\nAll done. You can now continue using the app normally.'); + } finally { client.release(); } +} + +(async function main() { + var cmd = process.argv[2] || 'help'; + try { + if (cmd === 'reindex') await reindex(); + else if (cmd === 'check') await check(); + else { + log('PedScribe maintenance CLI'); + log(''); + log('Commands:'); + log(' check Report health (no writes)'); + log(' reindex REINDEX + REFRESH COLLATION + ANALYZE'); + log(''); + log('Run inside the container:'); + log(' docker exec pediatric-ai-scribe node scripts/maintenance.js '); + log(' docker exec pediatric-ai-scribe npm run maint:'); + } + } catch (e) { + warn(e.message); + process.exit(1); + } finally { + await pool.end(); + } +})();