From 11155df1fe3f1f53cf60bf5570013c1418623879 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Fri, 16 Oct 2026 10:31:40 +1100 Subject: [PATCH] Phase 7a: read-only verifier for the 06:00 scheduled run (AUD-115) make verify-n8n / deploy/verify-n8n-scheduled-run.sh checks the workflow is active, reads the newest scheduled (mode=trigger) execution from the n8n database, and prints the notify rows. Exit 1 until a scheduled run exists and is green, so it distinguishes 'not yet' from 'failed'. Never runs the workflow and never sends. Document the first-run time and the re-armed monitor. --- Makefile | 8 ++- deploy/DEPLOYMENT.md | 3 + deploy/verify-n8n-scheduled-run.sh | 99 ++++++++++++++++++++++++++++++ docs/phase-7a-n8n-schedule.md | 30 ++++++++- 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100755 deploy/verify-n8n-scheduled-run.sh diff --git a/Makefile b/Makefile index e1450a3..9101a21 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ GO_RUN = timeout 600 docker run --rm --network host --user "$$(id -u):$$(id -g)" -e HOME=/src/.gocache -e GOMODCACHE=/src/.gocache/mod -e GOCACHE=/src/.gocache/build \ -v "$$PWD":/src -w /src $(GO_IMAGE) -.PHONY: help test vet build up down restart logs ps deps fmt hash-password gen-secret verify-phase1 verify-phase8 parse scan calendar notify morning-brief n8n-key n8n-import +.PHONY: help test vet build up down restart logs ps deps fmt hash-password gen-secret verify-phase1 verify-phase8 parse scan calendar notify morning-brief n8n-key n8n-import verify-n8n help: @echo "make test run go test ./... in golang:1.25-alpine" @@ -40,6 +40,7 @@ help: @echo "make morning-brief scan both children, then send the briefs (the n8n job)" @echo "make n8n-key print the restricted SSH trigger key plan (Phase 7a)" @echo "make n8n-import print the n8n import/publish plan (Phase 7a; APPLY=1 applies)" + @echo "make verify-n8n prove the 06:00 scheduled run is green (Phase 7a; read-only)" @echo "make verify-phase1 prove /health, the login and the Caddy route" @echo "make verify-phase8 prove the Hub API: /capabilities, /ask, /notify (--send delivers)" @@ -112,6 +113,11 @@ n8n-key: n8n-import: deploy/import-n8n-morning-brief.sh $(if $(APPLY),--apply,) +# Phase 7a: prove the 06:00 scheduled run itself. Read-only; exit 1 until the +# first automatic run has happened and is green. +verify-n8n: + deploy/verify-n8n-scheduled-run.sh + # Phase 1 definition of done: /health 200, login page + session login, and the # Caddy route on .35. verify-phase1: diff --git a/deploy/DEPLOYMENT.md b/deploy/DEPLOYMENT.md index 9cc1bd6..972a551 100644 --- a/deploy/DEPLOYMENT.md +++ b/deploy/DEPLOYMENT.md @@ -109,6 +109,9 @@ the API directly on the internal address. node by default, so a command node cannot activate.) It needs no `SCHOOL_NOTIFY_TOKEN` in n8n — the host script reads the project env file. See `docs/phase-7a-n8n-schedule.md` for the design and the failure drill. + After the first 06:00, verify the scheduled run itself with the read-only + `make verify-n8n` (`deploy/verify-n8n-scheduled-run.sh`); it exits 1 until a + scheduled (`mode=trigger`) run exists and is green. Notes: diff --git a/deploy/verify-n8n-scheduled-run.sh b/deploy/verify-n8n-scheduled-run.sh new file mode 100755 index 0000000..cfc3813 --- /dev/null +++ b/deploy/verify-n8n-scheduled-run.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +# Phase 7a (AUD-115): prove the 06:00 n8n schedule fired and the briefs were sent. +# +# Read-only. It never runs the workflow and never sends anything, so it is safe to +# call from a monitor or by hand at any time. +# +# Checks: +# 1. the workflow is active in n8n, +# 2. the newest scheduled run (mode "trigger") of schoolMorningBrf finished +# "success" — a hand run (mode "cli") does not count, +# 3. prints the notify rows so the morning's 8 deliveries can be read. +# +# Exit 0 only when 1 and 2 hold. Exit 1 with a clear "no scheduled run yet" when the +# 06:00 trigger has not fired, so the caller can tell "not yet" from "failed". +# +# Usage: +# deploy/verify-n8n-scheduled-run.sh +# +# Environment overrides: +# CONTAINER n8n container name (default n8n) +# APP_CONTAINER app container name (default compass-teams-nhs) +# N8N_DATA_DIR n8n data dir on the host (default /home/sam/Docker/Containers/n8n_data) +# WF_ID workflow id (default schoolMorningBrf) +set -euo pipefail + +CONTAINER="${CONTAINER:-n8n}" +APP_CONTAINER="${APP_CONTAINER:-compass-teams-nhs}" +N8N_DATA_DIR="${N8N_DATA_DIR:-/home/sam/Docker/Containers/n8n_data}" +WF_ID="${WF_ID:-schoolMorningBrf}" +DB="$N8N_DATA_DIR/database.sqlite" + +fail=0 + +echo "== 1. the workflow is active in n8n ==" +if docker exec "$CONTAINER" n8n list:workflow --active=true 2>/dev/null | grep -q "$WF_ID"; then + echo "active: $WF_ID" +else + echo "NOT active: $WF_ID — the 06:00 schedule will not fire" >&2 + fail=1 +fi + +echo +echo "== 2. the newest scheduled (mode=trigger) execution ==" +if [[ ! -f "$DB" ]]; then + echo "n8n database not found: $DB (set N8N_DATA_DIR)" >&2 + fail=1 +elif python3 - "$DB" "$WF_ID" <<'PY' +import datetime, sqlite3, sys + +db, wf = sys.argv[1], sys.argv[2] +con = sqlite3.connect(f"file:{db}?mode=ro", uri=True, timeout=8) +con.row_factory = sqlite3.Row +rows = list(con.execute( + "select id, mode, status, startedAt, stoppedAt from execution_entity " + "where workflowId=? and mode='trigger' order by id desc limit 3", (wf,))) +if not rows: + print("no scheduled (mode=trigger) execution yet for", wf) + sys.exit(1) + +aedt = datetime.timezone(datetime.timedelta(hours=11)) + +def local(ts): + for fmt in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S"): + try: + dt = datetime.datetime.strptime(ts, fmt).replace(tzinfo=datetime.timezone.utc) + return dt.astimezone(aedt).strftime("%Y-%m-%d %H:%M %Z") + except ValueError: + continue + return ts + +for r in rows: + print(f" id={r['id']} mode={r['mode']} status={r['status']} started={local(r['startedAt'])}") + +newest = rows[0] +sys.exit(0 if newest["status"] == "success" else 1) +PY +then + echo "the newest scheduled run finished success" +else + echo "the newest scheduled run is not green (or no scheduled run has happened yet)" >&2 + fail=1 +fi + +echo +echo "== 3. notify rows (the morning's 8 deliveries) ==" +if docker exec "$APP_CONTAINER" /app/school notify status 2>/dev/null | grep -v '^gateway=' | tail -20; then + : +else + echo "could not read notify status from $APP_CONTAINER" >&2 + fail=1 +fi + +echo +if [[ "$fail" -eq 0 ]]; then + echo "OK: the 06:00 schedule is active, its newest scheduled run is green, rows above" +else + echo "NOT YET / FAILED: see the messages above" >&2 +fi +exit "$fail" diff --git a/docs/phase-7a-n8n-schedule.md b/docs/phase-7a-n8n-schedule.md index 993837a..6728e1a 100644 --- a/docs/phase-7a-n8n-schedule.md +++ b/docs/phase-7a-n8n-schedule.md @@ -147,6 +147,25 @@ already in use"*, because the running n8n instance owns 5679. It changes only th This sends the real briefs, so run it only when a real delivery is wanted. +## Verify the scheduled run (read-only) + +```bash +make verify-n8n # or: deploy/verify-n8n-scheduled-run.sh +``` + +`deploy/verify-n8n-scheduled-run.sh` never runs the workflow and never sends +anything. It checks that the workflow is active, reads the newest **scheduled** +(`mode=trigger`) execution from the n8n database, and prints the notify rows. A hand +run (`mode=cli`) does **not** satisfy it. The exit code distinguishes the states: + +- `0` — a scheduled run exists and its newest one finished `success`. +- `1` — the workflow is inactive, or the schedule has not fired yet, or the newest + scheduled run failed. + +It reads the n8n database directly (`N8N_DATA_DIR`, default +`/home/sam/Docker/Containers/n8n_data`) because n8n 2.x stores scheduled executions +with `mode=trigger`, which `n8n list:workflow` does not report. + ## Applied — 2026-10-16 Sam accepted plan revision 2 (`257b4b0a`), and the change was applied to the shared n8n @@ -166,8 +185,15 @@ on `.13`. Evidence from that heartbeat: all `ok` with `sent_at` set. The remaining check is the first automatic run: the Schedule Trigger fires at -**06:00 Australia/Melbourne on 2026-10-17**. An issue monitor on AUD-115 wakes the -assignee after that to record the rows. +**06:00 Australia/Melbourne on 2026-10-17** (`2026-10-16T19:00Z`). The n8n container +runs `TZ=Australia/Melbourne` and the workflow sets +`settings.timezone=Australia/Melbourne`; the pre-existing `AI Calendar - Family +Notifier` schedule stores its local 05:00/16:00/20:00 firings as 18:00/05:00/09:00 +UTC, which confirms the workflow timezone is honoured. + +The first issue monitor fired too early and was cleared, so it was re-armed for +**06:30 Australia/Melbourne on 2026-10-17** (`2026-10-16T19:30:00Z`) to record the +automatic run's rows with `make verify-n8n`. ## Not touched