docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
# Architecture — Queue North Website
```
Status: Current
Owner: _null
Last reviewed: 2026-08-18
Governs: docs/architecture/**, server/index.js, scripts/** — the module
boundaries, the database schema and the API response shapes
Review trigger: Any new module, any change to a module boundary or a data shape;
any new table or column; any new external service the server
calls; any change to how routes are prerendered
```
## The shape of it
One Express process serves everything. There is no separate web server, no
reverse proxy inside the container, and no second runtime.
```text
browser
|
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
+── queuenorth.com ──► nginx-proxy-manager (thor/exodus) ┐ PRODUCTION
| │
+── qn.isnull.dev ───► Cloudflare ─────────────────────────┤
v
Express (server/index.js, port 3001) on nebula ← the only process, one instance
docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
|
+--> dist/ prerendered HTML + the React bundle, served static
+--> /api/health liveness, and a real SELECT against SQLite
+--> /api/leads POST → validate → SQLite → (fire and forget) Zoho
+--> /api/support POST → validate → SQLite → (fire and forget) Zoho Cases
|
+--> db/queuenorth.db better-sqlite3, synchronous, single writer
|
+--> Google reCAPTCHA v3 verify, server-side, before any insert
+--> Zoho CRM WebToLead form post, or REST/OAuth as a standby
```
## Module boundaries
**`src/` knows nothing about the database.** It talks to three JSON endpoints
through `src/lib/api.js` and nothing else. There is no ORM in the client, no
shared schema module, and no import that crosses from `src/` into `server/` .
**`server/index.js` knows nothing about React.** It serves `dist/` as static
files and falls through to `dist/index.html` for client routes. The one place
this is not quite true is the privacy policy, below.
**`src/data/*.js` is the content layer.** Services, industries and the privacy
policy text live there as plain data, imported by both the client pages and
`src/entry-server.jsx` . Adding a service is a data edit, not a component edit.
**`scripts/prerender.js` is a build step, not a runtime.** It renders every route
to static HTML at build time using `src/entry-server.jsx` . Nothing at request
time renders React on the server.
### The three boundaries worth knowing about
1. **The privacy policy has two renderers, one source.** `src/data/privacyPolicy.js`
is the single source of truth; `src/pages/PrivacyPolicy.jsx` renders it in the
SPA and the prerender step emits a static copy. This exists because Meta's
crawler does not execute JavaScript, and a policy it cannot read is a policy
that does not count. **Do not add policy text to a component.**
2. **Zoho is an overlay, never a dependency.** Every handler writes SQLite first
and then calls the forwarder without awaiting it. A Zoho outage, a bad token
or a network timeout costs a CRM record and never a lead. `forwardLeadToZoho`
dispatches to `forwardToZohoWebToLead` or `forwardToZoho` on
`ZOHO_FORWARDING_MODE` ; both carry a 10 s `AbortController` .
3. **reCAPTCHA is the one thing that runs *before* the insert.** It is the only
check that can reject a submission outright, so it is deliberately the only
external call on the blocking path — with a 5 s timeout, and it fails open when
`RECAPTCHA_ENABLED` is false.
## Data shapes
`server/index.js` owns the schema and applies it at startup via `initSchema()` .
There is no external migration runner; the one migration that exists rebuilds
`leads` to add the `UNIQUE` constraint and is idempotent.
### `leads`
| Column | Type | Notes |
| --- | --- | --- |
| `id` | INTEGER PK AUTOINCREMENT | |
| `company` | TEXT NOT NULL | max 200 after sanitisation |
| `name` | TEXT NOT NULL | max 100. Split on the last space for Zoho's `First_Name` / `Last_Name` |
| `email` | TEXT NOT NULL **UNIQUE** | max 254 (RFC 5321). A duplicate answers **409** , and the Zoho forward is still attempted — the local row existing does not mean the CRM record does |
| `phone` | TEXT | |
| `zip` | TEXT | maps to Zoho `Zip_Code` |
| `message` | TEXT | |
| `service_interest` | TEXT | normalised from empty to NULL. Maps to Zoho `Description` , not a custom field |
| `created_at` | DATETIME | `CURRENT_TIMESTAMP` |
### `support_requests`
| Column | Type | Notes |
| --- | --- | --- |
| `id` | INTEGER PK AUTOINCREMENT | |
| `name` , `company` , `email` | TEXT NOT NULL | **no** UNIQUE — the same customer may raise many tickets |
| `phone` | TEXT | |
| `issue` | TEXT NOT NULL | minimum 10 characters, enforced client and server side |
| `priority` | TEXT | defaults to `medium` |
| `created_at` | DATETIME | `CURRENT_TIMESTAMP` |
**The asymmetry between the two tables is deliberate.** A lead is a person you
want once; a support request is an event that recurs. Adding `UNIQUE` to
`support_requests.email` would silently drop a customer's second ticket.
### Every response shape the API produces
| Status | Body | When |
| --- | --- | --- |
| 200 | the resource, or `{status, db, timestamp}` | success |
| 400 | `{error: 'Validation failed', fields: {…}}` | Zod rejected it |
| 403 | `{error}` | reCAPTCHA below `RECAPTCHA_MIN_SCORE` |
| 404 | `{error: 'Not found'}` | unmatched `/api/*` only; other paths fall through to the SPA |
| 409 | `{error}` | duplicate `leads.email` |
| 413 | — | body over 1 MB |
| 429 | `{error, message, retryAfter}` | rate limiter |
| 500 | `{error}` | never a stack trace |
| 503 | `{error, db: 'error'}` | health check could not reach SQLite |
| 504 | `{error: 'Request timeout'}` | the 30 s request timeout fired |
## Documents here
- **`GUARDS.md`** — how to write a check that actually checks. Read it before
adding a structural test or a probe; every rule in it was learned from a guard
that had been green over something broken.
- **`zoho-setup.md`** — the CRM integration end to end: app setup, credentials,
environment variables, and how to confirm a lead arrived.
## What ships in `scripts/`
Ten scripts came from the template on 2026-08-18 and three were already here.
The template's full catalogue is a **menu** , not an inventory — see
[`../TOOLS.md` ](../TOOLS.md ). This table is what this project actually has, and
each row says what it does *here* .
| Path | What it is |
| --- | --- |
| `scripts/check-env.sh` | which of the 17 Zoho / reCAPTCHA / CORS / rate-limit variables are set and plausible, before the server reads them. Exit 2 means nothing was checked |
fix(security): stop secrets.sh flagging every prerendered page, and clear the dangling doc claims
secrets.sh --built reported ten credentials in dist/ and all ten were the same
false positive: the template's user:pass@host pattern reads the schema.org
JSON-LD on every prerendered page — //queuenorth.com"},"areaServed":{"@ — as a
host, a password and an @. One more finding for every page added, which is the
noise that turns a scanner into something people mute.
Quotes, braces, commas and angle brackets cannot occur in a real userinfo
component. Checked against a database URL with an inline password, one
percent-encoded, and a git remote carrying a token — all three still caught, all
ten false positives gone, and the historical Zoho leak from 033bdf6 still caught
when replayed.
The first version of that fix wrote its three test cases out literally in the
header, and --tracked then reported two credentials in the scanner itself. The
placeholders now use angle brackets, which are in the exclusion class the
comment is describing — so the examples cannot match the pattern they
illustrate. Same shape as the trap DOC_TRUST_MAP.md records about Exempt: lines.
doc-claims: 240 claimed paths, all present, up from 5 dangling. DOC_TRUST_MAP
was claiming banner.webp exists while saying it does not; GUARDS.md pointed at
prove-guard.sh, which this project declined. docs/history/ is excluded rather
than corrected — its entries name files that existed when they were written, and
editing an append-only log to satisfy a present-tense check is a category error.
TOOLS.md records the exclusion and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:40:46 -05:00
| `scripts/secrets.sh` | credential shapes in a staged diff, and `--tracked` for a whole-tree audit. ** `--built dist/` is the one that matters here**: `VITE_RECAPTCHA_SITE_KEY` is inlined into the bundle at build time, so the repository scan cannot see what users receive. Carries this project's own shapes, and one **tightened** pattern — see below |
docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
| `scripts/verify.sh` | every check this project has, in one table. Honestly thin — there is no test suite, and it says so rather than printing a green row |
| `scripts/doc-triggers.py` | which documents a pending change fires, read from the `Governs:` headers. Run it before committing, not after |
| `scripts/forgejo-issue.py` | files and closes issues in the tracker convention, refusing malformed ones before they are filed |
| `scripts/status.sh` | what is running on **nebula** as `qn-website-dev` , its version and its restart count. Read-only |
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
| `scripts/healthcheck.sh` | a liveness tick against `queuenorth.com` , asserting HTTP 200 **and** `"status":"ok"` **and** `"db":"ok"` — a 503 with a JSON body is a real answer, not an outage. `HEALTHCHECK_BASE_URL` points it at the other front door |
docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
| `scripts/preflight.sh` | headers and TLS against the live origin. No `--auth` checks: there are no accounts |
| `scripts/backup.sh` | a verified SQLite dump. Its ENGINE block was rewritten for better-sqlite3's online `.backup()` — see below |
| `scripts/restore-check.sh` | restores the newest dump into a scratch file, runs `PRAGMA integrity_check` , counts tables, and **times it** . A backup nobody has restored is a guess |
feat(deploy): production always runs a numbered version
Policy set by _null. The stack pins :vX.Y.Z — never :dev, never :latest.
The reason is what a pointer cannot do. A stack file naming :dev says "dev"
today and will still say "dev" after the image underneath it changes, so the
file records a preference rather than a fact and "what is running?" has no
answer that survives the next push. Rollback is worse: there is nothing to roll
back TO, because a pointer only ever names now.
deploy.sh: with no --tag it now resolves the newest published NUMBERED version
from the registry and says which it picked, instead of defaulting to :dev. A
floating tag is refused outright, with --allow-floating as the loud escape hatch
for the one case that is not a mistake — reproducing a fault on whatever a
pointer currently is.
release.sh: no longer moves :dev. It published both tags until now because the
stack followed the pointer; under this policy nothing deploys :dev, so moving it
would only publish something that misrepresents what is running.
scripts/docker-push.sh deleted, with its npm script. Its entire job was building
and pushing an unnumbered :dev with no version bump and no guards, which is now
the exact thing the policy exists to prevent. Keeping a command that quietly
violates a policy is how the policy stops being true.
:dev and :latest stay in the registry, frozen. Not deleted: the running
container was created from :dev, and removing the tag an existing deployment
names is how a recreate fails to pull.
Stack 58 still pins :dev as this lands. Correcting that is the first deploy made
under the policy, and it is why the default resolves a version rather than
reusing whatever the stack already names.
Guards proven: default resolves v0.8.3; --tag dev refused; --tag latest refused;
--allow-floating warns and proceeds; unpublished tag still refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:54:42 -05:00
| `scripts/release.sh` | **publishes.** Bump, guard, build, verify the image's own version label, push `:vX.Y.Z` , commit last, tag. Refuses to overwrite a published tag, to build on a dirty tree, or to build when the three hard-coded copies of the public origin disagree. Adapted from PrivacyLLC-Web's — see below |
| `scripts/deploy.sh` | **deploys, and does not build.** Points Portainer stack 58 at an already-published **numbered** version — it refuses a floating tag — taking a verified backup first and preserving the stack's twelve environment variables, then waits for health and checks both public origins. Reports the digest before and after |
docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
| `scripts/docker-test.sh` | builds the image and runs it locally on 3001. Predates the template |
fix(ui): header CTA clipped at iPad portrait, and reCAPTCHA cut off at 320px
Two real defects, found by rendering the site rather than reading it. Neither
was among the twenty issues filed for Batches 10 and 11.
#214 (P1) — at exactly 768px, iPad portrait, the header's "Request
Consultation" CTA measured x 676-778 against a 768px viewport: 10px sliced off,
with no scrollbar to reveal it because html/body carry overflow-x:hidden. At
that width the burger menu is already hidden, so the primary conversion action
was simply unreachable. The nav's five gap-6 gaps were the slack; gap-4 at md
(gap-6 from lg) frees 40px, keeping the CTA on screen at md rather than
deferring it to lg and leaving 768-1023px with no CTA at all.
#215 (P2) — Google's reCAPTCHA checkbox iframe is a fixed 304px that cannot be
resized. At 320px it measured x 41-345, so 25px of branding and the privacy and
terms links were clipped. Scaled to 0.85 below 360px with the wrapper height
reduced to match, since transform does not affect layout and the form would
otherwise gain dead space.
Also adds scripts/qa-browser.mjs, which found them. It measures horizontal
scroll, broken images, elements past the right edge, CLS and LCP across pages
and viewports. Written because seven of the ten issues in Batches 10 and 11
misstated their own evidence, and two of those would have made the site worse
if actioned.
Two false positives in the tool itself, both fixed before trusting it:
- Lazy images below the fold read as broken. It now scrolls the page AND chases
each un-loaded image individually, and reports observed HTTP failures apart
from never-rendered images.
- `--paths / --viewports 320` swallowed "320" as a path. Argument collection now
stops at the next flag.
Verified against a local production build: 5 paths x 5 viewports, zero findings.
Before the fix the same run reported the CTA overflow at 768 on four pages and
the reCAPTCHA overflow at 320 on /contact.
Not yet live — this needs a release and a deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 04:05:37 -05:00
| `scripts/qa-browser.mjs` | renders the site in real Chromium at real viewports and **measures** it — horizontal scroll, broken images, elements past the right edge, CLS and LCP. Exit 2 if playwright is missing, because "could not check" is not a pass. Not in `verify.sh` : playwright is global here, not a project dependency |
docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
| `scripts/prerender.js` | the build step that emits static HTML for every route. Predates the template |
feat(release): adapt PrivacyLLC-Web's release.sh, and add the deploy half it deliberately omits
scripts/release.sh — publish. Bump, guards, build, verify the image's own
version label, push :vX.Y.Z and move :dev, commit last, tag.
Adapted, not copied. The arguments are PrivacyLLC's and were paid for there; the
mechanism is this project's, because almost none of it transferred. Three
differences, each a fact about this repository rather than a preference:
- It gates on verify.sh, not a test suite, because there is not one. The
original refuses to release on a half-run 1,600-test run. This one says out
loud that a build, a secret scan and a doc-header check are not tests and that
nothing in the gate touched a route, a form or an API response.
- It moves :dev as well as publishing :vX.Y.Z, because Portainer stack 58
follows :dev. That makes :dev a pointer and never evidence.
- It checks the public origin in three files, not one. The original passes its
origin in as a build arg; here https://queuenorth.com is written out in
src/lib/seo.js, src/components/SEO.jsx and scripts/prerender.js and is baked
into every canonical URL, og:url, sitemap.xml and robots.txt. The guard asks
whether the three still agree.
Why this was needed: publishing was `npm run docker:push` with the bump as a
separate thing to remember, and it was not remembered. package.json said 0.8.3
while four commits announced "batch 0.9.0" through "0.9.3", and NO image was
ever published for any of them — the registry's newest tag is v0.8.3.
No prune. The original has one; this project has published thirteen tags in its
life, deleting a published image is irreversible, and the one that matters is
whichever the container was created from — exactly what a newest-N rule gets
wrong.
scripts/deploy.sh — deploy, and do not build. The template's deploy.py builds,
pushes AND deploys; adopting it beside release.sh would mean two commands that
both build, a second image for the same code, and two answers to "what is
running". This does only the missing half: point stack 58 at an already
published tag, having taken a verified backup first.
Its most important guard is not the obvious one. Portainer treats a stack PUT as
the whole desired state, so sending it without the stack's Env array would strip
twelve variables — the reCAPTCHA secret and the Zoho form tokens among them —
and the container would come back HEALTHY while quietly capturing no leads. It
reads them, counts them, sends them back, and refuses outright if none.
Guards proven to refuse, not assumed: already-published tag, http origin,
trailing-slash origin, the three origin constants drifted, dirty tree, bad flag,
unpublished deploy tag, missing Portainer key, wrong stack id.
One real bug found and fixed while testing: the image-line rewrite used
`python3 -` with a heredoc while also piping the stack file to stdin, so python
tried to execute the YAML.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:49:03 -05:00
**Why `release.sh` and `deploy.sh` are two scripts.** Publishing an image and
running it are separate decisions, which is the rule both the template and
PrivacyLLC-Web's release script state and the reason neither of them deploys.
The template's `deploy.py` does build, push *and* deploy; adopting it beside
`release.sh` would have produced two commands that both build, a second image
for the same code, and two answers to "what is running". So `deploy.sh` does
only the half that was missing.
Three things about `release.sh` differ from the script it was adapted from, and
each is a fact about this project rather than a preference:
- **It gates on `verify.sh` , not on a test suite,** because there is not one.
The original refuses to release on a half-run 1,600-test run; this one says out
loud that a build, a secret scan and a doc-header check are not tests and that
nothing in the gate exercised a route, a form or an API response.
feat(deploy): production always runs a numbered version
Policy set by _null. The stack pins :vX.Y.Z — never :dev, never :latest.
The reason is what a pointer cannot do. A stack file naming :dev says "dev"
today and will still say "dev" after the image underneath it changes, so the
file records a preference rather than a fact and "what is running?" has no
answer that survives the next push. Rollback is worse: there is nothing to roll
back TO, because a pointer only ever names now.
deploy.sh: with no --tag it now resolves the newest published NUMBERED version
from the registry and says which it picked, instead of defaulting to :dev. A
floating tag is refused outright, with --allow-floating as the loud escape hatch
for the one case that is not a mistake — reproducing a fault on whatever a
pointer currently is.
release.sh: no longer moves :dev. It published both tags until now because the
stack followed the pointer; under this policy nothing deploys :dev, so moving it
would only publish something that misrepresents what is running.
scripts/docker-push.sh deleted, with its npm script. Its entire job was building
and pushing an unnumbered :dev with no version bump and no guards, which is now
the exact thing the policy exists to prevent. Keeping a command that quietly
violates a policy is how the policy stops being true.
:dev and :latest stay in the registry, frozen. Not deleted: the running
container was created from :dev, and removing the tag an existing deployment
names is how a recreate fails to pull.
Stack 58 still pins :dev as this lands. Correcting that is the first deploy made
under the policy, and it is why the default resolves a version rather than
reusing whatever the stack already names.
Guards proven: default resolves v0.8.3; --tag dev refused; --tag latest refused;
--allow-floating warns and proceeds; unpublished tag still refused.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:54:42 -05:00
- **It publishes one tag and does not move `:dev` .** Both scripts follow the
policy below: production always runs a numbered version, so nothing deploys a
pointer and publishing one would only misrepresent what is running.
feat(release): adapt PrivacyLLC-Web's release.sh, and add the deploy half it deliberately omits
scripts/release.sh — publish. Bump, guards, build, verify the image's own
version label, push :vX.Y.Z and move :dev, commit last, tag.
Adapted, not copied. The arguments are PrivacyLLC's and were paid for there; the
mechanism is this project's, because almost none of it transferred. Three
differences, each a fact about this repository rather than a preference:
- It gates on verify.sh, not a test suite, because there is not one. The
original refuses to release on a half-run 1,600-test run. This one says out
loud that a build, a secret scan and a doc-header check are not tests and that
nothing in the gate touched a route, a form or an API response.
- It moves :dev as well as publishing :vX.Y.Z, because Portainer stack 58
follows :dev. That makes :dev a pointer and never evidence.
- It checks the public origin in three files, not one. The original passes its
origin in as a build arg; here https://queuenorth.com is written out in
src/lib/seo.js, src/components/SEO.jsx and scripts/prerender.js and is baked
into every canonical URL, og:url, sitemap.xml and robots.txt. The guard asks
whether the three still agree.
Why this was needed: publishing was `npm run docker:push` with the bump as a
separate thing to remember, and it was not remembered. package.json said 0.8.3
while four commits announced "batch 0.9.0" through "0.9.3", and NO image was
ever published for any of them — the registry's newest tag is v0.8.3.
No prune. The original has one; this project has published thirteen tags in its
life, deleting a published image is irreversible, and the one that matters is
whichever the container was created from — exactly what a newest-N rule gets
wrong.
scripts/deploy.sh — deploy, and do not build. The template's deploy.py builds,
pushes AND deploys; adopting it beside release.sh would mean two commands that
both build, a second image for the same code, and two answers to "what is
running". This does only the missing half: point stack 58 at an already
published tag, having taken a verified backup first.
Its most important guard is not the obvious one. Portainer treats a stack PUT as
the whole desired state, so sending it without the stack's Env array would strip
twelve variables — the reCAPTCHA secret and the Zoho form tokens among them —
and the container would come back HEALTHY while quietly capturing no leads. It
reads them, counts them, sends them back, and refuses outright if none.
Guards proven to refuse, not assumed: already-published tag, http origin,
trailing-slash origin, the three origin constants drifted, dirty tree, bad flag,
unpublished deploy tag, missing Portainer key, wrong stack id.
One real bug found and fixed while testing: the image-line rewrite used
`python3 -` with a heredoc while also piping the stack file to stdin, so python
tried to execute the YAML.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:49:03 -05:00
- **It checks the public origin in three files rather than one.** The original
passes its origin in as a build arg, so it has one copy to validate. Here
`https://queuenorth.com` is written out in `src/lib/seo.js` ,
`src/components/SEO.jsx` and `scripts/prerender.js` , and it is baked into every
canonical URL, `og:url` , `sitemap.xml` and `robots.txt` . The guard asks whether
the three still agree, because a wrong origin cannot be corrected without
another build and is invisible until somebody reads the page source.
**Neither script prunes the registry.** The original does, because it releases
often enough for that to matter. This project has published thirteen tags in its
life, deleting a published image is irreversible, and the one that matters is
whichever the running container was created from — exactly what a newest-N rule
gets wrong.
fix(security): stop secrets.sh flagging every prerendered page, and clear the dangling doc claims
secrets.sh --built reported ten credentials in dist/ and all ten were the same
false positive: the template's user:pass@host pattern reads the schema.org
JSON-LD on every prerendered page — //queuenorth.com"},"areaServed":{"@ — as a
host, a password and an @. One more finding for every page added, which is the
noise that turns a scanner into something people mute.
Quotes, braces, commas and angle brackets cannot occur in a real userinfo
component. Checked against a database URL with an inline password, one
percent-encoded, and a git remote carrying a token — all three still caught, all
ten false positives gone, and the historical Zoho leak from 033bdf6 still caught
when replayed.
The first version of that fix wrote its three test cases out literally in the
header, and --tracked then reported two credentials in the scanner itself. The
placeholders now use angle brackets, which are in the exclusion class the
comment is describing — so the examples cannot match the pattern they
illustrate. Same shape as the trap DOC_TRUST_MAP.md records about Exempt: lines.
doc-claims: 240 claimed paths, all present, up from 5 dangling. DOC_TRUST_MAP
was claiming banner.webp exists while saying it does not; GUARDS.md pointed at
prove-guard.sh, which this project declined. docs/history/ is excluded rather
than corrected — its entries name files that existed when they were written, and
editing an append-only log to satisfy a present-tense check is a category error.
TOOLS.md records the exclusion and why.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:40:46 -05:00
**Why `secrets.sh` has a tightened pattern.** Its `user:pass@host in a URL`
rule excludes only `/` , `@` , `:` and whitespace in the template. That is right
for source and wrong for this project's build output: every prerendered page
carries schema.org JSON-LD, and `//queuenorth.com"},"areaServed":{"@` parses as
a host, a password and an `@` . Ten findings per `--built` run, one more for
every page added — the noise that turns a scanner into something people mute.
Quotes, braces, commas and angle brackets cannot occur in real userinfo, so
excluding them costs nothing and was checked against three real credential URLs
before being applied. The reasoning is in the script's own header.
docs: adopt project template, retire the markdown backlog
This repository is the one the template's README describes: 205 issues, zero
milestones, and severity labels spelled "P0 Critical" / "P1 High" / "P2 Medium",
which privacyllc.dev reports as NOT ADOPTED rather than as 87% complete.
Six markdown records described the same work and none pointed at the tracker.
Two of them said the project was in "Phase 5" while the code was at 0.9.3.
Migrated, then deleted in this commit:
FUTURE.md -> docs/history/BATCH_LEDGER.md (Archived). Its open
items were all already filed as issues, so nothing
needed migrating into the tracker
HISTORY.md -> docs/history/DEVELOPMENT_LOG.md, verbatim, 0 lines lost
DEVELOPMENT_LOG.md -> the same file, as a second labelled block. Not
interleaved: the changelog has three duplicated version
headings, so one date order would have implied more
than the record supports
PROJECT.md -> docs/planning/PROJECT_PLAN.md
STRUCTURE.md -> the agent pipeline into README.md; its versioning rules
retired
BUILD_SUMMARY.md -> BATCH_LEDGER.md. Its embedded SQL schema deliberately
NOT carried: it predated the UNIQUE constraint on
leads.email, and server/index.js owns the schema
SCRIPTS.md -> docs/TOOLS.md, corrected for the SSR + prerender build
Moved with history (git detects all four as renames):
OVERHAUL_PLAN.md, review.md, project-requirements.md, docs/zoho-setup.md
Kept because this project earned them: the five-agent pipeline, the design
system in OVERHAUL_PLAN.md (Status: Current, with a front-note saying which half
is history), the positioning argument in REDESIGN_REVIEW.md, and REQUIREMENTS.md
whole, including its change policy.
Deleted from the template because they do not apply, each said out loud in
DOC_TRUST_MAP.md: QA pass I (no money moves), the authorisation checklist group
and the session-token row (no accounts, no sessions), and one PRECAUTIONARY
paragraph in SECURITY.md about holding credentials on behalf of users — there
are none, and PROJECT_PLAN.md records accounts as out of scope. Pass H was kept
and rewritten: its authorisation half does not apply, its what-a-stranger-can-
reach half is the most exposed surface here.
Also removed: main.js, the old static site's hash router, referenced by nothing
and preserved in .drop/; and test-results/.last-run.json, a May Playwright
artifact reading {"status":"failed"} for a suite that does not exist.
The repository was made private on Forgejo before this commit. That is what let
the internal history be committed rather than exempted — null/fruit-fall is
already private and reports normally.
Two defects found on the way in and fixed here: zoho-setup.md told admins to
edit `server/zoho/`, a directory that has never existed in any commit (the
mapping is in server/index.js), and README.md's route list still advertised
/8x8, removed at 0.6.6, while omitting /privacy-policy.
Branding: icon.webp and logo.webp converted from this project's own marks in
assets/. banner.webp is absent and is filed as an issue rather than faked.
Verified: verify.sh 3/3, doc-claims 71 claimed paths all present, backup and a
first-ever restore of the live leads database (2 tables, 3 rows, under 1s).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:19:02 -05:00
**Why `backup.sh` and `restore-check.sh` are not the template's originals.** Both
ship as PostgreSQL tools. `backup.sh` is built to be adapted — everything
engine-specific is in one ENGINE block — so that block now calls
`better-sqlite3` 's `.backup()` inside the running container and verifies the
result with `sqlite3` before renaming it into place. `restore-check.sh` had no
such seam: it is `pg_restore` and `psql` end to end, so the SQLite version is a
rewrite that keeps the argument and replaces the mechanism.
## What does not belong here
- Product intent — that is `docs/planning/PROJECT_PLAN.md`
- Engineering standards and the stack policy — that is `docs/planning/REQUIREMENTS.md`
- What it should look and sound like — that is `docs/design/`
- What happened while building it — that is `docs/history/`
## A note on drift
Architecture docs go stale faster than any other kind, because code changes under
them silently. This is exactly what the **Review trigger** line is for: name the
change that should send somebody back here, and a reader can tell whether the
trigger has fired.
The specific instance to avoid in this repository: `BUILD_SUMMARY.md` carried a
copy of the SQL schema that predated the `UNIQUE` constraint on `leads.email` .
It was not carried forward on adoption. ** `server/index.js` owns the schema; the
tables above describe it and do not duplicate it.**