# Development log — Queue North Website ``` Status: Current Owner: _null Last reviewed: 2026-08-18 Governs: the dated record of what happened Review trigger: Nothing. This file is appended to, never revised. ``` ## How to use this Newest first. **One entry per work session**, written before you stop — that is step 6 of `docs/WORK_CYCLE.md`, and the two lines it insists on are `Next action` and `Blockers`. Those two are not decoration. The next session starts by reading the top of this file, and a session that ended without saying what came next hands the one after it a re-derivation instead of a starting point — which is where drift enters. Neither line competes with anything: the live next action is the field on the project at privacyllc.dev and the live blockers are issues in the tracker, while these say what both were **at this date**. A record of then never disagrees with a record of now. **Append-only by convention.** Correcting an old entry rewrites the record of what was known at the time, which is the one thing this file is for. If an entry turns out to be wrong, add a later entry saying so; do not edit the first. Note the Review trigger above says "nothing", deliberately. A dated log cannot rot the way a description of current state can — the entries were true when written and stay true. It is exempt from review for the same reason a receipt is. ## Where the entries below came from Everything from `v0.7.1` down predates this repository adopting the template, and arrived from two root-level files that were merged here on 2026-08-18 — the gitignored `HISTORY.md` (a version-by-version changelog) and the gitignored `DEVELOPMENT_LOG.md` (an agent-activity log). Both are reproduced **verbatim**. They are kept as two blocks rather than interleaved by date, deliberately. The changelog carries three duplicated version headings — two `v0.5.2`, two `v0.5.3` and two `v0.5.4`, written by different agents on the same day — so merging the two streams into one date order would have required choosing an order the record does not actually support. Two labelled blocks say what is known; one merged sequence would have implied more. ## Entries ### 2026-08-18 — Policy: production always runs a numbered version `_null`: *always stay on a num version.* Recorded here because it changed three things, and one of them was a script written an hour earlier. **What a pointer cannot do**, which is the whole argument. A stack file naming `:dev` says `dev` today and will still say `dev` after the image underneath it changes. The file records a preference, not a fact, so "what is running?" has no answer that survives the next push — and rollback is worse, because there is nothing to roll back *to*. A pointer only ever names now. **`deploy.sh`** now resolves the newest published **numbered** version from the registry when no `--tag` is given, and says which it picked. A floating tag is refused outright. `--allow-floating` is the escape hatch for the one case that is not a mistake — reproducing a fault on whatever a pointer currently is — and it warns every time. **`release.sh` stopped moving `:dev`.** It published both tags when it was written, because stack 58 followed the pointer. Under this policy nothing deploys `:dev`, so publishing it would only put something misleading in front of whoever reads the registry next. **`scripts/docker-push.sh` was deleted**, with its npm script. Its entire job was building and pushing an unnumbered `:dev` with no bump and no guards — the exact thing the policy exists to prevent. It had been kept a few hours earlier on the grounds that it was "what the muscle memory reaches for", which is a reason to remove it rather than a reason to keep it: a command that quietly violates a policy is how the policy stops being true. `:dev` and `:latest` stay in the registry, frozen rather than deleted. The running container was created from `:dev`, and removing the tag an existing deployment names is how a recreate fails to pull. **The policy is not yet true of production.** Stack 58 still pins `:dev`. The first deploy made under it repoints the stack at a version, and only then does the stack file answer the question on its own. That is also why the deploy default resolves a version rather than reusing whatever the stack already names — reusing it would have quietly perpetuated the thing being fixed. - **Closed:** none. Policy and tooling. - **Next action:** `npm run release` (0.9.3 → 0.9.4, publishing a numbered image that contains Batch 10's accessibility fixes), then `npm run deploy -- --tag v0.9.4`. That pair moves stack 58 off `:dev` and onto a number, ships the a11y work, and closes #212 — the CORS trailing slash is corrected in the same stack file the deploy rewrites. Both need sign-off: the deploy recreates the container and takes both front doors down together. - **Blockers:** the deploy needs permission. Six issues remain blocked on the site owner (#68, #110, #162, #213 in Batch 13; #69, #70 in Batch 14). ### 2026-08-18 — A release command, and the deploy half PrivacyLLC's deliberately omits Asked to model the release script on PrivacyLLC-Web's. Read theirs (30 KB, and worth the read), took the arguments, wrote the mechanism for this project. **Why it was needed, in one fact.** Publishing here was `npm run docker:push` with the version bump as a separate thing to remember. It was not remembered: `package.json` said `0.8.3` while the four most recent commits announced *batch 0.9.0* through *batch 0.9.3*, and **no image was ever published for any of them**. The registry's newest tag is still `v0.8.3`. Four batches of work went out under a version naming none of them — the same failure their header describes, arrived at independently. **Three things did not transfer, and each is a fact about this repository.** *It cannot gate on tests.* Theirs refuses to release on a half-run 1,600-test suite. There is no suite here, so `release.sh` gates on `verify.sh` and then 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. Saying so is the point; a gate that implies more than it did is worse than none. *It must move `:dev`.* Theirs pins an immutable version in the stack. Stack 58 follows `:dev`, so a release that only published `:vX.Y.Z` would be undeployable here. It publishes both — which makes `:dev` a pointer and never evidence, and `status.sh` reads the digest and version label instead. *The origin guard had to change shape.* Theirs validates one build arg. Here `https://queuenorth.com` is written out in three source files and baked into every canonical URL, `og:url`, `sitemap.xml` and `robots.txt`. So 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. **No prune, deliberately.** Theirs prunes because they release often. Thirteen tags exist here in total, deleting a published image is irreversible, and the one that matters is whichever the running container was created from — precisely what a newest-N rule gets wrong. Their own script spends forty lines guarding that case; not having the feature is a cheaper way to not have the bug. **The deploy half.** Theirs ends by saying it did not deploy and that the Portainer edit is manual — which is exactly the gap this project has been carrying. The template's `deploy.py` would fill it, but it builds and pushes too, so beside `release.sh` it would mean two commands that both build and two answers to "what is running". So `deploy.sh` does only the missing half. **Its most important guard is not the obvious one.** Portainer treats a stack `PUT` as the whole desired state. Sending one 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. That is a silent, total loss of the site's only purpose, caused by a successful-looking deploy. It reads them, counts them, sends them back, and refuses outright if the stack reports none. **Every guard was proven to refuse** rather than 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 surfaced by that 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 as a script. It would have failed on the first real deploy. - **Closed:** none. This is tooling, not a deliverable — #212 still needs the deploy itself. - **Next action:** `npm run deploy -- --dry-run`, then the deploy, which needs sign-off because it recreates the container and takes both front doors down together. That one act ships Batch 10's accessibility fixes and closes #212. Consider `npm run release` first so what gets deployed is a numbered version rather than a moved pointer — `:dev` currently points at an image built before any of today's work. - **Blockers:** the deploy needs permission. Six issues remain blocked on the site owner (#68, #110, #162, #213 in Batch 13; #69, #70 in Batch 14). ### 2026-08-18 — Batch 10 shipped, and half of it was not a defect Asked to fix whatever needed fixing. Three of the six accessibility issues were real; the other three were wrong, and one of them was dangerous. **Verified before fixing, which is the whole story of this session.** | # | Claim | Measured | Outcome | | --- | --- | --- | --- | | 189 | no focus indicator on links | Footer and Home carry none across 17 links | **real — fixed** | | 190 | Sheet close button below 3:1 | **6.25:1** | closed, no change | | 191 | footer text ~3.3:1, below AA | **5.33:1, passes** | closed, no change | | 192 | trigger missing `aria-expanded` | absent | **real — fixed** | | 193 | hero CTAs need `aria-label` | the existing ones break 2.5.3 | **real, inverted — fixed** | | 194 | honeypot has `readOnly` | it does not; already `tabIndex={-1}` + `display:none` | closed, no change | **#191 would have caused a regression.** Its proposed colour, `#475569` on `#0B2A3C`, measures **1.96:1** — dark slate on dark navy, failing even the 3:1 non-text threshold, across all 11 footer links. The footer is fine as it is. **#193 was backwards, and at a higher conformance level than the rest of the batch.** The `aria-label`s already on the hero CTAs were WCAG 2.5.3 Label in Name failures — Level A. `"Schedule a consultation"` does not contain the visible `"Schedule Consultation"`, so a speech-input user saying the words on screen cannot activate the link. Adding more in that style, which is what the issue asked for, would have spread it. Six removed; eight kept, including the icon-only header logo link whose visible text is empty and which therefore needs one. **#189's own fix direction was also wrong**, though harmlessly. It specified `ring-primary-cyan`; `#22D3EE` is 6.26:1 on navy and **2.38:1 on white**, so on a light-first design that ring fails 1.4.11 over most of the site. Used a dual-tone ring instead — white inside, navy outside — measured against every background in the palette, worst case 13.62:1. **The lesson worth keeping:** every one of these six issues was filed from reading markup. Four of the six either misstated a number or inverted the fix. Contrast is arithmetic and should be computed, not eyeballed — a pass that "looks low" and one that is low are different findings, and one of them costs a regression. Also: the image now carries `org.opencontainers.image.version`, so `status.sh --deployed-version` will answer once a new image is built. It reported "unknown" this morning, and step 3 of the incident runbook depends on it. Verified by building and inspecting the label, not by assuming. **#212 could not be finished.** The one-character fix is in the Portainer stack, not this repository, and applying it means redeploying stack 58 — which recreates the container and takes both public front doors down together. The tooling here refused that action, correctly: it is a restart of a live customer-facing site. Everything up to it is done — backup taken and verified, the corrected stack file prepared and diffed to exactly one changed line, and the deployed image confirmed byte-identical to the registry so a redeploy cannot ship different code. - **Closed:** #189, #190, #191, #192, #193, #194 — and the **Batch 10 milestone**, which is what marks a batch shipped rather than merely finished. #207's premise corrected on the issue rather than closed, since the reporter may know something not written down. - **Next action:** Batch 11 — UI defects on narrow viewports (#195–#198). Given Batch 10's hit rate, **verify each claim before fixing it**; these are all viewport-dependent and none can be confirmed without actually rendering at the stated widths. Separately, #212 needs one deploy of Portainer stack 58. - **Blockers:** #212 needs permission to restart production. Six issues remain blocked on the site owner (#68, #110, #162, #213 in Batch 13; #69, #70 in Batch 14). Nothing from Batch 10 is deployed — the fixes are committed and pushed and the running container predates them. ### 2026-08-18 — queuenorth.com confirmed as the permanent origin, and a wrong runbook claim corrected `_null` answered the one question left open by the adoption: **queuenorth.com is the permanent public origin**, with `qn.isnull.dev` kept as a second ingress. **The answer exposed a mistake made earlier the same day.** The first draft of `docs/OPERATIONS.md` said queuenorth.com "is not this deployment — it is the customer's own domain and is not served by this container. Do not diagnose against it." That was written from a DNS lookup and an assumption, and it was wrong in the worst direction a runbook can be wrong: it sent whoever read it away from the production host during an incident. What is actually true, established by request rather than inference: 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; `queuenorth.com` reaches the container through nginx-proxy-manager on thor/exodus and `qn.isnull.dev` reaches it through Cloudflare. **Two front doors, one container, and no non-production environment** — restarting it restarts the customer's live site whatever the `-dev` in its name suggests. The correction is written into `OPERATIONS.md` as a correction, with the wrong sentence quoted, rather than silently replaced. Same for the QA Round 0 table, which gained the production observations as extra rows instead of having its originals rewritten. **The deploy path is no longer a gap.** Adoption left it explicitly undone — "how nebula moves to a new image is not written down anywhere". It is a **Portainer stack, id 58 on nebula**, file at `/data/compose/58/docker-compose.yml`, found by reading the container's own compose labels. `OPERATIONS.md` now carries it, including the sentence that matters most: that stack file is a separate copy from this repository's `docker-compose.yml` and the two have already drifted. That drift is #212 and it is exactly one character — line 21 of the stack file has `CORS_ORIGIN=https://queuenorth.com/` with a trailing slash. Left in place deliberately: nothing is broken today (the forms are same-origin, and the slash makes the header match no origin, so it fails closed), and fixing it means recreating the container and taking both front doors down together. It belongs in the next deploy that happens for another reason. `scripts/deploy.py` was declined at adoption because none of this was known. It is built for exactly this shape and is worth taking next time. - **Closed:** none. #212 stays open — the repository half is done and the production half is one line in Portainer. - **Next action:** Unchanged — start `Batch 10 — Accessibility WCAG 2.1 AA` with #189, walking the pages keyboard-only at 320px before fixing anything. Fold the #212 one-character fix into whatever deploy happens next rather than restarting production for it. - **Blockers:** None on this work. Six issues remain blocked on the site owner (#68, #110, #162, #213 in Batch 13; #69, #70 in Batch 14), two of which are unsubstantiated claims live on the public site since May. ### 2026-08-18 — Adopted the project template Merged `~/.openclaw/Projects/Template` into this repository. It was the worked example in that template's own README — *"every one of the 205 in the repository that had never adopted the convention"* — and the numbers matched exactly: 205 issues, zero milestones, severity labels spelled `P0 Critical` / `P1 High` / `P2 Medium`, and privacyllc.dev reporting the project as **not adopted** rather than as 87% complete. **The repository was made private first.** Six documents were gitignored with the comment *"never commit"*, and committing them into a public tree was not the same decision as committing them at all. `null/fruit-fall` is already private and reports normally, so nothing was lost by it. That is what made `Exempt:` lines unnecessary — this project exempts nothing. **What moved.** `FUTURE.md` was the markdown backlog and is gone; every open item in it was already an issue, so nothing needed migrating into the tracker and only its reasoning survives, in `BATCH_LEDGER.md`. `HISTORY.md` and the old `DEVELOPMENT_LOG.md` are the two blocks below this one, verbatim, zero lines lost. `PROJECT.md`, `STRUCTURE.md`, `BUILD_SUMMARY.md` and `SCRIPTS.md` were absorbed and deleted. Four tracked documents moved into `docs/` as git renames. **What was kept because this project earned it:** the five-agent pipeline, the design system in `OVERHAUL_PLAN.md`, the positioning argument in `REDESIGN_REVIEW.md`, and `REQUIREMENTS.md` whole including its change policy. **What was deleted from the template because it does not apply here:** QA pass I (no money moves), the authorisation checklist group and the session-token row (no accounts, no sessions anywhere in the product), and one *(precautionary)* paragraph in `SECURITY.md` about holding credentials on behalf of users. That last one is a precautionary rule dropped deliberately and it is named in `DOC_TRUST_MAP.md` so nobody has to guess whether it was an oversight. **Ten scripts, configured rather than copied.** `backup.sh`'s ENGINE block was rewritten for SQLite and `restore-check.sh` was rewritten outright — the template's is `pg_restore` and `psql` end to end with no seam to configure. Three guards in `scripts/verify.d/`, because `verify.sh` detects nothing in a project with no test runner and would otherwise exit 2. **Every guard was proven to fail before being trusted**, per `GUARDS.md` §1. The one worth naming: `secrets.sh` was pointed at the real historical leak replayed out of commit `033bdf6`, and caught all three Zoho tokens. **What it proved.** Two things that were not known this morning: 1. **The lead database is restorable.** `backup.sh` took a verified snapshot from the running container and `restore-check.sh` replayed it from SQL into a scratch database — 2 tables, 3 rows, under a second. Before today nothing had ever backed it up and nothing had ever restored it. 2. **Three defects, found by the tooling on the way in.** `zoho-setup.md` told admins to edit `server/zoho/`, a directory that has never existed in any commit; `README.md`'s route list still advertised `/8x8`, removed at `0.6.6`, and omitted `/privacy-policy`; and `check-env.sh` found `CORS_ORIGIN` naming a host the site is not served from, with a trailing slash, drifted from what `docker-compose.yml` commits. The first two were fixed in the same commit. The third is filed, because choosing the right origin is not mine to decide. **Four issues filed and no more.** Every open issue is a denominator. A test suite was deliberately *not* filed for — nobody has decided to build one, and inventing that work would make every percentage wrong in one direction. It is recorded as a standing gap in `ClaudeQACoverage.md` instead. - **Closed:** none. Nothing was finished today that was open before it; the work was structural. The 178 already-closed issues were assigned to `Batch 00 — Pre-convention history`, which was then closed as the batch that shipped. - **Next action:** Start `Batch 10 — Accessibility WCAG 2.1 AA`, with #189 (missing focus rings on all link elements) first — it is the widest of the six and the only one that affects every page. All six were found by reading markup rather than by testing, so begin by walking the home, services and contact pages keyboard-only at 320px and confirm the six are the whole list before fixing any of them. - **Blockers:** None blocking this work. Five issues are blocked on the site owner and cannot move without them — #68, #110, #162 and #213 in `Batch 13`, and #69 and #70 in `Batch 14`. Two of those, #108 and #110, are live claims on the site the business may not be able to substantiate, which is a credibility exposure rather than an engineering one and has been open since May. --- ## From `HISTORY.md` — the version changelog, verbatim > Migrated 2026-08-18. Newest first, as written. Not edited. ## v0.7.1 — Low-Priority Fixes (6 issues, batches 10.0–10.2) — 2026-05-18 ### Batch 10.0: Backend Cleanup (Neo) - **#128:** Removed React Query dependency (~45KB bundle savings) — replaced with simple async fetch - **#127:** Added HTTP→HTTPS redirect middleware for production (x-forwarded-proto) - **#129:** Added CSP connectSrc comment documenting Zoho server-to-server calls ### Batch 10.1: Image Optimization & UX (Scarlett) - **#97:** Converted 5 large PNGs to WebP (95-96% size reduction each, total ~11MB → ~511KB) - **#130:** Added loading spinner animation to Contact and Support form submit buttons ### Batch 10.2: Accessibility (Scarlett + Ripley) - **#101:** Added aria-labels to interactive elements across Header, Footer, MobileNav, Home, Contact, Support - Fixed JSX template literal syntax bug where aria-label and className both used `${}` on same element - Fixed Footer.jsx template literal aria-labels that would render as literal text - Fixed Home.jsx duplicated aria-label text fragment ## v0.7.0 — Audit Bug Fixes (44 issues, batches 9.0–9.9) — 2026-05-18 ### Batch 9.0: Backend Security (Neo) - **#120:** Added UNIQUE constraint on leads.email + migration script - **#121:** Fixed Docker healthcheck — process.exit(1) on DB failure instead of always returning 200 - **#123:** Removed world-writable chmodSync on SQLite DB file ### Batch 9.1: UI Fixes (Scarlett) - **#104:** Header CTA visibility — changed from invisible navy-on-navy to cyan-on-navy - **#89:** Added ScrollToTop component for route navigation - **#88:** Created 404 NotFound page with catch-all route ### Batch 9.2: UI Fixes (Scarlett) - **#91:** 8x8 white logo visibility on Trust Bar — added `brightness-0 invert` CSS filter - **#94:** Industries page now uses dynamic icons instead of hardcoded building SVG - **#125:** Added Building2 fallback for missing industry icons - **#92:** Industry cards use homeDesc with fallback text - **#93:** Removed redundant shortDesc from Home page service cards ### Batch 9.3: Navigation Fixes (Scarlett) - **#109:** Desktop nav now has hover dropdown menus for Services and Industries - **#102:** Replaced all `