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.
This commit is contained in:
parent
cd2513d361
commit
0bbecb76f9
4 changed files with 150 additions and 4 deletions
27
README.md
27
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
116
scripts/maintenance.js
Normal file
116
scripts/maintenance.js
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
#!/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();
|
||||
}
|
||||
})();
|
||||
Loading…
Reference in a new issue