pediatric-ai-scribe-v3/scripts/maintenance.js
Daniel 0bbecb76f9 Maintenance CLI + unpin postgres digest
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.
2026-04-14 04:00:13 +02:00

116 lines
4.1 KiB
JavaScript

#!/usr/bin/env node
// ============================================================
// PedScribe maintenance CLI
// Usage: node scripts/maintenance.js <command>
//
// 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 <cmd>');
log(' docker exec pediatric-ai-scribe npm run maint:<cmd>');
}
} catch (e) {
warn(e.message);
process.exit(1);
} finally {
await pool.end();
}
})();