import { sdk } from '../sdk' /** * verify-database — StartOS Package Action. * * Operator confidence check. Runs SQLite's `PRAGMA integrity_check` and * a `PRAGMA quick_check` against /data/app.db, plus a few row-count * sanity queries. Reports the results in the StartOS UI as a * structured `result` so the operator can spot anomalies at a glance * without dropping into a shell. * * Use cases: * - "Did the StartOS Backup last night actually capture a healthy * DB?" (Run the action AFTER a backup completes; integrity_check * ok means the file is consistent.) * - "I had a power loss / the host crashed — is the DB ok?" * - "I just sideloaded a fresh image — did the seed copy correctly?" * * Read-only. Safe to run while the service is running. We use * allowedStatuses: 'only-running' so the SQL goes through the live * service's data-mounted subcontainer rather than starting one. */ export const verifyDatabase = sdk.Action.withoutInput( 'verify-database', async () => ({ name: 'Verify database integrity', description: "Read-only. Runs SQLite PRAGMA integrity_check + quick_check against /data/app.db and reports row counts. Useful after a backup, a host crash, or a fresh sideload.", warning: null, visibility: 'enabled', allowedStatuses: 'only-running', group: null, }), async ({ effects }) => { let result = { integrity: 'unknown', quickCheck: 'unknown', counts: {} as Record } await sdk.SubContainer.withTemp( effects, { imageId: 'main' }, sdk.Mounts.of().mountVolume({ volumeId: 'main', subpath: null, mountpoint: '/data', readonly: true, }), 'verify-database', async (sc) => { const sql = [ "SELECT 'integrity:' || (SELECT * FROM pragma_integrity_check LIMIT 1);", "SELECT 'quick:' || (SELECT * FROM pragma_quick_check LIMIT 1);", "SELECT 'count:' || 'User' || '=' || (SELECT COUNT(*) FROM User);", "SELECT 'count:' || 'Workout' || '=' || (SELECT COUNT(*) FROM Workout WHERE deletedAt IS NULL);", "SELECT 'count:' || 'Exercise' || '=' || (SELECT COUNT(*) FROM Exercise);", "SELECT 'count:' || 'SetLog' || '=' || (SELECT COUNT(*) FROM SetLog);", "SELECT 'count:' || 'Session' || '=' || (SELECT COUNT(*) FROM Session WHERE expiresAt > CURRENT_TIMESTAMP);", ].join('\n') const res = await sc.execFail(['sqlite3', '/data/app.db'], { input: sql }, 60_000) const lines = res.stdout .toString() .split('\n') .map((s) => s.trim()) .filter(Boolean) for (const line of lines) { if (line.startsWith('integrity:')) { result.integrity = line.slice('integrity:'.length) } else if (line.startsWith('quick:')) { result.quickCheck = line.slice('quick:'.length) } else if (line.startsWith('count:')) { const [name, value] = line.slice('count:'.length).split('=') if (name && value !== undefined) { result.counts[name] = value } } } }, ) const ok = result.integrity === 'ok' && result.quickCheck === 'ok' return { version: '1', title: ok ? 'Database is healthy' : 'Database integrity issues detected', message: ok ? 'integrity_check + quick_check both returned ok. Row counts below.' : `integrity_check returned: ${result.integrity}; quick_check returned: ${result.quickCheck}. Investigate before relying on the next backup.`, result: { type: 'group', value: [ { type: 'single', name: 'integrity_check', description: 'PRAGMA integrity_check result. Should be "ok".', value: result.integrity, copyable: false, qr: false, masked: false, }, { type: 'single', name: 'quick_check', description: 'PRAGMA quick_check result. Should be "ok".', value: result.quickCheck, copyable: false, qr: false, masked: false, }, ...Object.entries(result.counts).map(([name, value]) => ({ type: 'single' as const, name: `${name} rows`, description: name === 'Session' ? 'Active (unexpired) sessions.' : null, value, copyable: false, qr: false, masked: false, })), ], }, } }, )