chore: adopt template scripts and git hooks, retire phase-versioning
Ten scripts from ~/.openclaw/Projects/Template, taken one at a time and
configured against this deployment rather than copied wholesale.
Configured, not just copied:
- check-env.sh SPEC written from what server/index.js actually reads — 24
variables, each with the consequence of getting it wrong
- secrets.sh plus this project's own shapes: a bare 60+ hex run, which is
how the Zoho WebToLead tokens leaked into four commits, and a
reCAPTCHA key shape as NOTED rather than a failure, because the
site key and the secret key are indistinguishable by shape
- status.sh nebula / qn-website-dev
- healthcheck.sh /api/health, asserting 200 AND "status":"ok" AND "db":"ok".
The template probed /healthz, which does not exist here
- preflight.sh https://qn.isnull.dev, no --auth — there are no accounts
- verify.sh GUARD_DIR=scripts/verify.d, since this project has no test
runner and no typecheck for it to detect
- backup.sh ENGINE block replaced for SQLite: better-sqlite3's online
.backup() inside the container, verified with PRAGMA
integrity_check before anything is renamed into place
- restore-check.sh rewritten rather than configured — the template's is
pg_restore/psql end to end with no seam. Replays the dump from
SQL into a scratch database and times it
Three guards in scripts/verify.d, because verify.sh would otherwise detect
nothing and exit 2: the build, the tracked-tree secret scan, and a check that
every document carries a valid Status, Governs and Review trigger.
Every guard was proven to fail before being trusted, per GUARDS.md rule 1:
healthcheck against a 200 that is not this app, secrets against the real
historical leak replayed out of 033bdf6, doc-headers against both a missing
Review trigger and the Status word "Historical", restore-check against a
truncated dump, an empty database and a raised row floor.
pre-commit is ADAPTED, not the template's. That one runs `npx tsc --noEmit` and
`npx vitest run`; this project has neither, so unchanged it would refuse every
commit. It runs the secret scan and `npm run build`. Hooks are not activated by
this commit — `git config core.hooksPath .githooks` is a separate, per-clone act.
package.json: adds `verify`, and corrects the version to 0.9.3. It said 0.8.3
while the last four commits said batch 0.9.0 through 0.9.3 — the second drift of
the phase-versioning rule, which is retired in the following commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:18:20 -05:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
#
|
|
|
|
|
# Liveness tick for a deployed service.
|
|
|
|
|
#
|
|
|
|
|
# ## Why this exists
|
|
|
|
|
#
|
|
|
|
|
# The five-minute healthcheck it replaced was prose handed to a model: "GET
|
|
|
|
|
# <origin>/healthz. Do NOT use /api/internal/v1/healthz." It duly reported a 404
|
|
|
|
|
# on `/api/internal/v1/health` — a third path, neither the one it was told to
|
|
|
|
|
# use nor the one it was told to avoid, and one that had never existed. The site
|
|
|
|
|
# was healthy throughout.
|
|
|
|
|
#
|
|
|
|
|
# An explicit prohibition constrained one wrong URL and left every other wrong
|
|
|
|
|
# URL open, because the address was being re-derived on every run rather than
|
|
|
|
|
# read. So it is written here once, as a string in version control, and the
|
|
|
|
|
# whole class of failure goes with it.
|
|
|
|
|
#
|
|
|
|
|
# That failure is the expensive kind. The job's own state read `lastRunStatus:
|
|
|
|
|
# ok, consecutiveErrors: 0` while a red alert went to a DM — so the monitor was
|
|
|
|
|
# reporting itself healthy and crying wolf at the same time, and a monitor
|
|
|
|
|
# nobody believes is a monitor nobody has.
|
|
|
|
|
#
|
|
|
|
|
# ## It holds no credential, and that is the point
|
|
|
|
|
#
|
|
|
|
|
# `/healthz` is unauthenticated by design — `SECURITY_CHECKLIST.md` makes it a
|
|
|
|
|
# checklist item, and the container's own HEALTHCHECK uses it. So unlike
|
|
|
|
|
# `reconcile.sh` and `analyze.sh`, this script reads no token, sources no env
|
|
|
|
|
# file, and has nothing to leak. At 288 runs a day that is worth more than the
|
|
|
|
|
# extra assurance an authenticated probe would buy.
|
|
|
|
|
#
|
|
|
|
|
# The authenticated sibling, `/api/internal/v1/agent/health`, reports more and
|
|
|
|
|
# needs a token. It is deliberately not used here: this asks "is the site up",
|
|
|
|
|
# which is a question with a public answer.
|
|
|
|
|
#
|
|
|
|
|
# ## Both halves are checked
|
|
|
|
|
#
|
|
|
|
|
# 503 is a real answer, not an outage — `/api/health` returns it when the
|
|
|
|
|
# `SELECT 1` against SQLite fails, so the container is marked unhealthy while
|
|
|
|
|
# the marketing pages keep serving perfectly well. Status alone is therefore
|
|
|
|
|
# not a verdict; the check asserts HTTP 200 *and* `"status":"ok"` *and*
|
|
|
|
|
# `"db":"ok"`.
|
|
|
|
|
#
|
|
|
|
|
# ## The path, written down rather than re-derived
|
|
|
|
|
#
|
|
|
|
|
# This project's endpoint is `/api/health`. It is NOT `/healthz`, which is what
|
|
|
|
|
# the template's copy of this script probed and what a plausible guess produces.
|
|
|
|
|
# The whole argument above is about exactly this line, so it is stated once,
|
|
|
|
|
# here, and nowhere else.
|
|
|
|
|
#
|
|
|
|
|
# ## Installing it
|
|
|
|
|
#
|
|
|
|
|
# install -m 0755 healthcheck.sh ~/bin/healthcheck.sh
|
|
|
|
|
# # then, in the crontab — every five minutes
|
|
|
|
|
# */5 * * * * $HOME/bin/healthcheck.sh \
|
|
|
|
|
# >> $HOME/.healthcheck.log 2>&1
|
|
|
|
|
#
|
|
|
|
|
# On this deployment it is run by an OpenClaw cron job instead, which is the
|
|
|
|
|
# same thing with a scheduler that can also deliver the alert.
|
|
|
|
|
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
|
|
|
|
|
# `-`, not `:-`. Unset means "no opinion, use production". Set-and-empty means
|
|
|
|
|
# a config is wrong, and substituting production for it would report the health
|
|
|
|
|
# of a site nobody asked about — quietly, and only where somebody was trying to
|
|
|
|
|
# point this somewhere else.
|
fix(infra): queuenorth.com is the production origin, and it is this deployment
_null confirmed queuenorth.com as the permanent public origin, which settles
#212 and exposed a wrong claim written earlier the same day.
docs/OPERATIONS.md said queuenorth.com "is not this deployment ... do not
diagnose against it". That came from a DNS lookup and an assumption. It is this
deployment: both hostnames serve the identical bundle and this server's own
/api/health shape, 24.41.108.95 is this network's own public IP, and both reach
qn-website-dev on nebula — queuenorth.com through nginx-proxy-manager on
thor/exodus, qn.isnull.dev through Cloudflare. Two front doors, one container,
no non-production environment.
That is the worst direction for a runbook to be wrong in, so the correction
quotes the wrong sentence rather than replacing it silently. The QA Round 0
table likewise gained the production observations as extra rows instead of
having its originals rewritten.
Dockerfile: the CORS_ORIGIN fallback was '*'. The server sets credentials:true
and browsers reject '*' with credentials outright, so that fallback would have
broken every form rather than over-permitting. Now the real origin.
healthcheck.sh and preflight.sh now watch production by default, with the
second front door reachable through their env overrides — the two ingresses
terminate TLS in different places and can rot independently.
Also fills a gap adoption left explicitly undone: the deploy path is a Portainer
stack, id 58 on nebula, found from the container's own compose labels.
OPERATIONS.md documents it, including that the stack file is a separate copy
from this repository's docker-compose.yml and the two have already drifted.
That drift is all that remains of #212 — one trailing slash on line 21 of the
stack file. Left in place: nothing is broken today, and fixing it recreates the
container and takes both front doors down together.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:08:12 -05:00
|
|
|
# The PRODUCTION origin — the one customers use. qn.isnull.dev is the same
|
|
|
|
|
# container through a different ingress, and is worth checking separately when
|
|
|
|
|
# you are trying to tell the app apart from what is in front of it:
|
|
|
|
|
#
|
|
|
|
|
# HEALTHCHECK_BASE_URL=https://qn.isnull.dev bash scripts/healthcheck.sh
|
|
|
|
|
BASE_URL="${HEALTHCHECK_BASE_URL:-https://queuenorth.com}"
|
chore: adopt template scripts and git hooks, retire phase-versioning
Ten scripts from ~/.openclaw/Projects/Template, taken one at a time and
configured against this deployment rather than copied wholesale.
Configured, not just copied:
- check-env.sh SPEC written from what server/index.js actually reads — 24
variables, each with the consequence of getting it wrong
- secrets.sh plus this project's own shapes: a bare 60+ hex run, which is
how the Zoho WebToLead tokens leaked into four commits, and a
reCAPTCHA key shape as NOTED rather than a failure, because the
site key and the secret key are indistinguishable by shape
- status.sh nebula / qn-website-dev
- healthcheck.sh /api/health, asserting 200 AND "status":"ok" AND "db":"ok".
The template probed /healthz, which does not exist here
- preflight.sh https://qn.isnull.dev, no --auth — there are no accounts
- verify.sh GUARD_DIR=scripts/verify.d, since this project has no test
runner and no typecheck for it to detect
- backup.sh ENGINE block replaced for SQLite: better-sqlite3's online
.backup() inside the container, verified with PRAGMA
integrity_check before anything is renamed into place
- restore-check.sh rewritten rather than configured — the template's is
pg_restore/psql end to end with no seam. Replays the dump from
SQL into a scratch database and times it
Three guards in scripts/verify.d, because verify.sh would otherwise detect
nothing and exit 2: the build, the tracked-tree secret scan, and a check that
every document carries a valid Status, Governs and Review trigger.
Every guard was proven to fail before being trusted, per GUARDS.md rule 1:
healthcheck against a 200 that is not this app, secrets against the real
historical leak replayed out of 033bdf6, doc-headers against both a missing
Review trigger and the Status word "Historical", restore-check against a
truncated dump, an empty database and a raised row floor.
pre-commit is ADAPTED, not the template's. That one runs `npx tsc --noEmit` and
`npx vitest run`; this project has neither, so unchanged it would refuse every
commit. It runs the secret scan and `npm run build`. Hooks are not activated by
this commit — `git config core.hooksPath .githooks` is a separate, per-clone act.
package.json: adds `verify`, and corrects the version to 0.9.3. It said 0.8.3
while the last four commits said batch 0.9.0 through 0.9.3 — the second drift of
the phase-versioning rule, which is retired in the following commit.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:18:20 -05:00
|
|
|
TIMEOUT="${PRIVACY_TIMEOUT:-15}"
|
|
|
|
|
|
|
|
|
|
stamp() { date -Is; }
|
|
|
|
|
say() { printf '%s healthcheck: %s\n' "$(stamp)" "$*"; }
|
|
|
|
|
|
|
|
|
|
if [ -z "$BASE_URL" ]; then
|
|
|
|
|
say "FAIL no base URL. HEALTHCHECK_BASE_URL is set but empty; unset it for the default."
|
|
|
|
|
exit 78 # EX_CONFIG
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
body=$(mktemp)
|
|
|
|
|
trap 'rm -f "$body"' EXIT
|
|
|
|
|
|
|
|
|
|
# Assigned in the `if`, not with `|| echo "000"` appended. On a connection
|
|
|
|
|
# failure curl *already* prints "000" via --write-out and then exits non-zero,
|
|
|
|
|
# so appending a fallback produces "000000", which matches no branch below and
|
|
|
|
|
# reports "unexpected HTTP" for the one failure this script names explicitly.
|
|
|
|
|
#
|
|
|
|
|
# No -v and no --trace, ever. This request carries no credential, but the habit
|
|
|
|
|
# is the rule SECURITY.md states: a request URL or header must not reach a log.
|
|
|
|
|
if ! status=$(
|
|
|
|
|
curl --silent --show-error --output "$body" --write-out '%{http_code}' \
|
|
|
|
|
--max-time "$TIMEOUT" \
|
|
|
|
|
"$BASE_URL/api/health"
|
|
|
|
|
); then
|
|
|
|
|
status="000"
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
case "$status" in
|
|
|
|
|
200)
|
|
|
|
|
# The status got us here; the body decides. Both fields are asserted rather
|
|
|
|
|
# than assumed because a 200 from Cloudflare, a cached page or an error page
|
|
|
|
|
# is still a 200, and none of them are this application answering.
|
|
|
|
|
#
|
|
|
|
|
# Both, not either: the app can answer `"status":"ok"` while its database
|
|
|
|
|
# handle is gone, and that is the state where the site looks fine and every
|
|
|
|
|
# form submission is being lost.
|
|
|
|
|
if grep -q '"status":"ok"' "$body" && grep -q '"db":"ok"' "$body"; then
|
|
|
|
|
say "ok"
|
|
|
|
|
elif grep -q '"status":"ok"' "$body"; then
|
|
|
|
|
say "FAIL 200 and the app is up, but the database is not answering."
|
|
|
|
|
say " Every form submission is failing. See docs/OPERATIONS.md."
|
|
|
|
|
exit 70 # EX_SOFTWARE
|
|
|
|
|
else
|
|
|
|
|
say "FAIL 200 but not this application. The response carried neither"
|
|
|
|
|
say " \"status\":\"ok\" nor a recognisable health body — check whether"
|
|
|
|
|
say " something in front of the origin answered instead."
|
|
|
|
|
exit 70 # EX_SOFTWARE
|
|
|
|
|
fi
|
|
|
|
|
;;
|
|
|
|
|
503)
|
|
|
|
|
# Named rather than left to the catch-all, because it is the one unhealthy
|
|
|
|
|
# answer this route is designed to give: migrations failed, the container is
|
|
|
|
|
# marked unhealthy, and the public pages are still being served. That is a
|
|
|
|
|
# different thing from the site being down and reads differently at 3am.
|
|
|
|
|
say "FAIL the database is not reachable (HTTP 503). The site is serving"
|
|
|
|
|
say " static pages; every lead and support submission is being lost."
|
|
|
|
|
exit 70 # EX_SOFTWARE
|
|
|
|
|
;;
|
|
|
|
|
000)
|
|
|
|
|
say "FAIL could not reach $BASE_URL"
|
|
|
|
|
exit 69 # EX_UNAVAILABLE
|
|
|
|
|
;;
|
|
|
|
|
*)
|
|
|
|
|
# Includes 404. If this ever fires on a path this script wrote itself, the
|
|
|
|
|
# route moved — which is a thing to fix in one place rather than a thing for
|
|
|
|
|
# a caller to guess around.
|
|
|
|
|
say "FAIL unexpected HTTP $status from $BASE_URL/api/health"
|
|
|
|
|
exit 1
|
|
|
|
|
;;
|
|
|
|
|
esac
|