feat(maintenance): reminders CLI + systemd timer drop-in
Phase 3-4 of maintenance reminders. scripts/maintenance-reminders.ts: thin CLI that opens the same pg pool as the app and calls runRemindersOnce. Flags: --soon-days N (default 7), --company <id> (default all), --dry-run (count without firing), --backfill (mark all currently due as already-sent, mutually exclusive with --dry-run). Prints a single-line JSON summary so journald/jq handles it cleanly. package.json: + reminders:check script. DEPLOYMENT.md: documents the systemd .service + .timer pair for 06:00 daily, with Persistent=true so a missed run during host downtime still fires on next boot. Includes the first-deploy protocol (--dry-run to scope, then either let day-one alert or --backfill for a clean slate). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
import 'dotenv/config';
|
||||
import { pool } from '../src/lib/server/db/client';
|
||||
import {
|
||||
runRemindersOnce,
|
||||
type RunResult
|
||||
} from '../src/lib/server/services/maintenance-reminders';
|
||||
|
||||
function stripSurroundingQuotes(s: string | undefined): string | undefined {
|
||||
if (!s || s.length < 2) return s;
|
||||
const first = s[0];
|
||||
const last = s[s.length - 1];
|
||||
if ((first === "'" && last === "'") || (first === '"' && last === '"')) {
|
||||
return s.slice(1, -1);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function readArg(flag: string, fallback?: string): string | undefined {
|
||||
const i = process.argv.indexOf(flag);
|
||||
return stripSurroundingQuotes(i >= 0 ? process.argv[i + 1] : fallback);
|
||||
}
|
||||
|
||||
function hasFlag(flag: string): boolean {
|
||||
return process.argv.includes(flag);
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const soonRaw = readArg('--soon-days', '7') ?? '7';
|
||||
const soonDays = Number.parseInt(soonRaw, 10);
|
||||
if (!Number.isFinite(soonDays) || soonDays < 0 || soonDays > 365) {
|
||||
console.error('--soon-days must be an integer in [0, 365]');
|
||||
process.exit(2);
|
||||
}
|
||||
const companyId = readArg('--company');
|
||||
const dryRun = hasFlag('--dry-run');
|
||||
const backfill = hasFlag('--backfill');
|
||||
|
||||
if (dryRun && backfill) {
|
||||
console.error('--dry-run and --backfill are mutually exclusive');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const startedAt = new Date();
|
||||
let result: RunResult;
|
||||
try {
|
||||
result = await runRemindersOnce({ companyId, soonDays, dryRun, backfill });
|
||||
} catch (e) {
|
||||
console.error(JSON.stringify({ ok: false, error: (e as Error).message }));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const out = {
|
||||
ok: true,
|
||||
startedAt: startedAt.toISOString(),
|
||||
durationMs: Date.now() - startedAt.getTime(),
|
||||
soonDays,
|
||||
companyId: companyId ?? null,
|
||||
...result
|
||||
};
|
||||
// Single-line JSON so journald/grep handle it cleanly.
|
||||
console.log(JSON.stringify(out));
|
||||
}
|
||||
|
||||
main()
|
||||
.catch((e) => {
|
||||
console.error(JSON.stringify({ ok: false, error: (e as Error).message }));
|
||||
process.exitCode = 1;
|
||||
})
|
||||
.finally(async () => {
|
||||
await pool.end().catch(() => {});
|
||||
});
|
||||
Reference in New Issue
Block a user