# 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 — v0.9.4 released and deployed. Production is on a number

The first release and the first deploy this project has ever made through a
script. Both worked; the site did not go down beyond the recreate.

**Asked three times whether it would break anything, and it would have — twice.**
Both were found by checking rather than by running, which is the only reason
this entry is not an incident report.

*`release.sh` would have shipped a site whose contact form could not be
submitted.* `.dockerignore` excludes `.env` from the build context, and the
script passed `--build-arg VITE_RECAPTCHA_SITE_KEY=${VITE_RECAPTCHA_SITE_KEY:-}`
without ever loading `.env` — and that variable is unset in every shell. The
`:-` made empty silently acceptable. An empty key makes
`RecaptchaPlaceholder.jsx` render *"Security verification is not configured."*
and never produce a token, and the server has `RECAPTCHA_ENABLED=true`, so every
submission is rejected. **Lead capture would have stopped entirely** — the whole
purpose of the site — and nothing about a successful build would have said so.

*`deploy.sh`'s default would have rolled production back two months.* The newest
published **numbered** tag was `v0.8.3`, built 2026-05-28; the running `:dev`
image was built 2026-08-01. Inspecting both images showed `v0.8.3` has no
`privacy-policy/` and **no prerendered route directories at all**. The default
path would have removed the privacy policy — which exists specifically because
Meta's crawler does not run JavaScript — and every prerendered route.

Both are now guards that refuse, not cautions that document. The release one
also greps the **built image's** bundle for the key before pushing, because a
guard that trusts the wiring is the wiring.

**What was checked before touching production**, since the question was asked
directly: the registry pulls anonymously (making the repo private did not affect
packages — they are owned separately); `docker compose config` validates the
rewritten stack file; the env-line count is asserted, and `compose config`
showed exactly why — without the `Env` array the `${VAR:-false}` defaults
resolve reCAPTCHA and Zoho WebToLead to **false**, so a stripped env would come
back healthy and quietly capture nothing; only one of the fourteen commits since
the running image touches anything a visitor sees.

**Watchtower was the missing piece of the story.** Stack 39 runs nightly at
02:00 with no label filter, watching every container, and `qn-website-dev` had
no opt-out. Following the moving `:dev` tag, the site was updating itself
overnight — the undocumented deploy path nobody could find. It now pins a
version *and* carries the opt-out label, so nothing changes without a deliberate
deploy.

**The deploy itself:** one PUT, three lines changed — the image, the CORS
trailing slash, and the Watchtower label — with a verified backup taken first
and automatic rollback armed. It did not need to roll back.

```
digest   …3253b10c  ->  …1d283944
version  none       ->  0.9.4
CORS     https://queuenorth.com/  ->  https://queuenorth.com
leads    3 before, 3 after
```

`status.sh --deployed-version` answers `0.9.4` — the first time this project has
been able to say what is running without reading a digest.

- **Closed:** **#212** (CORS origin), with the live header as evidence. Batch 10
  is now not only fixed but *shipped* — `aria-expanded` and the dual-tone focus
  ring are live, and the six Label-in-Name failures are gone from the served
  HTML.
- **Next action:** Batch 11 — UI defects on narrow viewports (#195–#198). Verify
  each claim by rendering at the stated width first: four of Batch 10's six
  issues misstated their own evidence, and these are all viewport-dependent.
- **Blockers:** none on the work. Six issues remain blocked on the site owner
  (#68, #110, #162, #213 in Batch 13; #69, #70 in Batch 14). The backup is still
  a single copy on one workstation with no schedule — #211, and it is the one
  thing here that would actually hurt.

### 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 `<Button onClick={navigate}>` with `<Link>` elements in Home.jsx
- **#105:** Contact page CTA scrolls to form instead of linking to itself

### Batch 9.4: UI Fixes (Scarlett)
- **#112:** Support CTA uses `bg-white text-primary-navy` instead of undefined `bg-secondary-bg`
- **#106:** Support page phone/email are now clickable links
- **#90:** Footer tel: links now include `+1` country code

### Batch 9.5: Backend Security (Neo)
- **#119:** Honeypot spam protection on Contact and Support forms
- **#126:** 409 Conflict shows friendly toast instead of error

### Batch 9.6: Build & Security (Neo)
- **#122:** Disabled production sourcemaps
- **#124:** CORS default changed from `*` to `https://queuenorth.com`
- **#131:** CSP allows WebSocket for Vite HMR in development

### Batch 9.7: Assets & Meta (Scarlett)
- **#99:** Added favicon, apple-touch-icon, site.webmanifest
- **#117:** og:image now uses PNG (1200x630) instead of unsupported SVG
- **#118:** Logo optimized from 348KB SVG to 44KB PNG (87% reduction)

### Batch 9.8: Icons & Copy (Scarlett)
- **#98:** Cisco uses ShieldCheck icon, Veteran uses Award icon (no more text placeholders)
- **#100:** Removed invalid Category/slug display from ServiceDetail sidebar
- **#103:** Removed JointLogoWhite placeholder section, replaced with clean Trust Bar
- **#107:** Footer address changed from marketing copy to "Orlando, FL"

### Batch 9.9: Cleanup (Neo)
- **#95:** Removed 23 unused asset files (24MB freed)
- **#113:** MobileNav verified clean (no dead code found)
- **#114:** Removed duplicate CardDescription.jsx (properly used from Card.jsx)
- **#115:** Removed unused Dialog.jsx component
- **#116:** Removed unused zustand dependency

## v0.6.6 — Phase 7: Zoho CRM Integration Complete — 2026-05-17

### Zoho Integration (all batches 7.0–7.4)
- **#74 (Batch 7.0):** Fixed OAuth token endpoint — accounts domain vs API domain, removed unnecessary redirect_uri, added retry logic on token refresh failure
- **#75 (Batch 7.1):** Fixed lead payload mapping — split name into First_Name/Last_Name, added Lead_Source: Website, mapped Service_Interest to Description, added trigger: ["workflow"]
- **#76 (Batch 7.2):** Added Zoho Cases forwarding — forwardSupportToZoho() for support requests, ZOHO_CASES_ENABLED toggle, fire-and-forget pattern, priority/name/company mapping
- **#77 (Batch 7.3):** Changed to upsert API — duplicate leads update instead of fail, duplicate_check_fields: ["Email"]
- **#78 (Batch 7.4):** Created docs/zoho-setup.md — step-by-step setup guide for non-technical admins, datacenter variants, troubleshooting
- **#73:** Parent Zoho CRM Integration issue closed

## v0.6.5 — Batch 0.6.5: Code Review Bug Fixes (10 issues) — 2026-05-17

### Fixed
- **#63 (P0):** Home.jsx — `industry.href` undefined, navigating to `/industries/undefined`. Fixed to use `/industries/${industry.id}`
- **#50 (P1):** Server — `sanitized` scope error in catch block. Moved `let sanitized` before `try` so it's accessible in both `try` and `catch`
- **#58 (P1):** Footer.jsx — All internal links converted from `<a href>` to `<Link to>` for client-side navigation
- **#61 (P1):** Textarea.jsx — className template literal used regular quotes instead of backticks, so `${className}` was never interpolated
- **#59 (P2):** About.jsx — CTA button converted from `<a href>` to `<Link to>`
- **#60 (P2):** Support.jsx — Contact Us button converted from `<a href>` to `<Link to>`
- **#62 (P2):** Badge.jsx — `text-foreground` (undefined) changed to `text-text`
- **#64 (P2):** Support.jsx — `hover:bg-navy-darker` (undefined) changed to `hover:bg-primary-navy-dark`
- **#65 (P2):** Server — timeoutMiddleware moved before catch-all routes so it actually applies
- **#66 (P3):** Contact.jsx — Self-referencing `<a href="/contact">` converted to `<Link to="/contact">`

### Open
- **#67 (P3):** Asset filenames with spaces (low priority, cosmetic)

## v0.6.2 — Phase 6 Complete: All Owner Feedback Issues Closed — 2026-05-17

### Completed
- **Issue #45:** Nav active-state styling — already implemented (useLocation + isActive helper, desktop + mobile)
- **Issue #46:** Service card icons — already implemented (lucide-react icons from services.js data)
- **Issue #47:** Cisco partnership signals — already implemented (Home hero, trust signals, Why Queue North, footer)

### Note
These three issues were resolved in earlier Phase 5/6 batches. Verified in code and closed.

## v0.6.1 — Batch 0.6.1: Security Hardening, Retry Logic, Debouncing — 2026-05-17

### Completed
- **Batch 0.6.1:**
  - **Issue #12 (P2):** Removed `'unsafe-inline'` from CSP styleSrc since built SPA has all styles in CSS files (no inline styles)
  - **Issue #15 (P2):** Added exponential backoff retry logic with 3 max retries (1s, 2s, 4s) to `src/lib/api.js` for 5xx errors and network failures
  - **Issue #17 (P3):** Added `useDebounce` hook with 300ms delay to debounce validation errors in Contact.jsx and Support.jsx
  - **Issue #18 (P3):** Closed — React Query already handles GET caching with `staleTime: 5 minutes`

### Files Modified
- `server/index.js` — Removed `'unsafe-inline'` from CSP styleSrc
- `src/lib/api.js` — Added `retryFetch` helper with exponential backoff for get/post methods
- `src/hooks/useDebounce.js` — Created debounce hook
- `src/pages/Contact.jsx` — Integrated debounce for validation errors
- `src/pages/Support.jsx` — Integrated debounce for validation errors
- `package.json` — Version bumped to 0.6.1

### Verified
- ✅ `npm run build` passes
- ✅ Docker image builds successfully
- ✅ Container starts on port 3001 and responds to health check
- ✅ Contact and Support forms debounce validation errors
- ✅ API retry logic configured (3 retries, exponential backoff)
- ✅ CSP correctly configured for static SPA (no inline styles)

### Forgejo Issues
- Issue #12 closed (CSP fix applied)
- Issue #15 closed (retry logic implemented)
- Issue #17 closed (debouncing added)
- Issue #18 closed (React Query handles GET caching)

**Note for Ripley:** Forgejo issue closing via API requires `FORGEJO_REGISTRY_TOKEN` environment variable. Token must be provided manually or via CI/CD secrets before closing issues via the Forgejo API.

---

## v0.5.9 — Phase 6 Batches 0.6.5–0.6.7: Industry Icons, 8x8 Page Merge, Image Verification — 2026-05-17

### Completed
- **Batch 0.6.5 (Industry Icons Bigger, Issue #41):**
  - Industry icons enlarged with improved color and contrast
  - Icons more visually prominent on all screen sizes

- **Batch 0.6.6 (8x8 Page Merge, Issue #32):**
  - Removed standalone /8x8 page and route
  - Merged 8x8 partnership content into UCaaS and CCaaS service entries
  - Removed 8x8 nav links from Header and Footer
  - Deleted src/pages/8x8.jsx

- **Batch 0.6.7 (Verify Image Issues, Issues #30, #31):**
  - Confirmed services and industries pages render icons correctly (lucide-react)
  - No broken image references — Phase 5 redesign resolved both issues
  - Docker build + runtime test passed

### Forgejo Issues Closed
- #30 (service images broken) — resolved by icon redesign
- #31 (industry images broken) — resolved by icon redesign
- #32 (8x8 page merge) — merged into service pages
- #33–#41 — all owner feedback issues resolved

### Files Modified
- `src/pages/Home.jsx` — Industry icon sizing (batch 0.6.5)
- `src/data/services.js` — Expanded UCaaS/CCaaS with 8x8 content (batch 0.6.6)
- `src/router.jsx` — Removed /8x8 route
- `src/components/layout/Header.jsx` — Removed 8x8 nav link
- `src/components/layout/Footer.jsx` — Removed 8x8 nav link
- `src/pages/8x8.jsx` — Deleted

---

## v0.5.7 — Phase 5 Batch 9: Hero Left-Alignment + About Section Fixes (Issues #33, #34, #36, #37) — 2026-05-17

### Completed
- **Batch 0.6.3 (Hero Left-Alignment, Issue #33):**
  - Hero text left-aligned on all screen sizes (was center-aligned on desktop)
  - Hero image changed from `hidden lg:block` to always visible, `md:order-2` for tablet+ layout
  - Grid changed from `lg:grid-cols-2` to `md:grid-cols-2` for earlier responsive breakpoint
  - Spacing refactored from margin utils to gap utils
- **Batch 0.6.4 (About Section Fixes, Issues #34, #36, #37):**
  - Removed "Founded in 2000" → updated to "Founded by military veterans"
  - Added "We continue the mission" veteran framing paragraph
  - Replaced "Cybersecurity for Communications" with "Vendor-Neutral Consulting" in expertise list
  - Updated About page image sizing to `max-h-96 h-auto object-cover`

### Files Modified
- `src/pages/Home.jsx` — Hero section layout changes, image visibility fix
- `src/pages/About.jsx` — Story text update, image sizing fix, expertise list update
- `package.json` — Version bumped to 0.5.7

### Verified
- ✅ `npm run build` passes clean
- ✅ Docker image builds successfully
- ✅ Container starts on port 3001 and responds to health check
- ✅ Home page loads at http://localhost:3001 — hero text left-aligned, image visible
- ✅ About page loads at http://localhost:3001/about — veteran framing present, image sized correctly

---

## Cisco Partnership Signals — 2026-05-17

### Added
- Cisco Certified Partner trust card in Trust Signals section (8x8 and Cisco dual partnership)
- Cisco platform mentions in Services section descriptions
- Cisco partner badge in Footer bottom section
- Cisco partner visual indicator (white "C" badge) in Trust Bar section

### Files Modified
- `src/pages/Home.jsx` — Cisco trust card, trust bar Cisco badge, vendor neutrality section
- `src/data/services.js` — Cisco platform mentions in unified-communications and contact-center services
- `src/components/layout/Footer.jsx` — Cisco in footer bottom section
- `package.json` — Version bumped to 0.5.6

### Verified
- ✅ `npm run build` passes clean
- ✅ Docker image builds successfully
- ✅ Container starts on port 3001 and responds to health check
- ✅ Home page loads at http://localhost:3001 — Cisco appears in trust signals
- ✅ Services page loads at http://localhost:3001/services

---

## Zoho P0/P1 Audit Fixes — 2026-05-17

### Fixed
- P0 #2: Added null check for Zoho credentials before forwarding (early return)
- P0 #3: Added response.ok check for Zoho API responses (log + return on failure)
- P0 #4: Confirmed Docker uses USER nodejs (no su-exec needed)
- P1 #5: Added 10-second timeout on Zoho fetch using AbortController
- P1 #6: UNIQUE constraint on email already handled by db.js migration
- P1 #10: Schema normalizes empty strings to null; Zoho payload uses `|| null`

### Files Modified
- `server/index.js` — Zoho forwarding logic (credential checks, response validation, timeout, payload normalization)

### Build
- ✅ `npm run build` passes

---

## v0.5.5 — Phase 5 Batch 8: Service Card Icons (Issue #35) — 2026-05-17

### Completed
- Services page updated with lucide-react icons matching Home page pattern
- 7 distinct icons: MessageCircle, Users, LifeBuoy, GraduationCap, LinkIcon, Wifi, Network
- Icon-switching logic in Services.jsx maps each service icon name to correct component
- No hardcoded lightning bolt — each service displays its own distinct icon

### Files Modified
- `src/pages/Services.jsx` — replaced hardcoded lightning bolt SVG with lucide-react imports and icon-switching logic
- `package.json` — version bumped to 0.5.5

### Verified
- ✅ `npm run build` passes clean
- ✅ Docker image builds successfully
- ✅ Container starts on port 3001 and responds to health check
- ✅ Services page loads at http://localhost:3001/services
- ✅ Each of the 7 service cards displays a distinct icon per services.js data

---

## v0.5.4 — Phase 5 Batch 7: Nav Active-State Styling — 2026-05-17

### Completed
- Header.jsx and MobileNav.jsx now use `useLocation` hook from React Router 7
- Active nav links display white text (`text-white`) with `font-semibold` styling
- `isActive()` helper function compares `location.pathname` to link href
- Both desktop and mobile navigation use consistent active-state styling

### Files Modified
- `src/components/layout/Header.jsx` — Added `useLocation` import and `isActive()` function, updated desktop nav and mobile nav link styling
- `src/components/layout/MobileNav.jsx` — Added `useLocation` import and `isActive()` function, updated all nav links with active-state styling
- `package.json` — Version bumped to 0.5.4

### Verified
- ✅ `npm run build` passes clean
- ✅ Docker image builds successfully
- ✅ Container starts on port 3001 and responds to health check
- ✅ Home page loads at http://localhost:3001
- ✅ Both Header.jsx and MobileNav.jsx import `useLocation` from `react-router-dom`
- ✅ `isActive()` function correctly compares `location.pathname` to link href
- ✅ Active links display white text with font-semibold styling

---

## v0.5.3 — Phase 5 Batch 6: Footer + Contact Improvements — 2026-05-17

### Completed
- Footer cleanup: email (`info@queuenorth.com`), phone (`(906) 482-6616`), and CTA (`Request Consultation`) all present
- Contact page hero: prominent phone/email display + CTA button
- Home page CTA links added to Trust Signals, Services, Why Queue North sections
- Minor updates to Support page

### Files Modified
- `src/components/layout/Footer.jsx` — Footer cleanup
- `src/pages/Home.jsx` — CTA links in sections
- `src/pages/Contact.jsx` — Hero CTA
- `src/pages/Support.jsx` — Minor updates
- `package.json` — Version bumped to 0.5.3

### Verified
- `npm run build` passes
- Docker image builds and container starts on port 3001
- Footer renders with email, phone, and CTA
- Contact page hero displays phone/email and Request Consultation button

---

## v0.5.2 — Phase 5 Batch 5: Why Queue North Section Refinement — 2026-05-17

### Completed
- Replaced generic "Architecture/Deployment/Lifecycle" pillars with 4 concrete differentiators
- Added lucide-react icons: Headphones, UserCheck, Activity, ShieldCheck
- Changed grid from 3-col to 4-col responsive layout (1 col mobile, 2 col tablet, 4 col desktop)
- Removed unused CardDescription import
- Updated Why Queue North section subtitle to "Four concrete differentiators that set us apart"
- Clean card layout with centered icon containers and consistent styling

### Files Modified
- `src/pages/Home.jsx` — Why Queue North section complete redesign
- `package.json` — Version bumped to 0.5.2

### Verified
- `npm run build` passes
- Docker image builds and container starts
- Home page loads on http://localhost:3001
- Responsive layout verified (1/2/4 columns as specified)

---

## v0.5.1 — Phase 5 Batch 1: Hamburger Menu Fix + DialogTitle A11y — 2026-05-13

### Verified Complete
- Fixed SheetContent missing positional CSS classes (panel was zero-size)
- Installed and configured tailwindcss-animate
- Added visually hidden SheetTitle for accessibility (Radix DialogTitle requirement)
- Forgejo issues #22, #23 closed

---

## v0.5.4 — Phase 5 Batch 4: Services Rewrite — Business Outcomes — 2026-05-17

### Completed
- Section title changed: "Our Services" → "What We Handle"
- Subtitle changed to outcome-focused: "From phones to firewalls, we keep your business running"
- Added `homeDesc` field to all 7 services in `src/data/services.js` with business-outcome language
- Service cards now show: icon + name + homeDesc (outcome) + shortDesc (supporting) + CTA
- lucide-react icons per service: MessageCircle, Users, LifeBuoy, GraduationCap, LinkIcon, Wifi, Network
- B2B professional card layout with icon containers (primary-navy/10 bg)
- Service detail pages (ServiceDetail.jsx) unchanged

### Files Modified
- `src/pages/Home.jsx` — Services section rewrite
- `src/data/services.js` — Added homeDesc field to each service
- `FUTURE.md` — Marked batch 0.5.4 complete

---

## v0.5.3 — Phase 5 Batch 3: Trust Signals Section — 2026-05-17

### Completed
- Added dedicated Trust Signals section immediately after hero section
- 8x8 Certified Partner badge: prominent white card with logo, certification number, and descriptive text
- Veteran Owned badge: VCERT verified with certification #, values-based messaging
- "25+ Years Experience": metric card with 25+ using Georgia font (font-numeric class)
- Metrics grid: 4 key stats displayed (99.99% uptime, <15m response, 24/7 support, 100% satisfaction)
- Clean card layout: white cards on section-alt background with hover effects
- Mobile-first: three-column grid on desktop, stacked on mobile
- Business outcomes focus: reliability, experience, support承诺, not technical jargon

### Files Modified
- `src/pages/Home.jsx` — Added Trust Signals section after hero
- `FUTURE.md` — Marked batch 0.5.3 complete

### Notes for Next Agent
- The trust signals are now front and center after the hero, before services
- Georgia font (`font-numeric`) consistently used for all metrics
- All metrics use realistic placeholder values that can be replaced with real stats when available
- The white card design provides contrast against the section-alt background

---

## v0.5.2 — Phase 5 Batch 2: Hero Section Rewrite — 2026-05-17

### Completed
- Hero headline replaced: "Modern Communications Infrastructure Without the Vendor Noise" → "Reliable Business Communications — Without the Runaround"
- Subtext updated to emphasize trust, reliability, and business outcomes
- 8x8 Certified Partner badge made prominent (featured card with logo, not small pill)
- Trust signals immediately visible on mobile without scrolling
- CTAs updated: "Schedule Consultation" (primary, /contact), "View Services" (secondary, /services)
- Color palette evolved: navy base with teal-900 gradient accent
- Mobile-first design verified — trust signals visible above fold on phone viewports

### Files Modified
- `src/pages/Home.jsx` — Hero section rewrite
- `.learnings/scarlett/LEARNINGS.md` — Added hero rewrite entry
- `FUTURE.md` — Marked batch 0.5.2 complete

### Notes for Next Agent
- The 8x8 badge is now prominently featured — keep this prominence in future sections
- Georgia font (`font-numeric`) used for "25+ Years Experience" — consistent with design rule
- Consider similar trust signal presentation in the Trust Bar section

---

## v0.4.8 — Phase 4 Batch 8: Error Handling Hardening + Dep Audit — 2026-05-13

### Verified Complete
- Global error handlers: uncaughtException + unhandledRejection → log + exit(1)
- 404 catch-all for `/api/*` routes returns `{ error: 'Not found' }`
- Health check enhanced: DB connectivity check, returns `{ status: 'ok', db: 'ok' }` or 503
- Request timeout: 30-second middleware, 504 response
- Consistent error format: 400/404/429/500/503/504 all follow `{ error, fields? }` pattern
- `npm audit`: 0 vulnerabilities
- `npm outdated`: major updates noted (Express 5, Vite 8, etc.) but not upgraded

## v0.4.7 — Phase 4 Batch 7: Rate Limiting + Security Headers + CORS — 2026-05-13

### Verified Complete
- express-rate-limit: 5 req/min per IP on /api/leads and /api/support, configurable via RATE_LIMIT_PER_MINUTE
- helmet: CSP (scripts 'self', styles 'self' inline, fonts 'self' + Google Fonts), HSTS, X-Content-Type-Options, X-Frame-Options DENY
- cors: configurable via CORS_ORIGIN env var, credentials enabled, API routes only
- Trust proxy enabled for correct client IP behind Docker/reverse proxy
- Rate limit returns 429 with JSON error + retryAfter
- All security headers confirmed via curl
- Docker config updated: all new env vars in Dockerfile and docker-compose.yml

## v0.4.6 — Phase 4 Batch 6: Zoho Forwarding Layer — 2026-05-13

### Verified Complete
- Zoho CRM forwarding via REST API v8 (POST /crm/v8/Leads)
- OAuth2 token management: refresh token → access token, in-memory cache with auto-refresh
- Fire-and-forget: Zoho forwarding is async, never blocks form response
- Configurable via 6 env vars: ZOHO_ENABLED, ZOHO_API_DOMAIN, ZOHO_CLIENT_ID, ZOHO_CLIENT_SECRET, ZOHO_REFRESH_TOKEN, ZOHO_REDIRECT_URI
- Best-effort design: Zoho failures logged but never propagate to client
- ZOHO_ENABLED=false: no Zoho activity, form submits normally
- ZOHO_ENABLED=true with invalid creds: form still succeeds, Zoho errors logged to console

## v0.4.5 — Phase 4 Batch 5: Server-Side Validation + Input Sanitization — 2026-05-13

### Verified Complete
- Zod schemas updated with `.trim()` and `.max()` on all fields
- Input sanitization: strip HTML/script tags, truncate to max lengths
- Request body size limit: 1MB (returns 413)
- Validation errors formatted as `{ error, fields }` — no stack traces
- All curl tests pass: valid data, empty fields, invalid email, XSS, max length, short issue
- XSS test confirmed: `<script>` and `<b>` tags stripped from stored data

## v0.4.4 — Phase 4 Batch 4: Client-Side Validation + Sonner Feedback — 2026-05-13

### Verified Complete
- Contact form: required field validation (company, name, email, message)
- Support form: required field validation (name, company, email, issue)
- Email format validation with regex on both forms
- Issue minimum length validation (10 chars) on Support form
- Inline error messages below each invalid field (red text, small)
- Sonner toast for validation errors on submit attempt
- Error clearing as user corrects fields (onChange)
- Red ring/border on Input/Textarea when field has validation error
- Form inputs disabled during submission (mutation.isPending)
- Form opacity reduced to 70% during submission
- Build passes clean

## v0.4.1 — Phase 4 Batch 1: Contact Form Wired to Express — 2026-05-12

### Verified Complete
- Contact form submits all fields to `/api/leads` via TanStack Query mutation
- Success/error response handling with Sonner toasts
- Confirmation shown on success, form resets
- Error state shown on failure
- Backend Zod validation + SQLite storage working
- Already implemented in Phase 1/2, verified end-to-end

## v0.2.13-fix — Phase 2 Fix: Legacy CSS Consolidation — 2026-05-13

### Fixed
- Removed duplicate `src/index.css` (was not imported anywhere)
- Created clean `src/index.css` as single Tailwind entry point
- Moved `maxWidth.container: 1280px` into `tailwind.config.js` theme extensions
- Updated `src/App.jsx` import from `./App.css` to `./index.css`
- Deleted redundant `src/App.css`
- All `bg-section-alt` usages verified still working

### Verified
- `npm run build` passes
- All 14 Phase 2 batches now fully verified

---

## v0.4.3 — Phase 4 Batch 3: SQLite Persistence Verification — 2026-05-13

### Verified Complete
- Database file (`db/queuenorth.db`) created on first run if missing
- `leads` and `support_requests` tables have correct schema (all columns match server/index.js)
- Data persists across server restarts
- Docker volume test: stop container, restart, confirm data present
- Health check endpoint responds correctly
- `docker-test.sh` persistence suite passes all checks

## v0.4.2 — Phase 4 Batch 2: Support Form Wired to Express — 2026-05-12

### Verified Complete
- Support form submits all fields to `/api/support` via TanStack Query mutation
- Success/error response handling with Sonner toasts
- Confirmation shown on success, form resets
- Error state shown on failure
- Backend Zod validation + SQLite storage working
- Already implemented in Phase 1/2, verified end-to-end

---

## v0.2.2 — Docker Image Size Fix — 2026-05-12

### Fixed
- Removed duplicate `node_modules` copy in Dockerfile runner stage (was copying full dev+prod modules as a permanent layer)
- Reduced image size from 331MB to 215MB (35% reduction)
- `npm ci --omit=dev` now runs cleanly without pre-existing dev modules

### Verified
- Docker build succeeds
- Container starts and health check passes
- Frontend serves correctly

---

# Queue-North-Website — Changelog

## v0.3.4 — Phase 3 Visual Overhaul Complete — 2026-05-12

### Completed Batches
- **Batch 1 (v0.3.1):** Tailwind theme tokens, spacing scale, container width, Inter font
- **Batch 2 (v0.3.2):** Home page redesign — hero, trust bar, services grid, why QN pillars, industries preview, final CTA
- **Batch 3 (v0.3.3):** Header/footer/mobilenav polish, navy-light color token, fixed Button.jsx TS generics build bug
- **Batch 4 (v0.3.4):** Inner pages layout system — consistent hero/card/CTA pattern across About, Services, ServiceDetail, Industries, IndustryDetail, 8x8
- **Batch 5:** Contact/Support forms verified compliant, no changes needed

### Verified
- All batches build successfully
- Contact and Support pages already compliant with OVERHAUL_PLAN.md
- No `asChild` usage on Button (unsupported, replaced with styled anchors)

---

## v0.3.2 — Phase 3 Batch 2: Home Page Redesign — 2026-05-12

### Changed
- Updated Home.jsx to import `industries` data from data file
- Replaced hardcoded industry data with dynamic rendering from `industries.js`
- Updated Services and Industries cards to use Button component for "Learn more" links
- Added MapPin icon from lucide-react for industry cards
- Added useNavigate hook for programmatic navigation
- Ensured consistent use of shadcn/ui Button component across the home page
- Updated version to `0.3.2` for Phase 3 Batch 2

### Verified
- `npm run build` passes
- All routes respond correctly
- Button components render with correct styling
- Responsive layout works on mobile and desktop

---

## v0.3.1 — Phase 3 Batch 1: Theme + Tailwind Config + Typography — 2026-05-12

### Changed
- Enhanced Tailwind spacing scale with utility values (24-48rem)
- Updated container max-width to 1280px per design spec
- Confirmed color palette, Inter font, Georgia numeric font already in place

---

## v0.1.1 — Phase 1 Agent-Pass Checkpoint — 2026-05-12

### Changed
- Corrected Phase 1 version to reflect completed agent-pass checkpoint semantics.
- Documented that every completed agent pass/checkpoint within a phase increments the patch version.
- Confirmed Phase 1 verified output is pushed to `dev` at `0.1.1`.

### Verified
- `npm run build` passes.
- Backend health endpoint responds successfully at `/api/health`.

## v0.2.1 — Dockerization — 2026-05-12

### Added
- Docker build for production deployment
- docker-compose.yml for local development with SQLite persistence
- npm scripts: `docker:build`, `docker:run`, `docker:compose:up/down/logs`
- Health check in Dockerfile and docker-compose
- Non-root `nodejs` user for security
- Layer caching optimization (copy package.json first, install, then copy source)
- `.dockerignore` to exclude node_modules, dist, db, logs, git, private docs

### Changed
- Updated package.json to `0.2.1` for Docker batch

### Verified
- Docker build succeeds with `npm run docker:build`
- Container starts and health check passes
- SQLite database persists in `./db` volume

---

## v0.2.0 — Phase 2 Layout Complete — 2026-05-12

### Added
- All route pages implemented (Home, About, Services, ServiceDetail, Industries, IndustryDetail, 8x8, Contact, Support).
- Data files for services and industries moved to `/src/data/`.
- Layout components (Header, Footer, MobileNav) built and integrated.
- Legacy `styles.css` removed (replaced by Tailwind configuration).
- Contact and support forms wired to Express backend via TanStack Query.
- All pages render meaningful content matching original business information.
- Version bumped to `0.2.0` for Phase 2 baseline.

### Changed
- Replaced hash-based routing with React Router 7.
- Standardized layout patterns across all pages (page hero, main content, sidebar, CTA band).
- Updated package.json, PROJECT.md, OVERHAUL_PLAN.md, HISTORY.md to reflect Phase 2 status.
- Overhaul plan updated to note Phase 2 scope and goals.

### Verified
- `npm run build` passes.
- All routes respond correctly.
- Contact form submits via `/api/leads`.
- Support form submits via `/api/support`.
- No references to legacy `styles.css` remain in source code.

## v0.1.0 — Phase 1 Foundation — 2026-05-12

### Added
- Rebuilt project foundation on Vite + React SPA with React Router.
- Added Tailwind CSS with Queue North light-first business palette.
- Added shadcn/ui-style local primitives for buttons, cards, inputs, textarea, select, badge, sheet, and dialog usage.
- Added Sonner toast support and TanStack Query provider/API helper.
- Added Express backend with `/api/health`, `/api/leads`, and `/api/support`.
- Added better-sqlite3 storage for `leads` and `support_requests`.
- Added all planned frontend routes for home, about, services, service details, industries, industry details, 8x8, contact, and support.
- Added Phase 1 documentation, build summary, script reference, and phase-based versioning rules.

### Changed
- Replaced the static HTML/CSS/JS entry with the Vite React entry.
- Updated README to point to `OVERHAUL_PLAN.md` as the design source of truth.
- Standardized versioning so Phase 1 uses `0.1.x`, Phase 2 uses `0.2.x`, and later phases follow the same pattern.
- Added Bishop verification rules and the requirement that Ripley pushes to `dev` after each verified phase.

### Verified
- `npm run build` passes.
- Backend health endpoint responds successfully at `/api/health`.
- Required routes are configured.
- Contact and support API paths exist and write through SQLite.

## v0.0.1 — Project Initialization — 2026-05-11

### Added
- Project initialized with PROJECT.md, STRUCTURE.md, FUTURE.md, HISTORY.md, DEVELOPMENT_LOG.md.

---

## From `DEVELOPMENT_LOG.md` — the agent-activity log, verbatim

> Migrated 2026-08-18. Newest first, as written. Not edited. Its entries
> overlap the changelog above in date but not in content: that file recorded
> what shipped, this one recorded what each agent did and verified.


## v0.6.0 — Navigation Active State Styling (Issue #38) — 2026-05-17

**Scarlett** — Nav Active-State Styling Implementation

**Status**: Already implemented in Header.jsx (no changes required)

**Implementation Details**:
- `useLocation` from `react-router-dom` v7.1.3 is properly imported
- `isActive(href)` helper compares `location.pathname === href`
- Desktop nav: Active links show `text-white underline underline-offset-4`, inactive use `text-white/70 hover:text-white`
- Mobile Sheet nav: Active links use `text-white font-semibold`, inactive use `text-white/70 hover:text-white`
- Sub-links (Services/Industries) use brand color `text-navy-light` (#68A3B8) for visual hierarchy

**Routes supported**: `/`, `/services`, `/industries`, `/about`, `/contact`, `/support` and sub-routes

**Notes**: Implementation was complete prior to this batch. All navigation links correctly indicate current page with clear visual feedback for both desktop and mobile views. No issues found.

---

## v0.4.8 — Error Handling Hardening + Dep Audit Batch — 2026-05-13

**Neo** — Server-side error handling hardening
- Added global error handlers:
  - `process.on('uncaughtException')`: logs error and exits with code 1
  - `process.on('unhandledRejection')`: logs error and exits with code 1
  - Both use `log.error()` for consistent timestamped logging
- Added 404 catch-all middleware for API routes:
  - Returns `{ error: 'Not found' }` for unmatched `/api/*` routes
  - Non-API routes fall through to SPA (express.static)
  - Logs warnings for API 404s
- Enhanced health check (`/api/health`):
  - Now verifies DB connection with `db.prepare('SELECT 1').get()`
  - Returns `{ status: 'ok', db: 'ok' }` on success
  - Returns 503 with `{ error: 'Service unavailable', db: 'error' }` on DB failure
- Added 30-second request timeout middleware:
  - Returns 504 with `{ error: 'Request timeout' }` for slow requests
  - Logs timeout events with `log.warn()`
- Verified consistent error response format:
  - 400: `{ error: string, fields: object }` (validation errors)
  - 404: `{ error: 'Not found' }`
  - 429: `{ error: 'Too Many Requests', message: string, retryAfter: number }`
  - 500: `{ error: string }` (no stack traces leaked)
  - 503: `{ error: 'Service unavailable', db: 'error' }`
  - 504: `{ error: 'Request timeout' }`
- Dependency audit: `npm audit` → 0 vulnerabilities
- Dependency review: `npm outdated` → Major updates noted (Express 5, Vite 8, Tailwind 4, etc.) but not upgraded per batch instructions
- All client-side code preserved (no changes to React components)

**Verification**
- Server starts successfully with all new middleware logged
- Build passes: `npm run build`
- Health endpoint returns DB status correctly
- API 404s return consistent JSON response
- Timeout middleware active (30s limit)

## v0.4.4 — Client-Side Validation + Sonner Feedback Batch — 2026-05-13

**Scarlett** — Client-side form validation implementation
- Added client-side validation to Contact.jsx:
  - Required field validation: company, name, email, message
  - Email format validation with regex
  - Inline error messages below each invalid field (red text, small)
  - Sonner toast for validation errors on submit attempt
  - Error clearing as user types (on change)
  - Red ring/border on Input/Textarea when field has validation error

- Added client-side validation to Support.jsx:
  - Required field validation: name, company, email, issue
  - Email format validation with regex
  - Issue minimum length validation (10 chars — matches server-side Zod rule)
  - Inline error messages below each invalid field
  - Sonner toast for validation errors on submit attempt
  - Error clearing as user types (on change)
  - Red ring/border on Input/Textarea when field has validation error

- Added loading state UI:
  - Form inputs disabled during submission (`mutation.isPending`)
  - Form opacity reduced to 70% during submission
  - Submit button already shows "Submitting..." text

**Verification**
- `npm run build` passes without errors
- Both forms show validation errors when submitting empty form
- Invalid email addresses trigger email field error
- Valid submissions trigger success toast
- Server errors trigger error toast
- Error fields clear when user corrects them

---

## v0.4.6 — Security Hardening Batch (Rate Limiting, Helmet, CORS) — 2026-05-13

**Private_Hudson** — Security hardening and environment variable configuration
- Installed security packages: `express-rate-limit`, `helmet`, `cors`
- Added rate limiting to `/api` endpoints:
  - 5 requests per minute (configurable via `RATE_LIMIT_PER_MINUTE`)
  - IPv6-safe key generator
  - Standard rate limit headers (`X-RateLimit-Remaining`, `X-RateLimit-Reset`)
  - 429 response with retry-after information
- Added Helmet security headers:
  - Content Security Policy (CSP) configured with strict default-src, script-src, style-src
  - HSTS enabled with 1-year max-age and includeSubDomains
  - Frameguard set to DENY
  - X-Content-Type-Options nosniff
  - X-Frame-Options DENY
  - X-XSS-Protection enabled
  - Referrer-Policy set to same-origin
  - Hide Powered-By header
- Added CORS configuration:
  - Configurable via `CORS_ORIGIN` env var (default: `*` for development)
  - Credentials enabled for authenticated requests
  - Exposed rate limit headers
- Updated server startup logging to show security config status
- Request body size limit (`1mb`) already in place

**Docker Configuration Updates**
- Added new environment variables to `docker-compose.yml`:
  - `RATE_LIMIT_PER_MINUTE=5`
  - `CORS_ORIGIN=https://queuenorth.com` (prod-safe default)
  - `LOG_LEVEL=info`
  - `ZOHO_ENABLED=false` (explicit off for non-CRM deployments)
  - `ZOHO_API_DOMAIN`, `ZOHO_CLIENT_ID`, `ZOHO_CLIENT_SECRET`, `ZOHO_REFRESH_TOKEN`, `ZOHO_REDIRECT_URI`
- Added environment defaults to `Dockerfile`:
  - All new env vars with reasonable defaults
  - `CORS_ORIGIN=\*` (development default)
  - `LOG_LEVEL=info`
  - `ZOHO_ENABLED=false`
- `docker-entrypoint.sh` verified — no changes required (env vars passed through automatically)

**Verification**
- `npm run build` passes without errors
- Server starts with security headers logged:
  - `[Security] Helmet enabled with CSP configured`
  - `[CORS] Enabled with origin: https://queuenorth.com`
  - `Rate limiting: 5 requests per 60 seconds`
- Rate limiting headers present on API responses
- All security headers confirmed via `curl -I http://localhost:3001/api/health`

---

## v0.4.5 — Server-Side Validation + Input Sanitization Batch — 2026-05-13

**Neo** — Server-side validation hardening and input sanitization
- Added request body size limit: `express.json({ limit: '1mb' })` — returns 413 for oversized payloads
- Added input sanitization after Zod validation (prevents XSS, trims whitespace, enforces max lengths at DB level):
  - `sanitizeString()` helper: trims whitespace, removes HTML/script tags, truncates to max length
  - `sanitizePayload()` applies sanitization to validated data before DB insert
- Updated Zod schemas with comprehensive validation:
  - All string fields: `.trim()` applied
  - All string fields: `.max()` limits applied per RFC standards:
    - `company`: 200 chars
    - `name`: 100 chars
    - `email`: 254 chars (RFC 5321)
- Updated error responses to match v0.4.6 error format standards:
  - 400: `{ error: 'Validation failed', fields: object }`
  - 500: `{ error: 'Failed to submit lead/support' }`

**Verification**
- `npm run build` passes without errors
- Zod validation correctly rejects invalid inputs
- Input sanitization trims whitespace and removes HTML tags
- Max length limits enforced at DB level

---

## v0.4.0 — Phase 4 Baseline — 2026-05-13

**Ripley** — Phase 4 initialization
- Updated `package.json` version to `0.4.0`
- Updated `PROJECT.md` to indicate Phase 4 active
- Updated `HISTORY.md` with v0.4.0 release notes
- Updated `FUTURE.md` with Phase 4 roadmap
- Updated `OVERHAUL_PLAN.md` with Phase 4 task breakdown

**Phase 4 Scope**
- v0.4.1-0.4.7: Incremental hardening batches (security, validation, error handling)
- v0.4.8: Final hardening batch (global error handlers, health check, dependency audit)
- Target: Production-ready, audited server with comprehensive error handling

---

## v0.0.1 — 2026-05-11

**Ripley** — Project initialized
- Created project directory at `/home/kaspa/.openclaw/Projects/Queue-North-Website/`.
- Set up initial PROJECT.md, STRUCTURE.md, FUTURE.md, HISTORY.md, DEVELOPMENT_LOG.md.
