# Phase 7a — the 6:00am n8n schedule (AUD-115) Phase 7 (AUD-111) proved the delivery path: `POST /notify/morning`, the per-boy and combined briefs, the Apprise channels, and eight real deliveries. This phase makes the schedule real: one n8n workflow that runs every day at 06:00 and sends the briefs. ## What changed `deploy/n8n-morning-brief.json` is now a two-node workflow: ``` Schedule Trigger "0 6 * * *" (Australia/Melbourne) | v Execute Command ssh … sam@192.168.20.13 'scripts/morning-brief.sh' ``` The earlier draft had three nodes (`scan` via `ssh remote` → `POST /notify/morning`). Two problems made that shape wrong here: 1. **`ssh remote` does not work.** The n8n container mounts `~/.ssh` read-only, and `~/.ssh/config` is a Nix symlink into `/nix/store`, which does not exist inside the container. The hostname `remote` does not resolve, and the container key is not in `~/.ssh/authorized_keys` (the host alias fails too). 2. **A split scan/send node hides scan failures.** If the scan node fails, n8n stops before the HTTP node, so `POST /notify/morning` never runs and the family gets no brief at all — the opposite of FR-12. `scripts/morning-brief.sh` is already the bounded command that does both steps and exits non-zero when the scan or a send fails. One Execute Command node is therefore both simpler and more correct: a failed scan turns the n8n execution red **and** still sends a brief that opens with the failure (`SCAN FAILED`), never "nothing due". Because the single command runs on the host, n8n needs **no** `SCHOOL_NOTIFY_TOKEN` and no HTTP node. The token stays in `environment.d/school-service.env` only. ## The SSH trigger key The n8n container mounts `/home/sam/.ssh` read-only at `/home/node/.ssh`. The trigger uses a dedicated key with a forced command, so it can run exactly one thing: ``` restrict,command="cd /home/sam/paseo/projects/compass_teams_nhs && ./scripts/morning-brief.sh" ssh-ed25519 AAAA… ``` - `restrict` disables pty, agent forwarding, port forwarding, X11, and shell access. - `command=` makes the server ignore whatever command the client sends. - The private key lives at `/home/sam/.ssh/id_ed25519_school_scan` (mode 0600) and is readable only by `sam` and the n8n container (which already mounts `~/.ssh`). - The key is not sam's personal key and is not used anywhere else. Create it with: ```bash deploy/setup-n8n-scan-key.sh # dry run: print the plan deploy/setup-n8n-scan-key.sh --apply # create the key + the authorized_keys line ``` ## Import and activate ```bash deploy/import-n8n-morning-brief.sh # dry run: print the plan deploy/import-n8n-morning-brief.sh --apply # import, publish, restart n8n ``` The script copies the JSON into the container with `docker exec -i … sh -c 'cat > …'` (`docker cp` fails against the read-only `~/.ssh` mount), runs `n8n import:workflow`, then `n8n publish:workflow --id=` (n8n 2.x replaced `update:workflow --active`), restarts n8n once so the 06:00 schedule registers, and prints `n8n list:workflow --active=true`. The JSON carries a stable `id` (`schoolMorningBrf`). n8n 2.18.5 refuses an import whose workflow has no `id` (`SQLITE_CONSTRAINT: NOT NULL constraint failed: workflow_entity.id`), so the id is required, not cosmetic. Re-importing the same file updates the same workflow instead of adding a second copy. `n8n publish:workflow` prints *"Changes will not take effect if n8n is running"*, which is why the import script restarts n8n once. The full import → publish → active check was rehearsed in a throwaway n8n container with a temporary data directory before this plan was proposed; no shared instance was touched. ## Verification ```bash docker exec n8n n8n list:workflow --active=true # the workflow is published docker exec compass-teams-nhs /app/school notify status ``` A healthy morning looks like this in `notify status`: ``` 1 Harry Harry email ok 06:0x 2 Harry Harry whatsapp ok 06:0x 3 Finn Finn email ok 06:0x 4 Finn Finn whatsapp ok 06:0x 5 combined Jo email ok 06:0x 6 combined Jo whatsapp ok 06:0x 7 combined Sam email ok 06:0x 8 combined Sam whatsapp ok 06:0x ``` Failure drill (never leave an empty day): point the key's forced command at a command that exits non-zero, trigger the workflow, and confirm the n8n execution is red and the brief body starts with the scan failure. Restore the command afterwards. ## Proving a run The Schedule Trigger only fires at 06:00, so the schedule itself is proved by `n8n list:workflow --active=true` plus the next morning's `notify status` rows. To prove the full send path without waiting, run the published workflow once by hand: ```bash docker exec n8n n8n execute --id= ``` This sends the real briefs, so run it only when a real delivery is wanted. `n8n list:workflow` prints the id. Both checks are recorded in this issue's final comment. ## Not touched - Compass / Teams: read-only. Nothing in this phase writes to the school. - Other n8n workflows: untouched. - Secrets: no address, phone number, Apprise key, session secret, or notify token is written into the workflow JSON, this document, or git.