From aa1d805e045f7ce70e55bec41b81eeac0f2eea64 Mon Sep 17 00:00:00 2001 From: Ripley Date: Tue, 18 Aug 2026 01:19:02 -0500 Subject: [PATCH] docs: adopt project template, retire the markdown backlog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitignore | 18 +- README.md | 248 +++-- docs/DOC_TRUST_MAP.md | 207 ++++ docs/OPERATIONS.md | 191 ++++ docs/TOOLS.md | 143 +++ docs/WORK_CYCLE.md | 277 ++++++ docs/architecture/GUARDS.md | 142 +++ docs/architecture/README.md | 182 ++++ docs/{ => architecture}/zoho-setup.md | 22 +- docs/data/README.md | 99 ++ docs/data/img/README.md | 57 ++ docs/data/img/icon.webp | Bin 0 -> 29016 bytes docs/data/img/logo.webp | Bin 0 -> 42486 bytes .../design/OVERHAUL_PLAN.md | 27 +- docs/design/README.md | 48 + review.md => docs/design/REDESIGN_REVIEW.md | 17 + docs/history/BATCH_LEDGER.md | 158 +++ docs/history/DEVELOPMENT_LOG.md | 918 ++++++++++++++++++ docs/history/HISTORY.md | 101 ++ docs/planning/PROJECT_PLAN.md | 118 +++ .../planning/REQUIREMENTS.md | 11 + docs/qa/ClaudeQACoverage.md | 77 ++ docs/qa/ClaudeQAPlan.md | 110 +++ docs/qa/ClaudeReport.md | 82 ++ docs/security/SECURITY.md | 159 +++ docs/security/SECURITY_CHECKLIST.md | 75 ++ main.js | 332 ------- test-results/.last-run.json | 4 - 28 files changed, 3400 insertions(+), 423 deletions(-) create mode 100644 docs/DOC_TRUST_MAP.md create mode 100644 docs/OPERATIONS.md create mode 100644 docs/TOOLS.md create mode 100644 docs/WORK_CYCLE.md create mode 100644 docs/architecture/GUARDS.md create mode 100644 docs/architecture/README.md rename docs/{ => architecture}/zoho-setup.md (86%) create mode 100644 docs/data/README.md create mode 100644 docs/data/img/README.md create mode 100644 docs/data/img/icon.webp create mode 100644 docs/data/img/logo.webp rename OVERHAUL_PLAN.md => docs/design/OVERHAUL_PLAN.md (94%) create mode 100644 docs/design/README.md rename review.md => docs/design/REDESIGN_REVIEW.md (87%) create mode 100644 docs/history/BATCH_LEDGER.md create mode 100644 docs/history/DEVELOPMENT_LOG.md create mode 100644 docs/history/HISTORY.md create mode 100644 docs/planning/PROJECT_PLAN.md rename project-requirements.md => docs/planning/REQUIREMENTS.md (81%) create mode 100644 docs/qa/ClaudeQACoverage.md create mode 100644 docs/qa/ClaudeQAPlan.md create mode 100644 docs/qa/ClaudeReport.md create mode 100644 docs/security/SECURITY.md create mode 100644 docs/security/SECURITY_CHECKLIST.md delete mode 100644 main.js delete mode 100644 test-results/.last-run.json diff --git a/.gitignore b/.gitignore index 174fd64..716aa8b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,14 +1,12 @@ -# Private project/agent docs — never commit -DEVELOPMENT_LOG.md -PROJECT.md -STRUCTURE.md -FUTURE.md -HISTORY.md -BUILD_SUMMARY.md -SCRIPTS.md -.drop/ +# Credentials and the original site drop — never commit, private repo or not. +# zoho.md holds the live reCAPTCHA secret key and the Zoho WebToLead tokens. +# See docs/security/SECURITY.md. zoho.md +.drop/ +# Client correspondence. Its asks are tracked as issues; the letter itself is not +# a doc-tree document. +Levi.md # Dependencies node_modules/ @@ -20,6 +18,7 @@ dist-ssr/ # Runtime/database artifacts db/*.db db/*.db-* +test-results/ # Environment/local files .env @@ -38,6 +37,5 @@ pnpm-debug.log* .vscode/ .idea/ .learnings/ -Levi.md Queue-North-Website.code-workspace Working Site.zip diff --git a/README.md b/README.md index d2704f0..389463e 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,21 @@ # Queue North Website +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: package.json, src/routes.jsx, Dockerfile, docker-compose.yml — the + project-facing overview, whose stack, route list and Docker story + all describe those files +Review trigger: Any change to the stack, the route list, the Docker or + deployment story, the agent pipeline, or where a document lives +``` + +The business website for Queue North Technologies — an 8x8 Certified Partner +delivering UCaaS, contact centre, networking and managed support for SMB and +enterprise organisations. Live at +[qn.isnull.dev](https://qn.isnull.dev). + ## Objective Queue North Website is the modern rebuild of the Queue North Technologies business website. @@ -65,86 +81,73 @@ Primary structure: - Dark navy conversion band - Consultation-focused message -## Planned Routes +## Routes ```txt -/ -/about -/services -/services/unified-communications -/services/contact-center -/services/managed-support -/services/consulting-training -/services/infrastructure-cabling -/services/wireless-access -/services/local-networking -/industries -/industries/healthcare -/industries/retail -/industries/manufacturing -/industries/education-finance -/8x8 -/contact -/support +/ Home +/about About +/services Services index +/services/:slug 7 service detail pages +/industries Industries index +/industries/:slug 4 industry detail pages +/contact Contact form → POST /api/leads +/support Support form → POST /api/support +/privacy-policy Privacy policy +* 404 ``` -## Overhaul Phases +Defined in `src/routes.jsx`. **Every one of them is prerendered to static HTML at +build time** by `scripts/prerender.js` — the SPA hydrates on top. `/privacy-policy` +additionally has a server-rendered fallback because Meta's crawler does not run +JavaScript. -Version numbers correlate directly to the active phase: +The standalone `/8x8` route was removed at `0.6.6`; that content now lives inside +the UCaaS and contact-centre service pages. -- **Phase 1 — Stack Scaffold**: `0.1.x` ✅ Complete - - ~~Vite + React app foundation~~ ✅ - - ~~Tailwind CSS setup~~ ✅ - - ~~shadcn/ui-style primitives~~ ✅ - - ~~React Router~~ ✅ - - ~~Express backend~~ ✅ - - ~~better-sqlite3 database~~ ✅ - - ~~Initial API health/contact/support paths~~ ✅ +## Where the work is -- **Phase 2 — Layout Rebuild**: `0.2.x` ✅ Complete - - ~~App shell: Header, Footer, layout wrapper, mobile nav~~ ✅ - - ~~Route pages fully built and navigable~~ ✅ - - ~~Existing business content ported into React~~ ✅ - - ~~Repeated service/industry content moved into data files~~ ✅ - - ~~Static hash routing fully replaced by React Router~~ ✅ +**Open work lives in the issue tracker, not in this file.** Milestones are +batches, issues are deliverables. There used to be a phase checklist here and in +four other documents; by the time it was removed it said *"Phase 5 — In +Progress"* while the last four commits said *batch 0.9.3*. That is the second +record this convention exists to remove. -- **Phase 3 — Visual Overhaul**: `0.3.x` ✅ Complete - - ~~Modern light-first business design~~ ✅ - - ~~Tailwind theme polish~~ ✅ - - ~~Typography, spacing, radius, shadows, and responsive rhythm~~ ✅ - - ~~Refined service/industry cards and CTA sections~~ ✅ - - ~~Mobile-first layout polish~~ ✅ +- **What is left, and what is next:** the milestones at + [dream.scheller.ltd/null/Queue-North-Website](https://dream.scheller.ltd/null/Queue-North-Website/milestones) +- **What is broken:** issues labelled `P0`, `P1`, `P2` +- **What the batches were, before the tracker held them:** + [docs/history/BATCH_LEDGER.md](docs/history/BATCH_LEDGER.md) -- **Phase 4 — Forms + Backend Hardening**: `0.4.x` ✅ Complete - - ~~Contact and support forms fully wired to Express~~ ✅ - - ~~SQLite persistence verified~~ ✅ - - ~~Client-side validation + Sonner feedback~~ ✅ - - ~~Server-side validation + input sanitization~~ ✅ - - ~~Optional Zoho forwarding layer~~ ✅ - - ~~Rate limiting + security headers + CORS~~ ✅ - - ~~Backend/API hardening as needed~~ ✅ +The rebuild itself is done: phases 1 through 10 completed between May and August +2026, from the original static HTML site to the current Vite + React SPA with a +prerendered build. `docs/history/DEVELOPMENT_LOG.md` has it version by version. -- **Phase 5 — Verification + Redesign**: `0.5.x` 🔄 In Progress - - ~~SPA router fix (BrowserRouter → RouterProvider)~~ ✅ - - ~~TS generics stripped from .jsx files~~ ✅ - - ~~Mobile menu Sheet/Dialog fix~~ ✅ - - ~~DialogTitle accessibility fix~~ ✅ - - ~~SPA catch-all route for client-side navigation~~ ✅ - - ~~Image assets copied to public/ (were 404)~~ ✅ - - ~~Real Queue North logo replacing placeholder~~ ✅ - - ~~CSP updated for Google Fonts~~ ✅ - - ~~Hamburger menu + SheetContent CSS fix~~ ✅ - - ~~tailwindcss-animate installed and configured~~ ✅ - - Hero section rewrite — B2B clarity, 8x8 partnership prominence - - Trust signals section — metrics, badges, certifications - - Services rewrite — business outcomes over technical jargon - - Why Queue North refinement — concrete differentiators - - Footer + CTA pass — contact paths everywhere - - Remaining P0/P1 audit fixes (Zoho, su-exec, email constraint) - - Accessibility checks - - Final push to `dev` for the completed phase +### Where the tracker is -Patch versions increment for completed task batches after the full pipeline finishes. Dispatch a task batch, run it through the required agents, then push that completed batch once. Example: Docker task batch goes through Neo → Private Hudson → Bishop → Ripley, then pushes as `0.2.1`. Notes/tags should use the version number only. +**[dream.scheller.ltd](https://dream.scheller.ltd)** — a Forgejo instance, and +naming it matters. The convention is portable; the *reporting* is not. Every +figure on this project's card at [privacyllc.dev](https://privacyllc.dev) is read +from this Forgejo repository. + +Credentials are in `~/.openclaw/docker-registry.env`: + +```bash +set -a; . ~/.openclaw/docker-registry.env; set +a +python3 scripts/forgejo-issue.py list +``` + +Despite its name that token is not registry-scoped — it works across the whole +API, with push and pull. Never print it into a log or a chat. + +Two traps that cost an hour each otherwise. **Cloudflare fronts the instance and +1010-blocks clients that do not look like a browser or curl** — Python's `urllib` +among them — so every request needs `User-Agent: curl/8.5.0`; +`scripts/forgejo-issue.py` already sends it and anything new must too. And +**`/issues` returns pull requests too** unless `type=issues` is passed. + +**Labels are exactly `P0`, `P1`, `P2`, `release-blocker`.** They are queried by +name. A fifth label, `P3`, exists on 21 closed issues from before adoption and is +frozen — nothing new gets it, because nothing counts it. ## Backend Goals @@ -176,9 +179,19 @@ The overhaul is handled through the agent pipeline below: Agents do not touch git. Ripley owns all commits, tags, and pushes. +This pipeline says **who does which part**. `docs/WORK_CYCLE.md` says **what must +be true before a piece of work counts as finished** — closing the issue with its +evidence, closing the milestone if the batch landed, updating the documents the +change triggered in the same commit, and recording the next action. They are +different questions and both apply. + +`.learnings/` holds each agent's accumulated notes and error logs. It is +gitignored and local to this machine. + ## Batch Pipeline Rule -Work is dispatched as task batches. A batch runs through the required agents, then Ripley pushes that completed batch once. +Work is dispatched as task batches. A batch runs through the required agents, +then Ripley pushes that completed batch once. Example Docker batch: @@ -186,15 +199,26 @@ Example Docker batch: Neo → Private Hudson → Bishop → Ripley ``` -The whole Docker batch is one checkpoint: `0.2.1`. +The whole batch is one push, not one push per agent. -Do not increment the patch version for each individual agent inside the same batch. Increment only after the full task batch finishes and is ready to push. +**A batch is a milestone in the tracker.** Closing the last issue under it does +not close the milestone, and a complete-but-open milestone reads as a batch still +in progress — closing it is what marks the batch *shipped*, and it moves a figure +on the project card the same day. -Notes, tags, and checkpoint labels should use only the version number, such as `0.2.1`. +> **Version numbers no longer encode the phase.** Until 2026-08-18 the rule was +> that the minor version *was* the phase number — Phase 5 releases are `0.5.x` — +> with Bishop responsible for keeping `package.json` in agreement with two +> root-level documents — PROJECT.md and HISTORY.md, named here without backticks +> because both were retired on 2026-08-18 and a backticked path is a claim that +> a file exists. It drifted twice; the second time `package.json` said `0.8.3` +> for two and a half months while commits said *batch 0.9.3*. The tracker holds +> the batch now, and +> [docs/history/HISTORY.md](docs/history/HISTORY.md) has the full reasoning. ## Design Direction -Based on the redesign review (see `review.md`), the site should feel: +Based on the redesign review (see [docs/design/REDESIGN_REVIEW.md](docs/design/REDESIGN_REVIEW.md)), the site should feel: - **Modern, clean, stable** — not experimental, not hacker aesthetic - **Business-first** — B2B UCaaS/IT partner, not a dev portfolio @@ -208,7 +232,7 @@ Color palette evolution (not rip-and-replace): Reference brands: RingCentral, Cloudflare, Dialpad — modern but enterprise-trustworthy. -See [review.md](./review.md) for the full redesign assessment. +See [docs/design/REDESIGN_REVIEW.md](docs/design/REDESIGN_REVIEW.md) for the full redesign assessment. ## Docker Deployment @@ -291,3 +315,77 @@ The `.dockerignore` excludes: - `.git`, logs, private docs This ensures minimal image size and reproducible builds. + +## Run Locally + +```bash +npm install +git config core.hooksPath .githooks # per clone — see below +npm run dev # Vite on 5173, Express on 3001 +``` + +`npm run build` is the real gate: client bundle, SSR bundle, then prerender. +`docs/TOOLS.md` has every script and which ones can stop you. + +### The git hooks + +Three hooks live in `.githooks/` because `.git/hooks` is not versioned. **The +`core.hooksPath` line above is per clone**, so every checkout runs it once; an +uninstalled hook fails silently. + +| Hook | What it does here | +| --- | --- | +| `pre-commit` | scans the staged diff for credentials, then runs `npm run build` if source is staged | +| `commit-msg` | refuses a message with no conventional type (`feat`, `fix`, `ui`, `docs`, `test`, `refactor`, `security`, `perf`, `chore`) | +| `post-commit` | **pushes to `origin`** | + +**`post-commit` pushes.** That is deliberate — work that exists on one laptop is +one disk away from gone — but it has a consequence: whatever documentation was +not in that commit is now behind the code by one push. That is the mechanical +reason documentation edits ride in the same commit as the change that caused +them. `SKIP_GUARDS=1` and `SKIP_PUSH=1` are the escape hatches, both loud. + +## Project Docs + +`README.md` is the landing page. Detailed procedures belong in `docs/`, and open +work belongs in the tracker. + +**[docs/DOC_TRUST_MAP.md](docs/DOC_TRUST_MAP.md) is the entry point** — read it +before trusting anything else in `docs/`. + +| Doc | Purpose | +| --- | --- | +| [docs/DOC_TRUST_MAP.md](docs/DOC_TRUST_MAP.md) | Which document owns which answer, and which source wins when records disagree | +| [docs/WORK_CYCLE.md](docs/WORK_CYCLE.md) | What happens at the end of a piece of work, and what privacyllc.dev will and will not update on its own | +| [docs/TOOLS.md](docs/TOOLS.md) | Where the scripts are, which ones can stop you, and this project's npm commands | +| [docs/OPERATIONS.md](docs/OPERATIONS.md) | What is deployed and where, what watches it, and what to run when it is down | +| [docs/planning/PROJECT_PLAN.md](docs/planning/PROJECT_PLAN.md) | Scope, audience, and what this project deliberately is not | +| [docs/planning/REQUIREMENTS.md](docs/planning/REQUIREMENTS.md) | Engineering standards. Not changeable without `_null`'s approval | +| [docs/architecture/README.md](docs/architecture/README.md) | Module boundaries, the database schema, every API response shape | +| [docs/architecture/GUARDS.md](docs/architecture/GUARDS.md) | How to write a check that can actually fail | +| [docs/architecture/zoho-setup.md](docs/architecture/zoho-setup.md) | The CRM integration, end to end | +| [docs/design/OVERHAUL_PLAN.md](docs/design/OVERHAUL_PLAN.md) | The visual system: tokens, type scale, spacing, layout blueprints | +| [docs/design/REDESIGN_REVIEW.md](docs/design/REDESIGN_REVIEW.md) | Brand positioning and the messaging standard | +| [docs/security/SECURITY.md](docs/security/SECURITY.md) | Threat model, secrets, third parties, what is out of scope | +| [docs/security/SECURITY_CHECKLIST.md](docs/security/SECURITY_CHECKLIST.md) | The list to work through before a release | +| [docs/qa/ClaudeQAPlan.md](docs/qa/ClaudeQAPlan.md) | What a QA round consists of | +| [docs/qa/ClaudeReport.md](docs/qa/ClaudeReport.md) | The QA verdict — quoted verbatim on the project card | +| [docs/qa/ClaudeQACoverage.md](docs/qa/ClaudeQACoverage.md) | What each pass actually reached, and the standing gaps | +| [docs/history/DEVELOPMENT_LOG.md](docs/history/DEVELOPMENT_LOG.md) | Dated, append-only: what happened and when | +| [docs/history/HISTORY.md](docs/history/HISTORY.md) | The narrative: decisions, and what was tried and dropped | +| [docs/history/BATCH_LEDGER.md](docs/history/BATCH_LEDGER.md) | Archived. What the batches were before the tracker held them | + +## Agent Notes + +- Product truth comes from the code and the tracker before prose. +- **Do not keep a work list in this README**, or in any markdown file. That is + what the tracker is for, and this project has already paid for the lesson. +- Finish with [docs/WORK_CYCLE.md](docs/WORK_CYCLE.md), every time: close the + issues you finished with the evidence that proves it, close the milestone if + the batch landed, update the documents the change triggered **in the same + commit**, then record the next action and any blockers before you stop. +- Nothing on privacyllc.dev writes itself except the tracker counts and the + pushed docs. The summary and next action change only when they are sent; the + milestone and blocker tables there change only when a human edits them. +- Do not claim a feature is built unless you can cite the route, file, or + screenshot that proves it. **There is no test suite to cite.** diff --git a/docs/DOC_TRUST_MAP.md b/docs/DOC_TRUST_MAP.md new file mode 100644 index 0000000..6430821 --- /dev/null +++ b/docs/DOC_TRUST_MAP.md @@ -0,0 +1,207 @@ +# Doc Trust Map — which document to believe + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: docs/** +Review trigger: Any doc added, deleted or moved; any change to which doc owns a subject +Fires on: added, deleted, moved +``` + +This describes what is **actually** in this repository, written after everything +else was settled. Where it differs from the template it came from, this one is +right about this project. + +## Work items are not in this tree + +The sequence of work, the open defects, and the things blocking a release all +live in this repository's **issue tracker**, not in `docs/`. Milestones are +batches; issues are deliverables; severity labels are `P0`, `P1`, `P2` and +`release-blocker`. + +This section exists to stop the next contributor starting a fresh markdown +to-do list. **It has already happened here.** Before 2026-08-18 this project +kept its work in `FUTURE.md`, a changelog in `HISTORY.md`, an agent log in +`DEVELOPMENT_LOG.md`, and phase checklists in `PROJECT.md`, `STRUCTURE.md` and +`README.md` — six records, beside 205 issues in a tracker that none of them +pointed at. Two of those documents still described the project as being in +"Phase 5" while the code was at `0.9.3`, and nothing anywhere said so. + +| Question | Answer lives in | +| --- | --- | +| What are we building, and for whom? | `docs/planning/PROJECT_PLAN.md` | +| What standards does the code have to meet? | `docs/planning/REQUIREMENTS.md` — and they need `_null`'s approval to change | +| What is the sequence of work? | milestones in the tracker | +| What is left in this batch? | open issues under that milestone | +| What is broken right now? | issues labelled `P0` / `P1` / `P2` | +| What makes a release wrong? | issues labelled `release-blocker` | +| What is the next action? | the `nextAction` field on the project at privacyllc.dev — the newest entry in `docs/history/DEVELOPMENT_LOG.md` says what it was *then* | +| What is blocking us? | the tracker, for the work; the blockers table at privacyllc.dev, for the stakeholder-facing version | +| What do I do when a piece of work is finished? | `docs/WORK_CYCLE.md` | +| What is deployed, and what do I do when it breaks? | `docs/OPERATIONS.md` | +| What happened, and when? | `docs/history/DEVELOPMENT_LOG.md` | +| Why is it shaped like this? | `docs/history/HISTORY.md` — decisions, and what was tried and dropped | +| What were the batches, before the tracker? | `docs/history/BATCH_LEDGER.md` — archived, must not be updated | +| Did QA pass, and what does the tester think? | `docs/qa/ClaudeReport.md` | +| What did QA actually reach? | `docs/qa/ClaudeQACoverage.md` — and here, the answer is *nothing yet* | +| How is it built? | `docs/architecture/README.md` | +| How does the CRM integration work? | `docs/architecture/zoho-setup.md` | +| What should it look like? | `docs/design/OVERHAUL_PLAN.md` | +| What should it sound like? | `docs/design/REDESIGN_REVIEW.md` | +| Which script do I run, and can it stop me? | `docs/TOOLS.md` — the signpost; `docs/architecture/README.md` has the table | +| What runs before a commit? | `.githooks/` | +| How do I write a check that will actually catch something? | `docs/architecture/GUARDS.md` | + +**Next action and blockers are recorded at the end of every piece of work, not +when somebody asks.** `docs/WORK_CYCLE.md` holds that procedure and the reason +each step is in it. + +## Folder layout + +| Folder | Contents | +| --- | --- | +| `docs/planning/` | `PROJECT_PLAN` — the vision. `REQUIREMENTS` — the engineering standards. Not the schedule; that is the tracker | +| `docs/qa/` | `ClaudeQAPlan` (playbook), `ClaudeQACoverage` (what each pass reached), `ClaudeReport` (the verdict) | +| `docs/architecture/` | `README` — boundaries, schema, response shapes, the script table. `GUARDS` — how to write a check. `zoho-setup` — the CRM manual | +| `docs/design/` | `OVERHAUL_PLAN` — the visual system. `REDESIGN_REVIEW` — positioning and tone | +| `docs/security/` | `SECURITY` — threat model and third parties. `SECURITY_CHECKLIST` — the pre-release list | +| `docs/history/` | `DEVELOPMENT_LOG` (dated, append-only), `HISTORY` (the narrative), `BATCH_LEDGER` (archived) | +| `docs/data/` | the branding marks privacyllc.dev renders for this project | +| `docs/` root | this map; `WORK_CYCLE`; `TOOLS`; `OPERATIONS` | + +`README.md` stays at the repository root; it is the landing page and moving it +breaks that. Everything else lives under `docs/`. + +## What this project keeps that the template does not describe + +- **`docs/planning/REQUIREMENTS.md`** — engineering standards with a change + policy attached. It is not the plan and not the architecture; it is the set of + rules every agent works to, and it predates the template. Kept whole. +- **`docs/design/OVERHAUL_PLAN.md`** — 25 KB, and **two documents in one file**. + Its design brief onward is the live token and layout authority; its earlier + migration-phase and agent-plan sections describe a rebuild that finished. Its + own front-note says which half is which. Splitting it would have risked losing + prose that nothing else records, so it was not split. +- **`docs/architecture/zoho-setup.md`** — written for a non-technical reader. + Note the trap in its own header: it documents the OAuth path, and production + runs WebToLead. +- **The agent pipeline** — Scarlett, Neo, Private Hudson, Bishop, Ripley — is in + `README.md`, not in this tree, because it is how work is dispatched rather + than a subject anything is authoritative about. `.learnings/` holds four + agents' notes and is gitignored. + +## What the template carries that this project deleted + +Recorded so a reader can tell a deliberate absence from an oversight. + +| Deleted | Why | +| --- | --- | +| `ClaudeQAPlan.md` pass I — money flowing backwards | no money moves through this site. A pass that never applies is noise | +| `SECURITY_CHECKLIST.md` — the authorisation group, and the session-token row | there is no login, no session, no role and no per-user data anywhere in the product | +| `SECURITY.md` — the *(precautionary)* bring-your-own-credential paragraph | **a precautionary rule, dropped deliberately.** It describes holding credentials on behalf of users; there are no users. `PROJECT_PLAN.md` records accounts as out of scope, which is what makes it safe to drop | + +Every other *(precautionary)* marker was kept. `OPERATIONS.md` was kept in full — +this is a deployed service with a live origin, a named host and a database. + +**Pass H was kept and rewritten** rather than deleted. Its authorisation half +does not apply; its *what a stranger can reach and exhaust* half is the most +exposed surface this project has. + +## A fifth severity label exists, and is frozen + +`P3` sits on 21 closed issues from before this repository adopted the +convention. It is **not** one of the four names the Command Center queries, so a +defect filed `P3` today would be counted by nothing. + +It was kept rather than deleted because removing it would erase the severity +record on those 21 issues, and merged into `P2` would misstate them. **Nothing +new gets it.** New defects use `P0`, `P1`, `P2` or `release-blocker`. + +## Source-of-truth ladder + +When two sources disagree, believe them in this order: + +1. **Verified code behaviour** — read the source. Note that on this project + "run the tests" is not available: there are none +2. **The issue tracker** — for anything about state of work +3. **Specialist docs** — authoritative for their own subject only +4. **History and logs** — a record of *then*, not a description of *now* + +A document that describes work as done is describing what was true when it was +written. The tracker is describing now. + +## The status header + +Every document in this tree opens with one, immediately after its H1 — and so +do the documents at the repository root, which `scripts/doc-triggers.py` reads +alongside this tree. The root is read one level deep only. + +``` +Status: Current | Draft | Superseded | Archived +Owner: +Last reviewed: +Governs: +Review trigger: +Fires on: +``` + +**Exactly those four status words.** A document whose status falls outside the +list is reported as having an unknown one rather than being quietly accepted. +`Historical` is the word this convention used to have and deliberately dropped — +`Archived` covers it. + +**Review trigger** is the line that matters. "Last reviewed" ages on its own and +a reader cannot tell a current document from an abandoned one by looking at it; +a trigger names the event that should bring somebody back. A header carrying +`Status` without `Review trigger` is reported as incomplete — that combination +looks finished and is not. + +**Governs** is a comma-separated list, and an entry may explain itself after the +glob with a spaced dash — `docs/data/** — the assets privacyllc.dev renders`. +`doc-triggers.py` cuts the entry there and reads the globs from the left of it. +**Use that form and no other.** A gloss in parentheses, or after a colon, is not +recognised: the whole entry becomes the glob, matches nothing, and the document +is silently never fired. + +**Fires on** is optional and only for the case where `Governs:` is much broader +than the trigger. This file is the extreme of that gap — it governs `docs/**`, +the broadest glob here, while its trigger is one of the narrowest. Omit the line +unless it is genuinely needed; absent means fire on every kind. + +`scripts/verify.d/30-doc-headers` checks all of this on every `npm run verify`, +which is what stops it being a convention people remember. + +## Declaring a document deliberately absent + +A repository may decide it will not keep one of these documents in git. Say so +**here**, one line per path, anywhere in this file: + +``` +Exempt: +``` + +**Write the real path only when you mean it.** Outside a fenced block, an +`Exempt:` line naming a real document is not an example — it is a declaration. +Keep illustrations fenced, and use placeholders anyway, as the form above does. + +**This project exempts nothing.** It considered it: the internal history and +planning documents were gitignored with the comment *"never commit"*, and +exempting them was one of the three options on the table. Instead the Forgejo +repository was made **private** on 2026-08-18 and the documents were committed, +because a half-populated doc tree reports to privacyllc.dev as a half-adopted +project and the reason for the secrecy was the audience, not the content. + +**`docs/data/` and `docs/data/img/` cannot be exempted.** A declaration naming +either is refused rather than honoured, because the Command Center renders what +is in them. + +## One asset is genuinely missing, and that is not an exemption + +`docs/data/img/banner.webp` does not exist. `icon.webp` and `logo.webp` do, both +converted from this project's own marks in `assets/`. + +No placeholder was generated, deliberately — a placeholder that looks finished +outlives the issue that would have replaced it. It is filed as a `P2` issue +instead, and the project card will render without a banner until somebody draws +one. diff --git a/docs/OPERATIONS.md b/docs/OPERATIONS.md new file mode 100644 index 0000000..9469c86 --- /dev/null +++ b/docs/OPERATIONS.md @@ -0,0 +1,191 @@ +# Operations — Queue North Website + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: Dockerfile, docker-compose.yml, scripts/status.sh, + scripts/healthcheck.sh, scripts/backup.sh, scripts/restore-check.sh — + what watches this in production, and what to do when it stops +Review trigger: A new alert or a new place errors are sent; a change to the + backup schedule or the restore procedure; any change to the + deploy host, container or image; any incident whose first ten + minutes were spent working out what to run +``` + +## Why this exists + +Every other document here describes what the project *is*. This one describes +what happens when it stops being that at three in the morning, and it is the +only document written for somebody who is not thinking clearly. + +The failure it prevents is specific: **the first ten minutes of an incident +spent working out which host, which container, and which command**, while the +thing everybody actually wants to know — is it down, or is it just unreachable +from here — goes unanswered. + +## What is deployed, and where + +| | | +| --- | --- | +| Public origin | `https://qn.isnull.dev`, fronted by Cloudflare | +| Host | **nebula** (`192.168.1.11`) — the same box as the Forgejo instance | +| Container | `qn-website-dev` | +| Image | `dream.scheller.ltd/null/queue-north-website:dev` | +| Port | 3001 in the container; the reverse proxy in front owns the public one | +| Database | `/app/db/queuenorth.db`, in the named volume `qn-website-dev_queuenorth-db` | +| Logs volume | `qn-website-dev_queuenorth-logs` → `/app/logs` | +| Runs as | UID 1001, non-root | + +`queuenorth.com` also resolves and returns 200. **It is not this deployment** — +it is the customer's own domain and is not served by this container. Do not +diagnose against it. + +## Where errors go + +**Nowhere. This project has no error tracking.** + +Said in one line rather than left blank, because a gap somebody chose is a +different thing from a gap nobody noticed. There is no Sentry project, no DSN and +no alert destination. An unhandled exception is logged to the container's stdout +and takes the process down: `server/index.js` handles `uncaughtException` and +`unhandledRejection` by logging and exiting 1, and Docker's `restart: +unless-stopped` brings it back. **That means a crash loop looks like a running +service to anything that only reads a state.** `scripts/status.sh` reports the +restart count for exactly this reason. + +**A liveness tick is not error tracking.** `scripts/healthcheck.sh` answers *is +it up*. Nothing here answers *is it working* — a server returning 500 to every +form submission is up, healthy, and losing every lead. + +## What alerts, and to whom + +| Signal | Where it goes | Who acts on it | +| --- | --- | --- | +| Container `HEALTHCHECK` fails 3× at 30 s | Docker marks the container unhealthy on nebula | **Nobody, automatically.** It is visible to anyone who looks and notifies no one | +| `scripts/healthcheck.sh` | whoever runs it | not scheduled | +| Everything else | — | — | + +**This table is nearly empty and that is the honest state.** Nothing on this +project pages anybody. The site could be down for a day and the first report +would come from the customer. + +Two things would change that and neither is filed as work yet: scheduling +`healthcheck.sh` somewhere that can shout, and giving the process somewhere to +send an exception. Recorded here rather than invented into the tracker. + +## Backups + +| | | +| --- | --- | +| Schedule | **None yet.** `scripts/backup.sh` works and nothing runs it on a timer | +| What is captured | `/app/db/queuenorth.db` — the `leads` and `support_requests` tables. Deliberately not captured: the logs volume, and the environment, which holds the secrets | +| Where it lands | `$HOME/backups/queue-north-website` on the operator's machine, mode 700. **Still one machine** — see below | +| Retention | `BACKUP_KEEP`, default 7 | +| **Last verified restore** | **2026-08-18, by `_null` — 2 tables, 3 rows, restored in under 1 s** | + +`scripts/backup.sh` proves a dump is readable before trusting it — it runs +better-sqlite3's online `.backup()` inside the container, copies the result out, +and refuses the run unless `PRAGMA integrity_check` returns `ok` and the table +count is plausible, only then renaming it into place. `scripts/restore-check.sh` +proves it can be restored, which is a different claim: a file that parses is not +a database you can get back. + +**A backup nobody has restored is a guess.** The date above is the only line in +this table that says otherwise, and as of 2026-08-18 it says something: a +snapshot was taken from the running container, replayed into a scratch database +from SQL, and counted. It came back. + +**Two things that are still true anyway**, and both are tracked in `Batch 15`: + +1. **Nothing runs on a schedule.** One backup taken by hand is not a backup + regime. Until a timer exists, the newest dump is as old as the last time + somebody remembered. +2. **The dump lives on one machine, beside nothing.** `backup.sh` says this + itself on every run: *a backup that only exists beside the database it came + from does not survive the disk, the host or the account.* It is currently on + the operator's workstation and nowhere else. + +The exposure is worth stating plainly: `/app/db/queuenorth.db` is the only live +copy of every lead and support request the site has ever taken, it lives in one +Docker volume on one host, and it took writes today. + +**A number worth knowing before you need it:** the restore took under a second, +because the database is 28 KB and holds three leads. That will stay true for a +long time at this volume — this is a contact form, not a transactional system — +so during an incident, restoring is cheap and there is no reason to hesitate over +it. + +`RESTORE_MIN_ROWS` is currently 0, which means the check cannot yet catch the +snapshot-of-an-empty-volume case. Set it to something below the real lead count +once that count is meaningful, and it starts catching the one failure a +structural check never can. + +## Rate limits and cost ceilings + +*(precautionary — no incident here has yet come from this.)* + +| Endpoint or job | What it costs per call | What bounds it | +| --- | --- | --- | +| `POST /api/leads` | one SQLite insert, one reCAPTCHA verify, one Zoho form post | `RATE_LIMIT_PER_MINUTE`, default 5, per IP across all of `/api`. Body capped at 1 MB, request at 30 s | +| `POST /api/support` | the same, plus a Zoho Case when enabled | same limiter | +| Google reCAPTCHA | free at this volume | the same limiter, upstream of it | +| Zoho CRM | free at this volume; the API path has per-org daily credits | fire-and-forget, so exhausting them degrades CRM sync and never the site | + +**Where does the spend alert go, and at what number?** Nowhere, and there is no +number. Nothing here bills per call at this volume, which is why that is +tolerable rather than an oversight — but if an LLM, a mail sender or a paid API +is ever added, this table is the first thing that has to change. + +## It is down — what now + +In this order. Each step is a command that answers one question, and none of them +change anything. + +1. **Is it actually down, or unreachable from here?** + `bash scripts/healthcheck.sh` from anywhere, then + `bash scripts/status.sh`. The second distinguishes those two and reports the + restart count. **A climbing restart count with a short uptime is a crash loop**, + not a healthy service, and reads as "running" in every tool that shows only a + state. +2. **What is it saying?** `bash scripts/status.sh --logs 200`. +3. **Is the deployed version the one you think?** `bash scripts/status.sh` + prints the image digest. **`--deployed-version` cannot answer here** — the + image carries no `org.opencontainers.image.version` label, so the script + correctly reports the version as unknown rather than guessing. Compare the + **digest** against the registry instead: + + ```bash + bash scripts/status.sh | grep digest + ``` + + Adding that label at build time would make this step a one-liner and is worth + doing next time the Dockerfile is touched. +4. **Is it the database?** `/api/health` returns 503 with `db: error` when the + `SELECT 1` fails. That is a real answer and not an outage of the whole site: + the marketing pages keep serving while every form submission is failing. +5. **Did something change recently?** The tracker and `git log` — a deploy, a + config edit, a Cloudflare rule. +6. **Is it Cloudflare rather than us?** Compare a request to the origin on nebula + against one to `qn.isnull.dev`. Cloudflare fronts everything here, including + the Forgejo instance on the same host. + +### Rolling forward or back + +**This is the gap in this runbook and it is named rather than papered over.** + +Publishing is `npm run docker:push`, which builds and pushes +`queue-north-website:dev` to the Forgejo registry. How nebula then *moves* to the +new image — a compose file on the host, a Portainer stack, a watchtower, a manual +`docker pull && docker compose up -d` — is not written down anywhere in this +repository, and was not recoverable from it on 2026-08-18. + +Until somebody writes it here, a rollback is: find the previous image digest in +the registry, and do on nebula whatever it is that normally happens. That is not +a procedure. The template's `deploy.py` was deliberately **not** adopted for this +reason — a deploy script pointed at a path nobody has confirmed is worse than no +deploy script. + +**Write the incident down afterwards**, in `docs/history/DEVELOPMENT_LOG.md` with +the date, and file what broke as an issue with a severity label. An incident +nobody recorded happens again with the same surprise. diff --git a/docs/TOOLS.md b/docs/TOOLS.md new file mode 100644 index 0000000..02defcd --- /dev/null +++ b/docs/TOOLS.md @@ -0,0 +1,143 @@ +# Tools — where the scripts are, and which ones can stop you + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: scripts/**, .githooks/**, package.json — the tooling, and which of + it can stop you +Review trigger: Any script added to, removed from or repurposed in scripts/; any + change to which of them gates; any change to the npm scripts +``` + +> A signpost, deliberately. Every project that adopts this template has a +> `docs/TOOLS.md`, so "read `docs/TOOLS.md` first" is an instruction that works +> without knowing anything about the project — which is the whole reason this +> file exists at a fixed path. + +## The list is not here + +**[`architecture/README.md`](architecture/README.md)** holds the table of what +ships in `scripts/` and what each one is. That is the one copy. + +A second table here would be two records of one fact, and the other one would +never hear that a script was renamed — the failure `DOC_TRUST_MAP.md` exists to +prevent, applied to the tooling instead of the documents. So this file answers +the questions that table does not, and points at it for everything else. + +## If this project has fewer scripts than the template lists + +That is the intended state, not a broken copy. The template's `scaffold.sh` +writes the documents and deliberately leaves the scripts behind — *"an +unconfigured `release.sh` landing in every new repository is a loaded gun, not a +head start"* — so they are taken one at a time, having been read. + +This project took ten of them on 2026-08-18 and declined the rest. What it +declined, and why, is in `docs/history/DEVELOPMENT_LOG.md` under that date. The +short version: no `release.sh` or `deploy.py` until the roll-forward path to +nebula is written down, and no `controls.sh` until there are backups for it to +report on. + +## Which ones can stop you + +Not in a table, because the honest answer lives in each script's own header and +would go stale here. The rule that matters: + +**Exit code `2` is never a pass.** These scripts distinguish "the check ran and +found nothing" from "the check did not run", because those look identical from +the outside and only one of them is evidence. A CI step or a hook that treats a +`2` as success has quietly turned the check off. Each script states its codes at +the top; read them there. + +In this repository specifically, `check-env.sh` and `verify.sh` will both exit 2 +if you gut their configuration, and `preflight.sh` exits 2 when the site is +simply unreachable — which is the case you most want to tell apart from a pass. + +The hooks are the other place work gets stopped. See below. + +## Where to start in a fresh clone + +```bash +npm install +git config core.hooksPath .githooks # per clone. Not optional. See below +bash scripts/check-env.sh --file .env # what is configured, before anything reads it +bash scripts/secrets.sh --tracked # what is already committed +``` + +Then [`architecture/GUARDS.md`](architecture/GUARDS.md) before you write a check +of your own — how to write one that can actually fail. + +## The hooks + +Three, in `.githooks/`, because `.git/hooks` is not versioned and a hook living +there protects exactly one clone. + +| Hook | What it runs here | +| --- | --- | +| `pre-commit` | `scripts/secrets.sh` on the staged diff, then `npm run build` when source is staged | +| `commit-msg` | refuses a message with no conventional type | +| `post-commit` | **pushes to `origin`** | + +`git config core.hooksPath .githooks` is per clone, so every checkout runs it +once. An uninstalled hook fails silently, which is the same class of problem the +hooks exist to prevent. + +**Two things worth knowing before you rely on them.** + +`pre-commit` is **not** the template's version. That one runs `npx tsc --noEmit` +and `npx vitest run`; this project has neither TypeScript nor a test runner, so +installing it unchanged would have refused every commit. It runs the secret scan +— which is the reason the hook earns its place here at all, given the Zoho form +tokens that reached four commits before anyone noticed — and then `npm run build` +when `src/`, `server/`, `index.html`, `vite.config.js` or `package.json` is +staged. **That is a build, not a test.** It catches a broken import and will not +catch a broken behaviour. + +`post-commit` pushes, and that is the intent — but it has a consequence worth +holding on to: whatever documentation was not in that commit is now behind the +code by one push. That is the mechanical reason `docs/WORK_CYCLE.md` asks for doc +edits in the same commit rather than in a tidy-up afterwards. With this hook +installed, *"I will document it next commit"* means the site has already +published the version without it. + +Escape hatches, both loud on purpose: `SKIP_GUARDS=1 git commit …` and +`SKIP_PUSH=1 git commit …`. + +## This project's npm scripts + +Run from the repository root. + +| Command | What it does | +| --- | --- | +| `npm install` | dependencies | +| `npm run dev` | Vite and the Express API together, via `concurrently`. Frontend on 5173, API on 3001 | +| `npm run build` | **three steps**: the client bundle, then an SSR bundle from `src/entry-server.jsx`, then `scripts/prerender.js`, which writes static HTML for every route. This is the only real gate this project has | +| `npm run build:client` | the client bundle alone. Does **not** prerender — do not use it to produce a release | +| `npm run preview` | serve the built client | +| `npm start` / `npm run server` | the Express server alone, serving `dist/` | +| `npm run docker:build` / `docker:run` | build and run the image locally | +| `npm run docker:compose:up` / `:down` / `:logs` | the compose stack | +| `npm run docker:push` | build, tag and push `queue-north-website:dev` to the Forgejo registry | +| `npm run docker:test` | build the image and smoke-test it on 3001 | + +**There is no `npm test`, and that is not an omission in this table.** There is +no test runner in the project. `docs/qa/ClaudeQACoverage.md` carries it as a +standing gap. + +A liveness check by hand, when you want one without the script: + +```bash +curl -s https://qn.isnull.dev/api/health # {"status":"ok","db":"ok","timestamp":"…"} +``` + +## Adding a script + +Put it in `scripts/`, give it a header saying what it does and **which incident +motivated it**, state its exit codes, and add a row to +`architecture/README.md`'s table — this file's `Review trigger` fires on exactly +that. + +The bar, from the scripts that are already here: **done by hand three times, or +once with a consequence.** A script written before either of those has no failure +to describe in its header, which is the part that stops the next person deleting +it. diff --git a/docs/WORK_CYCLE.md b/docs/WORK_CYCLE.md new file mode 100644 index 0000000..ffd0866 --- /dev/null +++ b/docs/WORK_CYCLE.md @@ -0,0 +1,277 @@ +# The work cycle — what happens at the end of a piece of work + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: what must be true before a piece of work counts as finished +Review trigger: Any change to what the Command Center reads, or to which of + those channels a person rather than an agent has to write +``` + +> **This is a procedure, not a status board.** Nothing here records what is open, +> what is next, or what is blocked — those live in the tracker and on +> privacyllc.dev, and a copy of them in this file would be the second disagreeing +> record that `DOC_TRUST_MAP.md` exists to prevent. + +## Why this file exists + +A piece of work ends in more than one place. The code is committed; the issue +that asked for it is still open; the document the change contradicts still says +the old thing; and the project screen at +[privacyllc.dev](https://privacyllc.dev) still shows last week's next action to +whoever opens it. + +None of those catch up on their own. Two of them cannot be caught up later by +anybody but the person who did the work, because by then nobody knows what the +next action was meant to be. + +## The cycle + +Run all of it, in this order, every time. It is short on purpose. + +1. **Close what you finished.** `closes #N` in the commit that does the work, so + the record comes from the thing that happened rather than a date typed + afterwards. If no single commit finished it, close it by hand with the + evidence — a path, a symbol, a test name, or the command that proves it. + *"Done" is not a close.* +2. **File what you found.** A defect noticed on the way past is an issue with a + severity label and the build SHA it was seen at, not a memory. Filing it costs + a minute; the alternative is finding it again from scratch, or shipping it. +3. **Close the milestone if the batch landed.** A milestone with every issue + closed and itself still open reads as a batch still in progress — see + [Open and closed are not bookkeeping](#open-and-closed-are-not-bookkeeping). +4. **Update the documents this change triggered.** Read the `Review trigger` + lines: a new module, a changed data shape, a new migration, a new boundary + something crosses. Those edits go in **the same commit as the code**, for the + reason in [Docs travel with the push](#docs-travel-with-the-push). +5. **Push.** Nothing above is visible off this machine until you do, and step 7 + reports on what was pushed. +6. **Write the log entry** in `docs/history/DEVELOPMENT_LOG.md`: what changed, + what it proved, **Next action**, and **Blockers**. Dated, append-only, newest + first. +7. **Tell the Command Center**, which is three calls: reconcile, so it re-reads + the tracker and the pushed docs; `PATCH` the project's summary and next + action, which nothing else writes; and a check-in if what changed is worth a + timestamped note. Commands are [at the bottom](#the-commands). + +Steps 6 and 7 are the two that get skipped, and they are the two nobody else can +do afterwards. + +## Next action — where it lives + +| Copy | Where | What it is | +| --- | --- | --- | +| The current one | `nextAction` on the project, privacyllc.dev | the live answer to "what happens next", shown to whoever opens the project screen | +| The dated one | the newest entry in `DEVELOPMENT_LOG.md` | what the next action was **at that point** — history, not status | + +These are not two records of the same thing, and the distinction is worth +holding on to. The field is overwritten every time and always describes now. The +log entry is never edited and describes a moment — which is what makes it safe +to keep, and why an old entry naming a next action that has since been done is +not stale, it is a receipt. + +Write the next action as an **action**: the thing a person would start on +Monday, specific enough to begin without asking a question. "Continue the work" +is not one. If the honest answer is that you do not know, that is a real answer — +say what has to be decided and by whom. + +**Filing an issue can change what the project card says next, without anyone +choosing that.** The dashboard's next action is the *newest open issue in the +current milestone* — not the most severe one; severity labels have no influence +on it at all. So a routine `P2` filed into the batch you are working in replaces +whatever the card was showing, and it will keep showing that until something +newer arrives. The `nextAction` field is the only way to say something different +on purpose, which is most of why step 7 exists. + +`currentSummary`, `nextAction` and `description` are **write-only**: they are +deliberately absent from every API response, because free text can name a +customer or an unannounced product. Only the admin screen shows them back. Never +report them as empty because a `GET` did not return them. + +## Blockers — where they live + +A blocker is recorded in up to three places, and **an agent can write only the +first and the third**: + +| Where | What it holds | Who writes it | +| --- | --- | --- | +| The tracker | the work itself — an issue labelled `release-blocker`, or `P0` when it ships broken | anyone, including an agent | +| The blockers table on privacyllc.dev | the business-facing blocker, with a severity, an owner and a resolution note | **a human, in the admin UI** | +| A check-in's `blockers` field | narrative: what is stuck, said in a timestamped note | anyone, including an agent | + +The agent API has no route that creates, edits or resolves a blocker on the +site. Posting a check-in whose `blockers` field says something is stuck records +*narrative* — it does not touch the blockers table, and it does not clear +anything. An agent that reports "blocker filed" after a check-in has told the +truth about the note and a falsehood about the table. + +So: **file the issue** — that is the copy the work is actually tracked in — and +when it belongs in front of a stakeholder, say so plainly and let a human enter +it. Before treating a site blocker as resolved, restate its title and status and +get explicit confirmation; the admin UI requires a resolution note that the API +cannot supply. + +If the work is blocked and nothing is filed anywhere, the project simply looks +slow. + +## Open and closed are not bookkeeping + +Every open issue is a denominator, and not in the abstract: the percentage on +the project screen **is** closed issues over all issues in this tracker. Nothing +else produces it. That has three consequences worth stating in full: + +- **An issue left open after the work is done** understates the project + permanently, and the understatement compounds — a fortnight of finished work + with unclosed issues reads as a fortnight of no progress. +- **An issue closed without evidence** cannot be reopened with confidence, + because nothing in it says what "fixed" meant. That is why the close comment + carries the path, symbol, test or command. +- **Invented future work** makes every percentage wrong, permanently and in one + direction. Do not pad the tracker. If the real answer is one milestone and + three issues, file exactly that. + +Milestones are the same argument at batch scale, and they are counted the same +way: milestones closed over milestones total is the second figure on the project +screen. Closing the last issue under a milestone does not close the milestone, +so a tracker full of complete-but-open batches reports a project as less +finished than it is — and cannot answer "what shipped". + +**File every issue into a milestone.** One filed outside still counts against +the headline percentage while being invisible to the milestone figure, which is +how two readings of the same project come to describe different amounts of work. +The site measures the gap rather than ignoring it. + +One trap that costs an afternoon, and it is about a *card* rather than a figure: +the Milestones list on privacyllc.dev reads the Command Center's own table, +which only an admin can write. A repository whose milestones are being counted +in the figure above can still show *"No milestones have been added yet"* in that +list. It is not a sync failure and no amount of reconciling changes it. + +## Docs travel with the push + +The Command Center reads this repository's documents at a commit, and stores the +SHA it read them from. When that SHA falls behind the repository's newest +commit, the docs report is marked **stale** — not wrong, not missing, *stale*, +which is the honest description of a document that was accurate at a commit +nobody is running any more. + +Two ways to produce it, and both are ordinary carelessness rather than bad luck: + +- **Code pushed, documents not updated.** The report is recomputed at the new + SHA against prose describing the old behaviour. Nothing flags this; the + document is simply confidently wrong now, and its `Last reviewed` line still + looks recent. +- **Documents updated, not pushed.** The site keeps reporting the old ones. A + correction that exists only on your machine has not been made. + +Hence step 4's insistence that documentation edits ride in the same commit as +the change that caused them. It is not tidiness — a doc commit that comes "after +this next thing" is the commit that does not get written, and the manual's +migration table in the project this template came from sat six migrations behind +before anybody noticed. Every reader in between trusted it. + +## What the Command Center reads, and who writes it + +Four independent channels, and **only two of them are automatic**: + +| On the project screen | Source | How it gets there | +| --- | --- | --- | +| Headline % — issues closed / all issues | the repository's tracker | automatic — on reconcile, and immediately on a webhook delivery | +| Second figure — milestones closed / all milestones | the repository's **milestones** | same read, same moment | +| Milestone coverage — issues that sit in no milestone | the repository's tracker | same read | +| QA verdict — round, build SHA, overall sentence | `docs/qa/ClaudeReport.md` | push the repository, then reconcile | +| Current summary / Next action | manual fields on the project | `PATCH /agent/projects/queue-north-website` — nothing else writes them | +| The Milestones card, and a typed weighted plan | the Command Center's **own** milestone table | admin UI only | +| Blockers | the Command Center's **own** blockers table | admin UI only | + +The first three are why steps 1 and 3 are steps: **closing an issue and closing +a milestone each move a figure a stakeholder can see, that day.** The last two +are lists rather than figures, and they are the ones an agent cannot write — a +repository whose milestones are counted in the second figure can still show +*"No milestones have been added yet"* on the card, because that card reads a +table only an admin fills in. + +A repository that has not adopted the four label names is reported as *not +adopted* rather than as zero defects, and one with its tracker switched off is +reported as switched off rather than as an empty backlog. Absence is never +rendered as a measurement — which is exactly why a real backlog nobody filed +looks like nothing at all. + +## The commands + +The tracker is the Forgejo instance at +**[dream.scheller.ltd](https://dream.scheller.ltd)**, and steps 1 to 3 happen +there. Its credentials, the Cloudflare trap and the reason it has to be that +instance rather than any tracker are in the project README's *Where the tracker +is* — one copy, named once. + +**This repository has no test suite**, so "the evidence that proves it" is +usually a file and symbol, a `curl` against the live origin, or what was observed +at a named viewport. Say which; "Done" is not a close. + +```bash +set -a; . ~/.openclaw/docker-registry.env; set +a +python3 scripts/forgejo-issue.py list +python3 scripts/forgejo-issue.py close 196 "Fixed in a1b2c3d; src/components/layout/Header.jsx +# no longer overlaps at 320px — checked in DevTools at iPhone SE width." +``` + +Step 7 talks to the Command Center instead: + +```bash +# The token lives in this machine's credential store, never in the repository. +TOKEN="$(grep PRIVACY_LLC_TOKEN ~/.openclaw/credentials/privacyllc.md | cut -d= -f2)" +BASE="https://privacyllc.dev/api/internal/v1" +AUTH="Authorization: Bearer $TOKEN" +``` + +**Reconcile** — re-read the tracker and the pushed documents. Do this after the +push, not before: + +```bash +curl -sk -X POST -H "$AUTH" -H "Content-Type: application/json" \ + -d '{"kind":"reconcile_all"}' "$BASE/agent/jobs" +``` + +**Summary and next action** — read the project first for its `version`, then +send a flat body carrying that version. A stale version is rejected rather than +silently overwriting somebody else's edit: + +```bash +curl -sk -H "$AUTH" "$BASE/agent/projects/queue-north-website" # for version + state + +curl -sk -X PATCH -H "$AUTH" -H "Content-Type: application/json" \ + -d '{"version":,"health":"on_track", + "currentSummary":"", + "nextAction":""}' \ + "$BASE/agent/projects/queue-north-website" +``` + +**A check-in** — a timestamped "what changed", when there is something real to +report. `summary` is required and must say something; the API refuses an empty +"no change" update, deliberately: + +```bash +curl -sk -X POST -H "$AUTH" -H "Content-Type: application/json" \ + -d '{"projectId":"queue-north-website","health":"on_track", + "summary":"","accomplishments":"", + "blockers":"","nextActions":""}' \ + "$BASE/agent/updates" +``` + +Health is one of `on_track`, `caution`, `off_track`, `unknown`. Never invent +one, and never report progress the API did not return. + +## What this file is not + +- **Not the work list.** That is the tracker: milestones are batches, issues are + deliverables. +- **Not the release procedure.** This project has no `release.sh` — publishing + is `npm run docker:push`, and how nebula moves to a new image is not yet + written down. `docs/OPERATIONS.md` records that gap; + `docs/security/SECURITY_CHECKLIST.md` is the list to work through first. +- **Not the QA procedure.** That is `docs/qa/ClaudeQAPlan.md`, which ends in its + own version of step 7. +- **Not a place to record status.** If you are about to add "current state" or a + list of outstanding items below this line, the tracker is where it goes. diff --git a/docs/architecture/GUARDS.md b/docs/architecture/GUARDS.md new file mode 100644 index 0000000..e0b512d --- /dev/null +++ b/docs/architecture/GUARDS.md @@ -0,0 +1,142 @@ +# Guards — how to write a check that actually checks + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: scripts/verify.d/**, .githooks/** — structural tests, source-grep + assertions, probes, and any check whose passing is taken as evidence +Review trigger: A guard is found to have been passing while the thing it guards + was broken; a new class of check is added to the suite. +``` + +A guard that cannot fail is worse than no guard, because it is trusted. Every +rule here was learned by finding one that had been green for months over +something broken. + +## 1. Prove the guard fails before you believe it passes + +The one discipline that matters most, and it takes thirty seconds: + +```bash +cp src/lib/thing.ts /tmp/thing.bak +# break exactly the thing the test protects +sed -i 's/if (body.error)/if (false)/' src/lib/thing.ts +npx vitest run tests/thing.test.ts # expect: exactly one failure +cp /tmp/thing.bak src/lib/thing.ts +npx vitest run tests/thing.test.ts # expect: green again +``` + +**Exactly one** is the part people skip. If breaking the guard's target fails +three tests, two of them are coincidental and will mask a real regression later. +If it fails none, the guard is decoration — and you have just learned that for +the price of one `sed`. + +`scripts/prove-guard.sh` performs exactly this, which removes the two ways it +gets skipped: the restore is a `trap`, so an interrupted run cannot leave the +code broken, and the failure count comes from the runner's own summary rather +than from eyeballing red — one failing test is routinely reported on half a +dozen lines, and counting those calls a clean result six coincidental +failures. + +Do this when you write a guard, and again when you change what it guards. A +test written alongside the code it tests has never been observed failing. + +## 2. A source-grep guard must tell code from the comment about code + +Structural tests that assert a file does *not* contain some pattern will match +the docblock explaining why that pattern is forbidden. So the clearest possible +comment breaks the test, and the obvious fix is to delete the explanation. + +Strip comments first: + +```ts +const codeOf = (path: string) => + readFileSync(path, "utf8") + .split("\n") + .filter((line) => !/^\s*(\*|\/\/|\{\/\*)/.test(line)) + .join("\n"); + +expect(codeOf("src/lib/thing.ts")).not.toContain("dangerouslySetInnerHTML"); +``` + +Otherwise the guard quietly punishes documenting the rule it exists to enforce — +which is exactly backwards, because the comment is how the next person learns +the rule at all. + +## 3. Pin the behaviour, not the spelling + +A guard should fail when the protected behaviour breaks and stay quiet +otherwise. One that asserts on a variable name fails on a rename that changed +nothing. + +```ts +// Brittle: breaks when the variable is renamed, while the fallback it protects +// is untouched. +expect(route).toContain("readAsset(project.forgejoRepo"); + +// Pins the behaviour: the route fetches through the wrapper that tries both +// spellings, and never through the raw reader. +expect(route).toMatch(/readAsset\(\s*\w+,\s*ASSETS\[which\]\s*\)/); +expect(body).not.toContain("readFileBytes("); +``` + +A guard that fails on changes it does not care about is one people learn to edit +rather than heed, and the edit is usually deletion. + +## 4. A negative result is only as good as the probe that produced it + +"The check found nothing" and "the check did not run" are different facts, and +they look identical from the outside. Before reporting an absence, prove the +instrument worked: + +```bash +# Not this alone — an unreadable file produces the same silence as an unset key +grep -c '^WANTED=' /proc/$PID/environ + +# Establish the read succeeded first +tr '\0' '\n' < /proc/$PID/environ | grep -c . # 0 here means "could not read" +``` + +This is the confident-absence failure one level up: the same trap as a screen +rendering a failed query as a count of zero, applied to your own diagnosis. + +## 5. A guard that is often wrong is worse than none + +A check with a high false-positive rate trains everybody to skip its output, +including on the day it is right. + +One written for this template flagged **684 of 1142** candidates on its first +run. That was not 684 findings, it was a broken heuristic — and shipping it +would have taught its readers that the check is noise. Two rounds of narrowing +brought it to 17 of 363, all of them real. + +If a new guard's first run is loud, tune it until it is quiet before anybody +relies on it. Report the false-positive rate you settled at, so the next person +knows what silence is worth. + +## 6. Guards belong before the artifact exists + +A check that runs after publication catches the problem once it is somewhere it +cannot be taken back from: the tag is in the registry, and refusing the commit +afterwards leaves git with no record of it. + +Order the gates so the expensive, irreversible step is last — preconditions, +guards, build, verify the built thing is what was asked for, publish, and record +it last of all. + +## 7. When the gate finds something that invalidates the operation, stop + +Printing a warning and continuing produces the worst outcome available: the bad +thing happens *and* a reassuring summary appears above it. + +The question is not how bad the finding is. It is **whether it invalidates what +the operation claims**: + +- A release whose test gate skipped half the suite — a release claims to be + tested. **Refuse.** +- A backup written to a group-readable directory — the backup is still a + backup. **Warn.** + +Escape hatches are fine, and they have to be asked for by name, never be the +default, and say plainly what is being given up. diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..0f37061 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,182 @@ +# Architecture — Queue North Website + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: docs/architecture/**, server/index.js, scripts/** — the module + boundaries, the database schema and the API response shapes +Review trigger: Any new module, any change to a module boundary or a data shape; + any new table or column; any new external service the server + calls; any change to how routes are prerendered +``` + +## The shape of it + +One Express process serves everything. There is no separate web server, no +reverse proxy inside the container, and no second runtime. + +```text +browser + | + v +Cloudflare ── qn.isnull.dev + | + v +Express (server/index.js, port 3001) ← the only process + | + +--> dist/ prerendered HTML + the React bundle, served static + +--> /api/health liveness, and a real SELECT against SQLite + +--> /api/leads POST → validate → SQLite → (fire and forget) Zoho + +--> /api/support POST → validate → SQLite → (fire and forget) Zoho Cases + | + +--> db/queuenorth.db better-sqlite3, synchronous, single writer + | + +--> Google reCAPTCHA v3 verify, server-side, before any insert + +--> Zoho CRM WebToLead form post, or REST/OAuth as a standby +``` + +## Module boundaries + +**`src/` knows nothing about the database.** It talks to three JSON endpoints +through `src/lib/api.js` and nothing else. There is no ORM in the client, no +shared schema module, and no import that crosses from `src/` into `server/`. + +**`server/index.js` knows nothing about React.** It serves `dist/` as static +files and falls through to `dist/index.html` for client routes. The one place +this is not quite true is the privacy policy, below. + +**`src/data/*.js` is the content layer.** Services, industries and the privacy +policy text live there as plain data, imported by both the client pages and +`src/entry-server.jsx`. Adding a service is a data edit, not a component edit. + +**`scripts/prerender.js` is a build step, not a runtime.** It renders every route +to static HTML at build time using `src/entry-server.jsx`. Nothing at request +time renders React on the server. + +### The three boundaries worth knowing about + +1. **The privacy policy has two renderers, one source.** `src/data/privacyPolicy.js` + is the single source of truth; `src/pages/PrivacyPolicy.jsx` renders it in the + SPA and the prerender step emits a static copy. This exists because Meta's + crawler does not execute JavaScript, and a policy it cannot read is a policy + that does not count. **Do not add policy text to a component.** + +2. **Zoho is an overlay, never a dependency.** Every handler writes SQLite first + and then calls the forwarder without awaiting it. A Zoho outage, a bad token + or a network timeout costs a CRM record and never a lead. `forwardLeadToZoho` + dispatches to `forwardToZohoWebToLead` or `forwardToZoho` on + `ZOHO_FORWARDING_MODE`; both carry a 10 s `AbortController`. + +3. **reCAPTCHA is the one thing that runs *before* the insert.** It is the only + check that can reject a submission outright, so it is deliberately the only + external call on the blocking path — with a 5 s timeout, and it fails open when + `RECAPTCHA_ENABLED` is false. + +## Data shapes + +`server/index.js` owns the schema and applies it at startup via `initSchema()`. +There is no external migration runner; the one migration that exists rebuilds +`leads` to add the `UNIQUE` constraint and is idempotent. + +### `leads` + +| Column | Type | Notes | +| --- | --- | --- | +| `id` | INTEGER PK AUTOINCREMENT | | +| `company` | TEXT NOT NULL | max 200 after sanitisation | +| `name` | TEXT NOT NULL | max 100. Split on the last space for Zoho's `First_Name` / `Last_Name` | +| `email` | TEXT NOT NULL **UNIQUE** | max 254 (RFC 5321). A duplicate answers **409**, and the Zoho forward is still attempted — the local row existing does not mean the CRM record does | +| `phone` | TEXT | | +| `zip` | TEXT | maps to Zoho `Zip_Code` | +| `message` | TEXT | | +| `service_interest` | TEXT | normalised from empty to NULL. Maps to Zoho `Description`, not a custom field | +| `created_at` | DATETIME | `CURRENT_TIMESTAMP` | + +### `support_requests` + +| Column | Type | Notes | +| --- | --- | --- | +| `id` | INTEGER PK AUTOINCREMENT | | +| `name`, `company`, `email` | TEXT NOT NULL | **no** UNIQUE — the same customer may raise many tickets | +| `phone` | TEXT | | +| `issue` | TEXT NOT NULL | minimum 10 characters, enforced client and server side | +| `priority` | TEXT | defaults to `medium` | +| `created_at` | DATETIME | `CURRENT_TIMESTAMP` | + +**The asymmetry between the two tables is deliberate.** A lead is a person you +want once; a support request is an event that recurs. Adding `UNIQUE` to +`support_requests.email` would silently drop a customer's second ticket. + +### Every response shape the API produces + +| Status | Body | When | +| --- | --- | --- | +| 200 | the resource, or `{status, db, timestamp}` | success | +| 400 | `{error: 'Validation failed', fields: {…}}` | Zod rejected it | +| 403 | `{error}` | reCAPTCHA below `RECAPTCHA_MIN_SCORE` | +| 404 | `{error: 'Not found'}` | unmatched `/api/*` only; other paths fall through to the SPA | +| 409 | `{error}` | duplicate `leads.email` | +| 413 | — | body over 1 MB | +| 429 | `{error, message, retryAfter}` | rate limiter | +| 500 | `{error}` | never a stack trace | +| 503 | `{error, db: 'error'}` | health check could not reach SQLite | +| 504 | `{error: 'Request timeout'}` | the 30 s request timeout fired | + +## Documents here + +- **`GUARDS.md`** — how to write a check that actually checks. Read it before + adding a structural test or a probe; every rule in it was learned from a guard + that had been green over something broken. +- **`zoho-setup.md`** — the CRM integration end to end: app setup, credentials, + environment variables, and how to confirm a lead arrived. + +## What ships in `scripts/` + +Ten scripts came from the template on 2026-08-18 and three were already here. +The template's full catalogue is a **menu**, not an inventory — see +[`../TOOLS.md`](../TOOLS.md). This table is what this project actually has, and +each row says what it does *here*. + +| Path | What it is | +| --- | --- | +| `scripts/check-env.sh` | which of the 17 Zoho / reCAPTCHA / CORS / rate-limit variables are set and plausible, before the server reads them. Exit 2 means nothing was checked | +| `scripts/secrets.sh` | credential shapes in a staged diff, and `--tracked` for a whole-tree audit. **`--built dist/` is the one that matters here**: `VITE_RECAPTCHA_SITE_KEY` is inlined into the bundle at build time, so the repository scan cannot see what users receive | +| `scripts/verify.sh` | every check this project has, in one table. Honestly thin — there is no test suite, and it says so rather than printing a green row | +| `scripts/doc-triggers.py` | which documents a pending change fires, read from the `Governs:` headers. Run it before committing, not after | +| `scripts/forgejo-issue.py` | files and closes issues in the tracker convention, refusing malformed ones before they are filed | +| `scripts/status.sh` | what is running on **nebula** as `qn-website-dev`, its version and its restart count. Read-only | +| `scripts/healthcheck.sh` | a liveness tick against `qn.isnull.dev`, asserting HTTP 200 **and** `"status":"ok"` — a 503 with a JSON body is a real answer, not an outage | +| `scripts/preflight.sh` | headers and TLS against the live origin. No `--auth` checks: there are no accounts | +| `scripts/backup.sh` | a verified SQLite dump. Its ENGINE block was rewritten for better-sqlite3's online `.backup()` — see below | +| `scripts/restore-check.sh` | restores the newest dump into a scratch file, runs `PRAGMA integrity_check`, counts tables, and **times it**. A backup nobody has restored is a guess | +| `scripts/docker-push.sh` | builds and pushes `queue-north-website:dev` to the Forgejo registry. Predates the template | +| `scripts/docker-test.sh` | builds the image and runs it locally on 3001. Predates the template | +| `scripts/prerender.js` | the build step that emits static HTML for every route. Predates the template | + +**Why `backup.sh` and `restore-check.sh` are not the template's originals.** Both +ship as PostgreSQL tools. `backup.sh` is built to be adapted — everything +engine-specific is in one ENGINE block — so that block now calls +`better-sqlite3`'s `.backup()` inside the running container and verifies the +result with `sqlite3` before renaming it into place. `restore-check.sh` had no +such seam: it is `pg_restore` and `psql` end to end, so the SQLite version is a +rewrite that keeps the argument and replaces the mechanism. + +## What does not belong here + +- Product intent — that is `docs/planning/PROJECT_PLAN.md` +- Engineering standards and the stack policy — that is `docs/planning/REQUIREMENTS.md` +- What it should look and sound like — that is `docs/design/` +- What happened while building it — that is `docs/history/` + +## A note on drift + +Architecture docs go stale faster than any other kind, because code changes under +them silently. This is exactly what the **Review trigger** line is for: name the +change that should send somebody back here, and a reader can tell whether the +trigger has fired. + +The specific instance to avoid in this repository: `BUILD_SUMMARY.md` carried a +copy of the SQL schema that predated the `UNIQUE` constraint on `leads.email`. +It was not carried forward on adoption. **`server/index.js` owns the schema; the +tables above describe it and do not duplicate it.** diff --git a/docs/zoho-setup.md b/docs/architecture/zoho-setup.md similarity index 86% rename from docs/zoho-setup.md rename to docs/architecture/zoho-setup.md index 548cc9a..17a5de3 100644 --- a/docs/zoho-setup.md +++ b/docs/architecture/zoho-setup.md @@ -1,5 +1,22 @@ # Zoho CRM Setup Guide for Queue North Admins +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: server/index.js, .env.example — the Zoho CRM integration: app setup, + OAuth credentials, environment variables, and how to test that leads + arrive +Review trigger: Any change to ZOHO_* environment variables, the forwarding mode, + the field mapping, or the datacenter; any Zoho API version change +``` + +> **Read the mode first.** This guide documents the OAuth/REST path. Production +> currently runs `ZOHO_FORWARDING_MODE=webtolead`, which needs none of the OAuth +> setup below — the REST path is kept as a configured standby. `.env.example` +> shows both. The reasoning behind the current shape is in +> `docs/history/BATCH_LEDGER.md`. + This guide walks you through the current Zoho CRM integration. Contact leads use the legacy Zoho WebToLead form tokens, while the OAuth/API integration remains available as a standby option for future lead upserts or support cases. --- @@ -158,7 +175,10 @@ If your Zoho datacenter is **outside the US**, adjust the domains: ### Field Mismatches - If leads/cases don't appear, check if Zoho requires custom fields like `Service_Interest` -- Edit the field mapping in `server/zoho/` to match your Zoho CRM field API names +- Edit the field mapping in `server/index.js` — `forwardToZoho()` builds the + `First_Name` / `Last_Name` / `Lead_Source` payload, and + `forwardToZohoWebToLead()` builds the form fields — to match your Zoho CRM + field API names ### Cases Not Appearing - Ensure `ZOHO_CASES_ENABLED=true` is set diff --git a/docs/data/README.md b/docs/data/README.md new file mode 100644 index 0000000..cadf8f5 --- /dev/null +++ b/docs/data/README.md @@ -0,0 +1,99 @@ +# Data — Queue North Website + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: docs/data/** — the assets privacyllc.dev renders for this project +Review trigger: A rebrand, or any change to the icon, logo or banner +``` + +## What goes here + +Three files, in `img/`, at exactly these names: + +```text +docs/data/img/icon.webp the square mark, used wherever the project is listed +docs/data/img/logo.webp the full lockup, used on the project page +docs/data/img/banner.webp the wide image, used across the project header +``` + +All **webp**. All **required**. Only `img/` is checked — an asset left in +`docs/data/` instead of `docs/data/img/` is not found. + +**Dimensions, weights and how to generate them are in +[`img/README.md`](img/README.md)**, beside the files they describe. This +document owns the rule; that one owns the spec. Stating both in both places is +how two copies of one convention start disagreeing. + +**No placeholders ship with this template, deliberately.** It carried a 0-byte +`logo.webp` once, and an empty file is the worst of the three states: a check +that asks "does the path exist" calls it present, and anything that reads the +bytes rejects it — a consumer verifying the webp signature answers 415, which +reads as a corrupt asset rather than a missing one. Absent is honest and the +conformance check reports it as absent, which is what gets it filled in. + +Extra sizes and variants are welcome beside them — `icon-512.webp`, +`logo-dark.webp` — and are not treated as clutter. Only the three exact names +are checked for. + +## What is here now + +| File | State | Source | +| --- | --- | --- | +| `img/icon.webp` | **present** — 512 × 512, 28 KB | `assets/icons/logo512.png`, the Queue North compass-star mark | +| `img/logo.webp` | **present** — 1024 × 1024, 41 KB | `assets/logo2.png`, the full lockup | +| `img/banner.webp` | **absent** | no 3:1 source exists — see below | + +Both present files were converted from the project's **own** existing marks on +2026-08-18, which is using the art the project already had rather than inventing +a placeholder. The source art lives in `assets/` at the repository root, which is +where this project keeps exported source files; nothing in the build references +that directory, so do not delete it as dead. + +`banner.webp` needs 2176 × 725 and nothing in the repository is that shape. +`assets/JointLogoWhite.png` is 4082 × 1066 and close, but it is a *joint* mark +with a partner, and whether that is the right thing on a Queue North project +header is somebody's decision rather than a conversion. Filed as a `P2` issue. + +## If an asset is missing, open an issue — do not invent one + +An agent cannot draw a logo, and this is the one gap in the whole convention +that cannot be closed by writing a file. + +So when an asset is absent, **file an issue** rather than producing something: +title it for the asset, label it `P2`, and end the body with its `Verify:` line +— `Verify: docs/data/img/logo.webp exists and the project card renders it.` + +**Do not generate a placeholder.** A placeholder that looks deliberate outlives +the issue that would have replaced it: nobody files a ticket against an image +that appears to be finished. An obviously absent asset keeps asking. + +## Why this folder is different from every other one here + +The Command Center *consumes* these. Every other document in this tree is written +for a person to read; these are fetched and rendered on privacyllc.dev's project +page. + +That has one consequence worth stating plainly: **this folder cannot be declared +exempt.** A repository may tell the conformance check that a required document is +deliberately absent — kept out of git on purpose, say — and the check will +believe it. It will not accept that declaration for `docs/data/`, because the +result would be a project card with nothing to show and nothing explaining why, +which is the exact failure the check exists to prevent. + +## Why webp and not PNG + +One format, checked by its magic bytes rather than its file extension, so the +site can serve it inline with confidence. A file whose first bytes are RIFF/WEBP +cannot be an HTML document or an SVG carrying script, which is what makes it safe +to render directly rather than forcing a download. + +Renaming a PNG to `.webp` will not work, and is meant not to. + +## This is not an asset library + +Screenshots, mockups, reference art and exported source files do not belong here. +They belong wherever the project already keeps them. This folder holds the three +marks that identify the project elsewhere, and stays small enough that its +contents are obvious at a glance. diff --git a/docs/data/img/README.md b/docs/data/img/README.md new file mode 100644 index 0000000..361e2b7 --- /dev/null +++ b/docs/data/img/README.md @@ -0,0 +1,57 @@ +# Project images + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: docs/data/img/** — the three files and their sizes +Review trigger: A rebrand; any change to a required name, dimension or ceiling; + any change to what the consumer accepts. +``` + +Three files, all **webp**, all **required**, at exactly these names: + +| File | Dimensions | Aspect | Typical weight | +| --- | --- | --- | --- | +| `icon.webp` | **512 × 512** | 1:1 | 8–60 KB | +| `logo.webp` | **1024** on the long edge | whatever the lockup is | 20–190 KB | +| `banner.webp` | **2176 × 725** | 3:1 | 30–130 KB | + +**Not `icon.ico`.** The consumer checks the file's magic bytes, not its name: a +`.ico`, or a PNG renamed to `.webp`, is refused with a 415 and the project falls +back to an initials tile. That signature check is what makes it safe to render +these inline, so it is not going to be relaxed. + +**512 KB is a hard ceiling per file**, enforced in code — the size is read from +the listing before the bytes are fetched, so an oversized asset is never +downloaded and simply never appears. Nothing enforces the dimensions, which is +why they are written down. + +## Making them + +```bash +magick logo-source.png -resize 512x512 -quality 82 icon.webp +identify -format '%f %wx%h %b\n' *.webp # check before committing +``` + +Quality 80–85 suits a flat mark. If a file lands over ~200 KB it is usually a +photographic banner that wants a lower quality rather than fewer pixels. + +## Why these numbers + +The icon renders small — a 44 px tile in a list, 58 px on a project header — so +512 covers the densest display several times over; the reference project +deliberately halved it from 1024. The banner spans a card about 760 px wide, so +~2176 covers it at 2×, and its 3:1 shape matters more than its width because the +header crops to fill. The logo has no fixed frame, so only its long edge is +specified. + +Extra sizes and variants are welcome beside these — `icon-512.webp`, +`logo-dark.webp`. Only the three exact names are checked for. + +No placeholders ship with this template. An empty file is the worst of the three +states: a check that asks whether the path exists calls it present, and anything +reading the bytes rejects it. Absent is honest, and the conformance check reports +it as absent — which is what gets it filled in. + +Why the requirement exists, and what reads it: [`../README.md`](../README.md). diff --git a/docs/data/img/icon.webp b/docs/data/img/icon.webp new file mode 100644 index 0000000000000000000000000000000000000000..b85ea2226de08bd2a717bb1e29c3548291a2dfe0 GIT binary patch literal 29016 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 IS^KRfB1x{;qG%uBf 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 z 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

c0wg>ag^-wisEePn%F<-KN`_I?$Ht> zito}@@-hhf1an&xerD&g6j&YEWr&leC6<~mZGBmBJ#IrOv@869o`l|aNtMH^ND!eB zwJSj~JiX?W1~Z3DGq)6LbH7L-xO+@` z-(SESSbo8x5e!v$NeJ{#ppDU8KIHF6V8ujtkNZLIN zzIfcmv{P9Y3sJ^@liopCBpA7Fl5H40S>O^96V_>>rm|2k)BM z%S@H^zCWLwA;}U~Iw=@@dz!?+$wK4dTRz>M{*nRu8eN~DkDF< zR_yeZs)^c~gBSW-?PQn&n#UotK0&Levw>jBl1110xTzM}sx2%4o-;s+aJhZDs6pV+iQlemHq zJTiYd*Yqy!$1xFWoJ_DRdE)7994Z`q$jZEKb{Ysd+id| zJIF#5^UOM>ARhcOe4Y_B75EJj>500FjFAc7X5Ta;;;yttzEpiBJBM+hf$Eh|2jkNn zk#3(pw2=`(M8|B5>$ntx&!_H609cjx2T=7e6eenc$_U@go3x?k+5f%5p4u!RHiD)> zXsb_`|HL5Z{4?n940C6uRlO+HX6E%@io)UzE*OcrlN*GbBF-;j!Ie5X$neshseP#5X?~Z(9loMg&PO2#WHSU!`G(*^dWK96O zw$511coGeIK?SC-7T89wKn^7sfb^WWj;xQl?8ZR$L=4L=_;JWehrqw>? zHDhR1;n!~eD|{wml@qLiS#zzL5cyRg7+{$fV!D z0*C$I2?2gFTBsgpw+F21llX0KP9_qxIn8?mEZ1LlAk zq^|W~3Jb@PWWs2*ws->8B5LMgGyTo;NfeqBNQIzQ1%@w)o_Q%L$m&mbIB@_jrbRh z5*dTQ;5gq70E?@CMeIY6#pXuhs`mhW?4*Y_qa;_%N2v9=&400WQga1;z-ahHXVNbH z;jkXHw6%{|n1LsJ;i?U{`bszPTfZl-TCB}3G@nFs>KvvX2% z@!&Id+dvsrKrTJm6#t`dMu~2VAGIDz)7zDG0m>$r)u4!phNnbsyXD4>>0a zVb~KUaW=Itv=KuDAs|-RJ%nJ~z6yA7=g`aU8>vK}H;I*gbz(HPIMarY*p3#UrwY^| zOY{rxl8wvbBVe5SN)==zJ`&hWW*6y3soJD{u*5pc+-Vta@An$N2(K7Tgt8@DW9&=j zk7w!(^BV1Lt&K`s|7#lYJ&Q*?+c#UaHR=fC^T6QHR*%kHdLte^F4pSCDNs6oG>8_W zm&){50uzQe_V5pdsR+9-SmvjT9u`w7|{QhGyXY~#Z7+Jc%;!B_ps>r zOqJx;FJXZNipY3Zgy1yU5}Xv3c(A{>%Ks1m9#$Jyw&~DauXQS^ZU6 zxr@R@Ew4u(4H2m-ozx*za>L!|OK*p`Szg$2Wr_$g@X+}kHWES;nHls^XtWjmCTT6< z1Bb63!gZ%QQUv+8Ave>Ek+$FNNwD$#H(RM0RyyExUx&#(!u#IdlRy9Fcp09B(4~Rx z8{184g@KQmTdY07?TzPxZ*CbtIAP*c<7bsSo4nxxrq*RYE%$2|5T_EnaH3ek}hq8JOjp|Dg$^oLh8`RdsAm8`QfB2O z^JMAtz8lf+PE7U_@$cMjh17ytlK>iB1ddY4OAiUao` zB4WkHYBP#Gqmk!;HRUAJ2AI#n6Fs}m$mDwO^mia32R!D$2mr9q6e`)ukNB&`s-0$@ z>KNf0+C=jE6lwQw%81A@`aV74pEm}4lM7E3aoa7yurEb-@6i7g6iALgQN-!`Zsd_4 eKO5^x)#eug{{pxluo}drdJ#S5`j5!}OZYz)3HWya literal 0 HcmV?d00001 diff --git a/OVERHAUL_PLAN.md b/docs/design/OVERHAUL_PLAN.md similarity index 94% rename from OVERHAUL_PLAN.md rename to docs/design/OVERHAUL_PLAN.md index 09008ed..563767c 100644 --- a/OVERHAUL_PLAN.md +++ b/docs/design/OVERHAUL_PLAN.md @@ -1,5 +1,27 @@ # Queue North Website — 2026 Overhaul Plan +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: tailwind.config.js, src/index.css — the visual system: palette, + typography, spacing, radius and shadow, component choices, and the + layout blueprints for each page type +Review trigger: Any change to a Tailwind theme token, the type scale, the + spacing rhythm, or the layout of a page type; any new page type +``` + +> **Two halves, and only one is current.** Its *Scarlett Design Implementation +> Brief* onward — Tailwind theme tokens, the numeric-typography rule, the type +> scale, section spacing, radius and shadow rules, the component list, the page +> blueprints and the explicit anti-patterns — is the **live design authority** for +> this project and should be trusted as written. +> +> Its earlier *Migration Phases* and *Agent Plan* sections describe a rebuild that +> completed at `v0.9.x`. They are kept as the record of what was planned and are +> **history, not instructions**. The narrative of how it actually went is in +> `docs/history/BATCH_LEDGER.md`; open work is in the tracker. + ## TL;DR Rebuild the current static HTML/CSS/JS website into a modern full-stack app using: @@ -23,7 +45,10 @@ The current project is a static site with: - `index.html` — all pages live in one large file - `styles.css` — large hand-written stylesheet with many page-specific overrides -- `main.js` — manual hash-based routing and interactions +- main.js — manual hash-based routing and interactions. Named without backticks + because this section describes the site as it was BEFORE the rebuild; that + file was deleted on 2026-08-18 and a backticked path would claim it still + exists. The original is preserved in `.drop/` - Inline Zoho webform scripts - Static assets in `assets/` diff --git a/docs/design/README.md b/docs/design/README.md new file mode 100644 index 0000000..4ce0889 --- /dev/null +++ b/docs/design/README.md @@ -0,0 +1,48 @@ +# Design — Queue North Website + +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: docs/design/** +Review trigger: Any new user-facing surface, or a change to the product's tone +``` + +## Documents here + +Two, and they answer different questions. Both predate the template and both are +this project's own writing. + +| Document | Owns | +| --- | --- | +| [`OVERHAUL_PLAN.md`](OVERHAUL_PLAN.md) | **what it looks like** — Tailwind theme tokens, the numeric-typography rule (Georgia for all numeric content), the type scale, section spacing and container sizes, radius/shadow/border rules, which shadcn-style primitives to use, the per-page layout blueprints, and the explicit anti-patterns. Its earlier *Migration Phases* and *Agent Plan* sections are history; the brief onward is live | +| [`REDESIGN_REVIEW.md`](REDESIGN_REVIEW.md) | **what it sounds like** — the B2B positioning argument, the messaging shift from "interesting technical person" to "reliable communications and IT partner", and the anti-patterns of tone. The eleven open content issues in `Batch 12` are defects measured against this | + +The single most load-bearing sentence across both: *business buyers purchase risk +reduction, not technology.* Copy that reads as clever costs trust here in a way +it would not on a developer-facing product. + +## What belongs here + +What it should feel like, and the decisions behind that: + +- **Product decisions** — what the user can do, in what order, and what happens + when they get it wrong. The error states are design, not an afterthought. +- **UI plans** — screens, states, and what each one is for. Include the empty + state and the loading state; they are the two most people see first and the + two most often left undesigned. +- **Copy** — the actual words. Interface text is a design surface, and writing + it late means writing it badly. +- **Tone** — how this product talks. One paragraph is enough, and it settles a + hundred small arguments. + +## What does not belong here + +- How it is built — that is `docs/architecture/` +- Scope and audience — that is `docs/planning/PROJECT_PLAN.md` + +## Include the rejected version + +For any decision that was genuinely close, record what was not chosen and why. +Without it, the same option gets proposed every few months and re-argued from +nothing. diff --git a/review.md b/docs/design/REDESIGN_REVIEW.md similarity index 87% rename from review.md rename to docs/design/REDESIGN_REVIEW.md index 5555cae..9dda10f 100644 --- a/review.md +++ b/docs/design/REDESIGN_REVIEW.md @@ -1,5 +1,22 @@ # Queue North Website Redesign Strategy +``` +Status: Current +Owner: _null +Last reviewed: 2026-08-18 +Governs: brand positioning, tone, and the messaging standard every page is + written against +Review trigger: Any change to how Queue North positions itself, or to the + audience the site is written for +``` + +> The positioning argument behind the rebuild, written before it started. It is +> still the standard: the open content issues in `Batch 12 — Enterprise content +> rewrite` (#199–#209) are all defects measured against this document. +> +> `OVERHAUL_PLAN.md` says what the site should look like. This says what it +> should *sound* like, and why the previous one lost trust in seconds. + # Core Problem Current website branding feels: diff --git a/docs/history/BATCH_LEDGER.md b/docs/history/BATCH_LEDGER.md new file mode 100644 index 0000000..082cae4 --- /dev/null +++ b/docs/history/BATCH_LEDGER.md @@ -0,0 +1,158 @@ +# Batch ledger — Queue North Website + +``` +Status: Archived +Owner: _null +Last reviewed: 2026-08-18 +Governs: what the batches were, before the tracker held them +Review trigger: Nothing. Superseded by the tracker; kept for the record. +``` + +## Why this file is archived rather than deleted + +Planning used to live in markdown as a numbered batch list. It now lives in the +tracker: **milestones are batches, issues are deliverables**, and severity is +`P0` / `P1` / `P2` / `release-blocker`. + +This file is what that list *was*. It is kept because the reasoning in it is +still worth reading, and deleted files are not readable. + +**It must not be updated.** A batch list beside the tracker is a second answer to +"what is open", and two records of the same thing will disagree without saying +which is right. That is precisely why the work moved. If you are tempted to add a +batch here, add a milestone instead. + +Its `Status: Archived` is therefore not a nicety — it is the machine-readable +form of "do not treat this as current". + +## "Ledger: missing" on the project screen is the correct answer + +The Command Center's docs report still looks for a batch ledger — at +docs/planning/FUTURE.md, named here without backticks deliberately, because +`doc-claims.sh` treats a backticked path as a claim that the file exists and +this one must not — and reports it as **missing** for every repository that has +moved planning into the tracker. That is the expected state and not a gap to +close: the report's own contract is that a tracker-first project shows a missing +ledger and no batch percentages. + +Creating that file to turn the line green would rebuild, in a new name, the +exact second record this one was archived for. + +This repository's record already reads `ledger: retired`, which is that contract +working as intended. + +## The batches, as they stood + +Ten phases, roughly 0.0.1 through 0.9.3, May to August 2026. The tracker now +holds all 205 issues; `Batch 00 — Pre-convention history` is the closed milestone +they were assigned to on adoption, because per-batch attribution was not +recoverable — closure timestamps collapse into five bulk-close days, 119 of them +on 2026-05-17 alone. + +| Phase | What it covered | Outcome | +| --- | --- | --- | +| 1 — Stack scaffold (`0.1.x`) | Vite + React + Tailwind, shadcn-style primitives, React Router, Express, better-sqlite3, first API paths | Complete | +| 2 — Layout rebuild (`0.2.x`) | app shell, all route pages, business content ported into React, service/industry data files, hash routing removed | Complete | +| 3 — Visual overhaul (`0.3.x`) | light-first business design, Tailwind theme, typography and spacing rhythm, mobile-first polish | Complete | +| 4 — Forms and backend hardening (`0.4.x`) | forms wired to Express, SQLite persistence, client and server validation, sanitisation, Zoho forwarding scaffold, rate limiting, Helmet, CORS | Complete | +| 5 — Verification and redesign (`0.5.x`) | SPA router fix, hero rewrite, trust signals, services rewrite, Why Queue North, footer and CTA pass | Complete | +| 6 — Owner feedback (`0.6.x`) | issues #30–#41: nav active state, service icons, Cisco signals, hero alignment, industry icons, 8x8 page merge | Complete except the About content, which needed owner input | +| 7 — Zoho CRM integration (`0.6.6`) | OAuth token endpoint fix, lead payload mapping, support→Cases, upsert, setup guide | Complete. Batch 7.5 (webhook verification) deferred and never taken up | +| 8 — Deferred enhancements | testimonials, blog, SEO, performance audit | SEO and performance done; testimonials and blog still open on owner content | +| 9 — Audit bug fixes (`0.7.0`) | 44 issues across batches 9.0–9.9 | Complete | +| 10 — Low-priority fixes (`0.7.1`) | 6 issues across batches 10.0–10.2 | Complete | +| — (`0.8.x`–`0.9.3`) | badges, mobile nav, breadcrumbs, injection hardening, 404, Zoho WebToLead mode, reCAPTCHA, privacy policy, full-route prerender | Complete. Deployed as `queue-north-website:dev` | + +## What was still open when the ledger was retired + +All of these were already filed as issues, which is why nothing needed migrating +out of this file when it was archived: + +- **#68** — About section content corrections. Blocked on owner input. +- **#69** — Testimonials / case studies. Blocked on real client content. +- **#70** — Blog / writing section. Blocked on an owner decision. +- **#108** — Certification number #25432 appears fabricated. Needs owner verification. +- **#110** — "25+ years" claim unverified against the original site. +- **#162** — About page: founding year, partnerships, cybersecurity, compass image. + +## The Zoho audit, as it was written + +Kept because it is the reasoning behind how the integration is shaped now, and +because points 1–7 and 11 were fixed by batches 7.0–7.3 — a reader who finds the +current code puzzling is usually looking at one of these decisions. + +### Current Implementation Status + +**✅ Already working:** +- OAuth2 refresh token flow with in-memory token caching +- 10s AbortController timeout on all Zoho fetches +- Null-check short-circuit if credentials missing +- Response.ok check before JSON parsing +- Double-slash URL path fix (`ZOHO_API_DOMAIN.replace(/\/$/, "")`) +- Fire-and-forget with `.catch()` (non-blocking, won't break lead submission) +- SQLite always writes first (Zoho is best-effort overlay) +- UNIQUE constraint on `leads.email` with 409 Conflict response +- Sanitization + Zod validation before insert +- Zoho forwarding also attempted on duplicate emails (in case Zoho record doesn't exist yet) + +**⚠️ Issues found in audit:** + +1. **`Service_Interest` is not a standard Zoho field** — Zoho uses `Lead_Source` (picklist) for tracking where leads come from, and custom fields for service interest. `Service_Interest` will be silently ignored unless a custom field with that exact API name exists in the Zoho org. Should map to `Lead_Source: "Website"` plus a custom field or `Description` for the specific service. + +2. **Name field mapping is wrong** — Code maps `leadData.name` to `Last_Name` (correct that it's required), but the contact form has a single "Name" field. Zoho requires `Last_Name` (mandatory) and has `First_Name` (optional). Current mapping `Last_Name: leadData.name || "Unknown"` puts the full name in Last_Name which works but is ugly in Zoho UI. Should split on last space. + +3. **Missing `Lead_Source` field** — Every web-submitted lead should have `Lead_Source: "Website"` so it's trackable in Zoho. Currently absent. + +4. **Token endpoint uses wrong URL** — The refresh token POST goes to `{ZOHO_API_DOMAIN}/oauth/v2/token` but Zoho requires the token endpoint to be on the **accounts server** (`accounts.zoho.com` for US, `accounts.zoho.eu` for EU, etc.), NOT the API domain (`www.zohoapis.com`). This is a **bug** — it will fail in production. + +5. **Missing `redirect_uri` in refresh token request** — The Zoho docs don't require `redirect_uri` for the refresh token grant, but our code sends it. It won't cause an error (Zoho ignores it), but it's unnecessary. + +6. **No `$approved` flag** — For web-to-lead submissions, Zoho recommends sending `"$approved": false` to route leads through approval workflows, preventing unverified web submissions from immediately entering the active pipeline. + +7. **No `trigger` parameter** — By default Zoho will fire all workflows/blueprints on API-created leads. Should send `"trigger": ["workflow"]` to explicitly control which automations run, or `"trigger": []` to suppress if unwanted. + +8. **Support requests not forwarded to Zoho** — Support form submissions go to SQLite only. Should create Cases (or at minimum Contacts + notes) in Zoho for ticket tracking. + +9. **Datacenter configuration** — `ZOHO_API_DOMAIN` defaults to `https://www.zohoapis.com` (US). Queue North is a US company, so this is correct. But the accounts-server URL (`https://accounts.zoho.com`) is a different domain and must be configured separately. Currently there's no `ZOHO_ACCOUNTS_DOMAIN` env var. + +10. **CSP `connect-src` missing Zoho domains** — Helmet CSP only allows `'self'` for `connect-src`. This doesn't affect server-to-server calls, but if any client-side code ever calls Zoho directly it would be blocked. (Not currently an issue since all Zoho calls are server-side.) + +11. **No retry on token refresh failure** — If the refresh token request fails, the lead is silently dropped. Should implement at least one retry with exponential backoff. + +12. **`Zip_Code` is not a standard Zoho field** — Standard Zoho Leads have `Zip_Code` as a field API name... actually this IS correct. `Zip_Code` is the standard field. ✅ + + +## Batch 7.5 — the one that was never done + +> **Zoho Webhook Verification — Future Enhancement** +> Optional: verify the Zoho integration works via a webhook callback. Not +> blocking for initial enablement. Could add a `/api/zoho/webhook` endpoint that +> Zoho calls on record changes — useful for confirming a lead was created, +> syncing status back, or notifying the team. Requires Zoho webhook setup in CRM +> and signature verification. + +Deliberately not filed as an issue on adoption. It was written as a "defer unless +requested" item and nobody has requested it; filing it would pad the tracker with +work nobody has chosen. It is recorded here so the idea is not lost. + +## The Phase 1–3 build record + +From the root-level `BUILD_SUMMARY.md`, migrated 2026-08-18 and then deleted. + +What it recorded as complete: the Vite/React/Tailwind foundation with all routes; +the Express backend serving `/api/health`, `/api/leads` and `/api/support`; SQLite +with `leads` and `support_requests`; layout components and the shadcn-style +primitives (Button, Card, Input, Textarea, Select, Badge, Sheet); every page +including the then-standalone `/8x8`; and the seven services and four industries +data files. + +Its "known issues" list is worth keeping, because three of the four were later +fixed and the record of them being *known* is what makes the fixes legible: +the Sheet component skipping TypeScript generics, image assets still being +placeholders rather than Queue North branding, `db/` needing to be gitignored, +and rate limiting not yet existing on the API endpoints. + +**Its embedded SQL schema was deliberately not carried forward.** It predated the +`UNIQUE` constraint on `leads.email` and the Zoho-related columns, so copying it +here would have created exactly the stale second record this tree exists to +prevent. `server/index.js` owns the schema. diff --git a/docs/history/DEVELOPMENT_LOG.md b/docs/history/DEVELOPMENT_LOG.md new file mode 100644 index 0000000..f2c18ff --- /dev/null +++ b/docs/history/DEVELOPMENT_LOG.md @@ -0,0 +1,918 @@ +# 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 — Adopted the project template + +Placeholder — replaced at the end of this session with the real entry, which +carries Next action and Blockers. + +--- + +## 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 `