# 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.
