refactor: pos system event-trigger → pos system schedule — per-job timers, notify policies, legacy migrate

The single-timer threshold monitor generalizes into a scheduler: each job is a
chmod-600 file in schedule.d/<name>.env (INTERVAL 5m..59m/1h..23h/hourly/daily/
weekly/OnCalendar=..., NOTIFY policy, optional MSG, RULE for threshold,
COMMAND = literal rest of line) with its own systemd user timer pair
(pos-schedule-<name>.timer + oneshot .service, Persistent, reconciled on
enable/disable — orphan units + the legacy pos-event-trigger timer
auto-removed). Policies: always (full output every run), onchange (diff vs
last run, first run sends), onerror (non-zero exit or empty output),
threshold (old event-trigger behavior: first numeric vs RULE, alert on
false→true + recovery, per-job firing state), never (silent side-effect jobs).
run [name|all], list, config (interactive add/edit/remove/enable/disable with
validation), enable/disable [name|all], status, migrate (converts legacy
event.env rules → rule-N.env threshold jobs, verbatim LHS as COMMAND, adopts
the legacy timer's OnCalendar or 5m, removes the old timer). Per-run logs +
state in ~/.local/share/linux_post_install/schedule/{logs,state}/.
config/event.env + event-rules.template → config/schedule.d/ starter jobs
(nvme-health, cpu-temp, disk-root, silent log-cleanup); postinstall installs
them no-clobber into an empty schedule.d/ (legacy event.env users get a
migrate hint instead). bin/pos EXAMPLES + INTERACTIVE_CMDS
(system-schedule config) updated; install.sh ships the renamed tool+lib.
This commit is contained in:
Your Name
2026-08-13 02:14:25 -04:00
parent 8b3435a8d5
commit 414a990801
18 changed files with 1134 additions and 727 deletions
+151
View File
@@ -0,0 +1,151 @@
# How-To: `pos system schedule`
Scheduled jobs: run any shell command on a timer and have the result sent via
`lib/notify.sh` (Telegram by default, `NOTIFY_PLATFORM` for more) — or run it
silently. Unlike the old `event-trigger` threshold monitors, each job has its
own interval and its own notify policy.
```bash
pos system schedule list # jobs + notify policy + last run
pos system schedule enable # start all job timers (per-job intervals)
pos system schedule status # per-job timer state + next run
pos system schedule run nvme-health # run one job now
pos system schedule run all --dry-run # preview everything
pos system schedule disable cpu-temp # stop a job, keep the file
pos system schedule migrate # convert old event.env rules
pos system schedule config # interactive job editor
```
---
## Job format
One file per job in `~/.config/linux_post_install/schedule.d/<name>.env`
(chmod 600):
```
INTERVAL=hourly # 5m..59m | 1h..23h | hourly daily weekly | OnCalendar=…
NOTIFY=onchange # always | onchange | onerror | threshold | never
MSG="NVMe health" # optional label; threshold alert text when NOTIFY=threshold
RULE="> 60c" # threshold only: op + threshold (unit suffix fine: 60c, 80%)
COMMAND=… # literal rest of the line — pipes/quotes/sudo fine
ENABLED=false # written by `disable`; default enabled
```
The `COMMAND=` value is everything after the prefix — no quoting/escaping of
pipes, quotes, or `sudo`. The job runs it with `bash -c` and observes the
exit code. Only `INTERVAL` is required (default `5m`); `NOTIFY` defaults to
`threshold` when `RULE` is present, otherwise `onchange`.
### Notify policies
| Policy | Behavior |
|--------|----------|
| `threshold` | Runs the command, compares its **first numeric output** against `RULE` (float-safe). Alerts once on false→true plus one recovery message on true→false — a hot CPU for two hours is one message, not twenty. `MSG` is the alert text (auto-composed when absent) |
| `onchange` | Sends the output only when it differs from the last run. First run always sends. Best for "tell me when this changes" reports |
| `always` | Sends the full output on every run |
| `onerror` | Sends only when the command fails (non-zero exit or empty output), with the `rc` |
| `never` | Runs and logs, never notifies — side-effect jobs (cleanups, backups) |
Every run also writes a per-job log + last-run record
(`~/.local/share/linux_post_install/schedule/{logs,state}/`) so `list`/`status`
show the result even for `never` jobs.
## Example — NVMe health every hour
Your exact use case:
```
# ~/.config/linux_post_install/schedule.d/nvme-health.env
INTERVAL=hourly
NOTIFY=onchange
MSG=NVMe health
COMMAND=sudo -n smartctl -a /dev/nvme0n1 | grep -Ei 'critical_warning|temperature|available_spare|percentage_used|media_errors|error_information'
```
Two setup notes:
1. **Passwordless sudo for smartctl.** User timers have no tty, so `sudo` needs
a NOPASSWD rule (smartmontools is in `preinstall.sh` PACKAGES):
```bash
echo '%sudo ALL=(ALL) NOPASSWD: /usr/sbin/smartctl' | sudo tee /etc/sudoers.d/smartctl
```
(Use `/usr/sbin/smartctl` — check `command -v smartctl`.) The `-n` flag makes
`sudo` fail instead of hanging if the rule is missing, which `onerror` will
report.
2. **`temperature` drifts**, so an `onchange` job on this grep will fire most
hours. If you only want fault-field alerts, drop the `temperature`
alternative: `grep -Ei 'critical_warning|available_spare|percentage_used|media_errors|error_information'`.
## Example — threshold rule (old event-trigger style)
```
# cpu-temp.env
INTERVAL=5m
NOTIFY=threshold
MSG=CPU too hot
RULE="> 60c"
COMMAND=sensors -u coretemp-isa-0000 | awk '/Package id 0:/{f=1} f && /temp1_input:/{print $2; exit}'
```
Gotchas carried over from event-trigger:
- Non-numeric/empty output → the job is skipped with a warning (others still run).
- **Pin the chip in `sensors` rules** — a bare `grep -m1 temp1_input` can match a
different chip's `temp1_input` first (e.g. `acpitz`'s case temp). Find the
real name in `sensors -u` (e.g. `coretemp-isa-0000`) and pin it as above.
- Unit suffixes on the threshold are fine: `60c`, `80%`, `10g`.
## Example — silent side-effect job
```
# log-cleanup.env — never notifies
INTERVAL=weekly
NOTIFY=never
COMMAND=find ~/.local/share/linux_post_install/logs -type f -name '*_pos_*.log' -mtime +30 -delete
```
## Scheduling
Each enabled job gets its own systemd **user timer pair**
(`pos-schedule-<name>.timer` + oneshot `.service` running
`pos system schedule run <name>`, `Persistent=true` — missed runs fire on next
boot/login). `enable`/`disable` reconcile the timers with `schedule.d/` and
remove orphaned units; the legacy single `pos-event-trigger` timer is cleaned
up automatically. Requires a reachable user systemd manager; run
`sudo loginctl enable-linger $USER` once so timers fire without login (the tool
tries this and warns if it can't).
## Migrating from event-trigger
If you have rules in `~/.config/linux_post_install/event.env`, convert them in
place:
```bash
pos system schedule migrate
```
Each rule becomes `schedule.d/rule-N.env` with `NOTIFY=threshold`, the legacy
timer's interval (or `5m`), and the rule split into `MSG`/`RULE`/`COMMAND`.
The old timer is disabled and removed; run `pos system schedule enable` to
start the migrated jobs.
`migrate` copies the rule's left side **verbatim** as `COMMAND` — the old tool
ran it literally and never had `disk root`/`loadavg`-style shorthands. A rule
like `disk root > 80%` migrates, but its job will log "produced no number" on
every run. After migrating, rewrite such jobs with a real command, e.g.
`COMMAND=df -P / | awk 'NR==2{print $5+0}'` (see the `config/schedule.d/`
starter jobs for patterns).
## Alerting
`run` sends via `lib/notify.sh notify_send`, which delivers to every platform
in `NOTIFY_PLATFORM` (default `telegram`; comma-separated = fan out). Adding a
Matrix/Synapse sender needs no changes here — see DOC/DEV.md → Alerting.
---
## Related
- Reference: [DOC/POS.md → system](../POS.md#system)
- Notifications: [communication](howto/communication.md) / `lib/notify.sh`