# Cron Jobs

M8. systemd-user timers + a command allowlist.

## Model

A **Cron Job** is:

- 5-field cron schedule (`min hour day month dow`).
- An owner user (must have a Linux account).
- A command from the **allowlist** (`php`, `wp`, plus a curated set per-app).
- An optional name (display label).

Internally each cron row becomes:

- `~/.config/systemd/user/jabali-cron-<id>.service` — the command.
- `~/.config/systemd/user/jabali-cron-<id>.timer` — the schedule.

Both are owned by the user. `loginctl enable-linger <user>` runs at user creation so timers fire without an active session.

## Why not crontab?

systemd-user timers give:

- Per-job journal logs (`journalctl --user -u jabali-cron-<id>`).
- `OnFailure=` hook → notification dispatcher.
- `RandomizedDelaySec=` — natural jitter without operators having to add `sleep $RANDOM`.
- No per-user `crontab -e` shell access (we don't grant interactive shell to panel users).

## Command allowlist

Only commands the admin has marked allowed can be scheduled. The default allowlist (`/internal/cronvalidate/`) is the shared validator used by both the REST API and the CLI (`Cron Job Intake` — the single ingest path, per CONTEXT.md). Custom shell scripts are not allowed by default — admins can extend the allowlist.

## CLI

```bash
jabali cron list --user <id>
jabali cron add --user <id> --schedule "0 3 * * *" --command "wp cron event run --due-now --url=https://example.com"
jabali cron update <job-id> --schedule "*/15 * * * *"
jabali cron delete <job-id>
jabali cron run-now <job-id>     # synchronous, ignores schedule
```

## Failures

If a job exits non-zero, the `OnFailure=jabali-cron-notify@%n.service` unit fires and the notifications dispatcher (M14) sends an alert via the user's configured channels (in-app bell, email, etc.).
