feat(maintenance): reminders CLI + systemd timer drop-in
Deploy to LXC / deploy (push) Successful in 15s
Validate / validate (push) Successful in 35s

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:
2026-04-27 16:15:03 +07:00
parent f5e4743120
commit 09e0fdc9ac
3 changed files with 132 additions and 0 deletions
+60
View File
@@ -190,6 +190,66 @@ systemctl status buildfor_life_ops
journalctl -u buildfor_life_ops -f
```
### Maintenance reminders timer
The app records `next_due_at` on every time-based maintenance schedule but does not poll itself. A daily systemd timer runs `pnpm run reminders:check`, which scans for schedules entering the 7-day warning window or already overdue and fans out via the existing in-app + email + Matrix notifier. Re-runs are idempotent — `maintenance_reminders_sent` deduplicates per `(schedule, kind, due_at)`.
`/etc/systemd/system/buildfor_life_ops-reminders.service`:
```ini
[Unit]
Description=buildfor_life_ops maintenance reminder cron
After=postgresql.service network.target
Wants=postgresql.service
[Service]
Type=oneshot
User=ops
Group=ops
WorkingDirectory=/home/ops/buildfor_life_ops
EnvironmentFile=/home/ops/buildfor_life_ops/.env
Environment=NODE_ENV=production
ExecStart=/home/ops/.local/share/fnm/aliases/default/bin/pnpm run reminders:check
```
`/etc/systemd/system/buildfor_life_ops-reminders.timer`:
```ini
[Unit]
Description=Run buildfor_life_ops maintenance reminders daily
[Timer]
OnCalendar=*-*-* 06:00:00
Persistent=true
Unit=buildfor_life_ops-reminders.service
[Install]
WantedBy=timers.target
```
Enable:
```bash
sudo systemctl daemon-reload
sudo systemctl enable --now buildfor_life_ops-reminders.timer
sudo systemctl list-timers buildfor_life_ops-reminders.timer
```
**First-run protocol** to avoid a deluge of stale alerts on day one:
```bash
# Inspect what would fire without notifying.
sudo -iu ops bash -lc 'cd ~/buildfor_life_ops && pnpm run reminders:check -- --dry-run'
# If the count is reasonable, run normally — the timer will pick up subsequent
# windows automatically. Or, if you want a clean slate, mark everything
# currently-due as already-notified (no fan-out), so day-one alerts only
# new breaches:
sudo -iu ops bash -lc 'cd ~/buildfor_life_ops && pnpm run reminders:check -- --backfill'
```
Logs end up in `journalctl -u buildfor_life_ops-reminders.service`. Each run prints a single JSON line (`{ ok, scanned, fired, skippedDedup, noRecipients, ... }`) so `journalctl --output=cat | grep '"ok":true' | jq` gives a clean trend view.
## 11. Reverse proxy (nginx)
`/etc/nginx/sites-available/buildfor_life_ops`: