Commit Graph

166 Commits

Author SHA1 Message Date
Neo 1a3ba305a7 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
Ripley 6a890ad8bc docs: log the release and deploy scripts, and what did not transfer from PrivacyLLC's
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:49:42 -05:00
Neo 2ae7352799 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
Ripley e3c360008e docs: log Batch 10, and record that four of its six issues misstated their evidence
Pass F moves from "not run" to "partial": a static audit did happen — contrast
computed, accessible names checked against visible text — but nothing has been
walked in a browser and no screen reader has been used. A focus ring proven at
13.62:1 in arithmetic has still never been seen.

Adds the standing gap that matters more than the individual fixes: four of six
filed accessibility defects had wrong numbers or an inverted fix direction, all
six having been written from reading markup. A filed defect's figures are a
claim to check.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:35:45 -05:00
Neo 77d5131473 chore(docker): label the image with its version so status.sh can report it
scripts/status.sh --deployed-version reads org.opencontainers.image.version and
the image carried no such label, so on 2026-08-18 it correctly answered
"unknown" and the digest was the only way to tell one deploy from another. Step
3 of the incident runbook depends on that command.

ARG APP_VERSION defaults to 0.9.3 and can be overridden at build time.

Verified by building: docker build succeeds and
`docker image inspect --format '{{json .Config.Labels}}'` reports
org.opencontainers.image.version = 0.9.3, plus title and source.

Takes effect on the next image build. OPERATIONS.md says so rather than
implying the running container has it — it does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:34:27 -05:00
Scarlett 9fdc6b6032 fix(a11y): visible focus indicator, aria-expanded, and six Label-in-Name failures
Batch 10, and only three of its six issues were real. The other three are
answered on the tracker with the arithmetic.

#189 — focus indicator. Footer and Home carried zero focus styling across 17
links. Added one global :focus-visible rule rather than sprinkling classes,
because the issue's own scope was "all interactive <a> and <Link> elements".

Not the ring the issue asked for. It suggested ring-primary-cyan; #22D3EE is
6.26:1 on navy and 2.38:1 on white, so on this light-first design that ring
would have failed WCAG 1.4.11 across most of the site. Two rings instead — white
inside, navy outside — so the white carries the dark bands and the navy carries
the light sections. Worst case across every background in the palette is
13.62:1. box-shadow so both rings follow each element's own border-radius, plus
a transparent outline for forced-colors mode.

#192 — aria-expanded={isOpen} and aria-controls on the mobile nav trigger, and
the id on SheetContent it now points at. Confirmed as "false" in the
prerendered HTML.

#193 — INVERTED. The issue asked for aria-labels to be added consistently to
hero CTAs. The aria-labels already there were WCAG 2.5.3 Label in Name failures,
Level A: "Schedule a consultation" is not a superset of the visible "Schedule
Consultation", so a voice-control user saying what they see cannot activate the
link. Doing what the issue asked would have spread a Level A failure. Removed
the six that broke it; kept the eight that genuinely add context and do contain
their visible text, including the icon-only header logo link that needs one.

Audit now reports 0 failures across Home, Services, Footer and Header.

Verified: npm run build; the ring is in the built CSS, aria-expanded="false" and
id="mobile-nav-content" are in the prerendered HTML, all six removed labels are
absent from it, and the visible CTA text still renders.

NOT verified: nothing has been walked keyboard-only in a browser, and no screen
reader has been used. That gap is recorded in docs/qa/ClaudeQACoverage.md.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 02:31:13 -05:00
Ripley 615043db44 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
Ripley 2732d1fc4f 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
Ripley 493b97e4a4 docs: record two Command Center traps that each cost time today
Both are silent in the expensive direction, which is why they are written down
rather than remembered.

A webhook registered without the shared signing secret looks entirely healthy
in Forgejo — active:true, test deliveries accepted with 204 — and has every
delivery rejected by privacyllc.dev with 401 bad_signature. Forgejo exposes no
delivery history through its API, so nothing says so. This repository ran that
way for ten minutes today. Includes how to prove a delivery lands.

A check-in's projectId is the UUID, not the slug, unlike every other route in
that API. A slug returns a generic {"code":"unknown"} 500 that names no field
and is indistinguishable from a genuinely malformed request.

Also records that neither reconcile_all nor sync_projects forces a re-read of a
project inside its freshness window, and that the agent API has no per-project
force — so refreshed=0 means "not due", not "nothing to do".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:36:46 -05:00
Ripley 1d952a0170 docs: log the template adoption, with next action and blockers
Step 6 of docs/WORK_CYCLE.md. Records what the adoption proved rather than
what it did — the two facts that were not known this morning are that the lead
database is restorable, and that three defects were sitting in the tree that
the adopted tooling found on the way in.

Next action: start Batch 10 (accessibility), #189 first, and walk the pages
keyboard-only before fixing anything — all six were found by reading markup.

Blockers: none on this work. Six issues are blocked on the site owner, two of
which are unsubstantiated claims live on the site since May.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:31:17 -05:00
Ripley 8e186fdf3f chore: port the existing pre-push hook into .githooks before activating hooksPath
This checkout already had a pre-push in .git/hooks. Setting core.hooksPath
redirects every hook to .githooks/, so activating the template's hooks would
have silently stopped it running — which is the exact failure the versioned
hooks directory exists to prevent, applied to a hook.

Kept its two working-tree checks: a push that leaves edits behind is how docs
end up one commit adrift of the code they describe.

Dropped its third. It refused whenever the branch was ahead of its remote —
the precondition for pushing at all — so it fired on every real push and its
only advice was to re-run with --no-verify. A guard that can never pass teaches
people to bypass the two beside it that can. It did exactly that on the push of
the adoption commits.

Both remaining checks proven to refuse a dirty tree and pass a clean one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 01:22:33 -05:00
Ripley aa1d805e04 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:21:13 -05:00
Ripley 5c04195cb1 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
null ac6f5d1f78 feat(seo): publish privacy policy, remove street address, prerender all routes (batch 0.9.3)
Client directive (Levi Halford, 2026-08-01) ahead of Google/Meta lead forms.

Privacy policy:
- Publish approved policy verbatim at /privacy-policy (src/data/privacyPolicy.js
  is the single source of truth; 292/292 source lines verified present)
- Privacy Policy link in the footer of every page
- Effective/Last Updated 2026-07-31, privacy@queuenorth.com as mailto

Remove St. Petersburg street address from every surface named in the brief:
footer, contact page, schema markup, SEO metadata, Google Maps links. Collapse
ProfessionalService + Organization schema into a single Organization with
areaServed: United States; drop geo coordinates, priceRange, openingHours.
Add the approved US-coverage sentence to About. No replacement address.

Crawler visibility (the site previously served 0 bytes of body HTML without JS):
- Prerender all 19 routes at build time via src/entry-server.jsx + scripts/prerender.js
- Hoist title/meta/canonical/JSON-LD into <head>; renderToString does not do this
  and react-helmet-async's context is empty under React 19
- Serve prerendered HTML; return a real 404 for unknown paths instead of 200
- Hydrate instead of discarding the prerendered markup

SEO/perf:
- Titles <=60 and descriptions <=160 chars across all pages
- Add BreadcrumbList to interior pages, WebSite to home
- Generate sitemap.xml from the route list with git-derived lastmod
- 301 duplicate URL forms (trailing slash, //, /index.html), preserving query
- Immutable caching for content-hashed assets; no-cache for HTML
- Split the 522 KB bundle into app/react-vendor/router/icons
- loading/decoding/fetchpriority + per-route hero preload; drop unused asset

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-01 01:45:52 -05:00
null 196389ddf3 fix(brand): update veteran-owned badge to certified mark logo (batch 0.9.2) 2026-06-14 16:17:42 -05:00
null d074e597b2 feat(zoho): WebToLead forwarding mode, veteran-owned certified badge, Docker/env/CI updates (batch 0.9.1) 2026-06-14 16:08:29 -05:00
null 05b27d216a feat: Dockerfile/docker-compose updates, server improvements, contact form with recaptcha, API integration (batch 0.9.0) 2026-06-14 15:37:26 -05:00
null 76cb558e8b chore: bump version to v0.8.3 2026-05-28 01:10:38 -05:00
null a8d9492a80 added 404 2026-05-28 00:41:24 -05:00
null 0f272fcf19 error and injection 2026-05-28 00:18:08 -05:00
null 3a61000c12 fix scroll 2026-05-27 23:40:09 -05:00
null e625a24b6e redirect to contact form 2026-05-27 22:33:54 -05:00
null f35de43952 fix footer 2026-05-27 22:19:04 -05:00
null c43d3bc955 company name footer one line 2026-05-27 22:08:46 -05:00
null 1dcfbfc7a7 chore: bump version to v0.8.2 2026-05-27 21:56:47 -05:00
null ec14701795 center footer 2026-05-27 21:54:44 -05:00
null 78967ff56f phone 2026-05-27 21:49:21 -05:00
null cc1970fd1d added phone numebr hamburger 2026-05-27 21:44:06 -05:00
null 8c1e0f4c3d ui 2026-05-27 21:35:12 -05:00
null fb12d8cf3c 1 2026-05-27 21:23:17 -05:00
null a3ba03b7e1 cisco about 2026-05-27 21:15:54 -05:00
null f59d053afd mobile navbar text per owner 2026-05-27 21:04:30 -05:00
null 033bdf6625 Form now POSTs to Zoho 2026-05-27 20:57:55 -05:00
null 548e20e6f0 . 2026-05-27 14:43:50 -05:00
null 225c4e5485 chore: bump version to v0.8.1 2026-05-27 14:33:13 -05:00
null 8f20670292 breadcrumbs 2026-05-27 14:27:30 -05:00
null 0cfa048d0d injection security 2026-05-27 14:14:24 -05:00
null 4410f01d48 Missing `Description` or `aria-describedby={undefined}` for {DialogContent}. 2026-05-27 14:11:48 -05:00
null 6bab4b5c89 corrections 2026-05-27 14:10:28 -05:00
null f9b36c198b badges size 2026-05-27 13:58:31 -05:00
null 4b17e9f109 badges 2026-05-27 13:28:13 -05:00
null 6de9490764 badge center 2026-05-27 12:51:08 -05:00
null a81e97fb31 mobile view 2026-05-27 12:42:25 -05:00
null 362a7e0059 heatmap 2026-05-26 17:08:55 -05:00
null 431d1157b3 chore: bump version to 0.8.0 2026-05-26 13:44:27 -05:00
null 510edace07 removed duplicate card 2026-05-26 13:41:46 -05:00
null 8731903463 updated badges homepage 2026-05-26 13:36:16 -05:00
null a2a32687ff branding 2026-05-26 13:29:37 -05:00
null f0f0cfd599 #181 2026-05-26 13:04:57 -05:00
null 335601b00e remove submit ticket 2026-05-26 12:55:23 -05:00