WFDASH-WEBAPP/docs/qa-review.verify.md
SJCoetz cdaa2e2b25 fix, feat, style: standardize quote numbers, polish UI
- Standardize quote number format from Q-YYYY-MM-NNN to QYYYYMMNNN across all client, server, test and API code
- Add atomic backfill logic for quote counters to avoid sequence collisions with existing records
- Rearrange Delivery Notes and Repairs to the Operations sidebar section
- Add supplier count badge to pricelist catalogue and improve filter tab overflow styling
- Optimize font loading in root layout to skip preloading unused font weights
- Improve table styling for customer and company pages with better text wrapping and empty states
- Fix rep display logic in quotes page to show "Unassigned" when no rep is assigned
- Add supporting documentation, PocketBase counter schemas and setup migration scripts
- Remove embedded SupplierMap from suppliers page for cleaner layout
- Improve invoice card layout for company and customer info spacing
- Improve filter label styling and accessibility in collection map page
2026-07-10 22:21:50 +02:00

366 lines
No EOL
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# QA Report Verification — `docs/qa-review.md`
**Verifier:** verifier agent
**Date:** 2026-07-10
**Verdict:** PASS (with one cosmetic citation inaccuracy that does not affect the substance)
---
## Summary
The producer's QA audit at `docs/qa-review.md` is honest, evidence-based, and broadly accurate.
All three gates (`npm run lint`, `npx tsc --noEmit`, `npx vitest run`) reproduce exactly as claimed.
Five spot-checked findings (3 P1 logic bugs, 1 P1 sanitizer risk, 1 P1 dev-script footgun) all hold up
when read against the cited files. Coverage is complete across the requested dimensions (build, types,
tests, Next 16, PB integration, PB migrations, security, performance, a11y, E2E gaps, dead code).
Severity assignments are reasonable — no over-classification spotted. The producer's recommendation
(needs fixes, ship-with-known-bugs) matches the evidence on disk.
---
## Gate 1 — `npm run lint`
**Method:**
```
cd /Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS && npm run lint 2>&1 | tail -80
```
**Evidence:**
```
/Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/src/lib/pdf/purchase-order-document.tsx
122:13 warning Image elements must have an alt prop ... jsx-a11y/alt-text
/Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/src/lib/pdf/quote-document.tsx
120:13 warning Image elements must have an alt prop ... jsx-a11y/alt-text
/Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/src/lib/server/quotes-service.test.ts
5:9 warning 'store' is assigned a value but never used @typescript-eslint/no-unused-vars
/Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS/src/lib/server/quotes-service.ts
138:12 warning 'err' is defined but never used
451:9 warning 'receivedAt' is assigned a value but never used
✖ 136 problems (50 errors, 86 warnings)
2 errors and 29 warnings potentially fixable with the `--fix` option.
```
**Result: PASS** — Producer claimed **50 errors / 86 warnings**. Reproduced exactly: 50 errors, 86 warnings.
Lint categories that appear in the report (`react-hooks/set-state-in-effect`, `@typescript-eslint/no-explicit-any`,
`jsx-a11y/alt-text`, etc.) all visible in the raw output.
---
## Gate 2 — `npx tsc --noEmit`
**Method:**
```
cd /Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS && npx tsc --noEmit 2>&1 | tail -50
```
**Evidence:**
```
src/app/(app)/collection-confirm/view-model.test.ts(278,63): error TS2353:
Object literal may only specify known properties, and 'setBy' does not exist in type
'Pick<InvoiceDetails, "number" | "date">'.
```
**Result: PASS** — Producer claimed **1 error** at the cited location. Reproduced exactly. Matches P1 #3.
---
## Gate 3 — `npx vitest run`
**Method:**
```
cd /Volumes/EXT/DEVELOPMENT/WFDASH_NEXTJS && npx vitest run 2>&1 | tail -120
```
Note: `--reporter=basic` is not a valid vitest reporter in this version and errors out;
the default reporter was used instead and reproduces the producer's counts.
**Evidence (failed tests):**
```
❯ src/lib/server/orders-service.test.ts (9 tests | 1 failed) 15ms
× increments the sequence using the latest matching order_number 5ms
❯ src/lib/server/quotes-service.test.ts (59 tests | 4 failed) 35ms
× starts the month at 001 10ms
× increments on each call 2ms
× produces unique numbers under concurrent calls (no race) 3ms
× creates a new quote, customer, company, items, and attachments 1ms
❯ src/app/(app)/collection-pool/page.test.tsx (1 test | 1 failed) 1199ms
× defaults to operational sort and reorders rows when nearby suppliers is selected
Test Files 3 failed | 52 passed (55)
Tests 6 failed | 483 passed (489)
```
**Result: PASS** — Producer claimed **6 failed / 483 passed across 3 files**. Reproduced exactly:
`quotes-service.test.ts` (4), `orders-service.test.ts` (1), `collection-pool/page.test.tsx` (1).
---
## Spot-checks — 5 findings verified against source
### Spot-check 1: P1 #1 — Quote number formatter (`src/lib/server/quotes-service.ts:156-158`)
**Method:**
Read the cited lines and the corresponding unit test.
**Evidence:**
```ts
// quotes-service.ts:156-158
function formatQuoteNumber(year: number, month: number, seq: number): string {
return `Q${year}${String(month).padStart(2, "0")}${String(seq).padStart(3, "0")}`;
}
```
```ts
// quotes-service.test.ts:182 — what the rest of the codebase expects
expect(n).toBe(`Q-${year}-${month}-001`);
```
```ts
// quotes-service.test.ts:367 — n8n v5 contract
expect(result.quote_number).toMatch(/^Q-\d{4}-\d{2}-001$/);
```
The implemented formatter produces `Q202607001`; tests and other surfaces (e.g. `orders/[id]/page.tsx:1691`
`placeholder="e.g. Q-2026-04-001"`) expect `Q-2026-07-001`. Confirmed bug.
**Result: PASS** — Bug is real, exactly as described. Fix proposed (add two dashes) is the minimal correct fix.
### Spot-check 2: P1 #2 — Order number generator (`src/lib/server/orders-service.ts:41-55`)
**Method:**
Read the cited lines.
**Evidence:**
```ts
// orders-service.ts:41-55
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, "0");
const prefix = `ORD-${year}-${month}-`;
const existing = await pb.collection(ORDER_COLLECTION).getList(1, 1, {
filter: `order_number ~ "${prefix}"`,
sort: "-order_number",
});
let seq = 1;
if (existing.items.length > 0) {
const lastNum = parseInt(existing.items[0].order_number.split("-").pop() || "0", 10);
seq = lastNum + 1;
}
```
Confirmed: `prefix` is built from wall-clock `now` only. If the most recent matching order is from a
prior month, the generator will produce `ORD-2026-07-008` instead of `ORD-2026-06-008`. Also confirmed
no atomic counter pattern (no `order_counters` collection mirroring `quote_counters`).
Unit test (orders-service.test.ts:228):
```ts
expect(orders.create.mock.calls[0][0].order_number).toBe("ORD-2026-06-008");
```
**Result: PASS** — Bug is real. Both failure modes (month derivation + atomicity) are present.
### Spot-check 3: P1 #3 — TypeScript excess-property error (`src/app/(app)/collection-confirm/view-model.test.ts:278` vs `view-model.ts:210`)
**Method:**
Read both cited lines and re-ran `npx tsc --noEmit`.
**Evidence:**
```ts
// view-model.ts:210
export function buildConfirmNote(invoice: Pick<InvoiceDetails, "date" | "number">): string {
return `Confirmed for invoicing — ${invoice.number} (${invoice.date})`;
}
// view-model.test.ts:278
expect(
buildConfirmNote({ date: "2026-07-08", number: "INV-2", setBy: "u", setByName: "n", setAt: "t" }),
).toBe("Confirmed for invoicing — INV-2 (2026-07-08)");
```
The `Pick<InvoiceDetails, "date" | "number">` signature rejects `setBy`, `setByName`, `setAt`.
tsc emits `error TS2353` as shown in Gate 2 above.
**Result: PASS** — Bug is real, the proposed fix options (drop fields from test OR widen the type)
are both defensible. Report does not force a single choice — correct posture.
### Spot-check 4: P1 #4 — Custom HTML sanitizer (`src/components/quote-email-card.tsx:166-233`)
**Method:**
Read the cited `sanitizeHtml` function and the call sites.
**Evidence:**
```ts
// quote-email-card.tsx:189
function sanitizeHtml(input: string): string {
if (typeof window === "undefined" || !input) return "";
// ... regex-based strip + per-attribute parse ...
}
// quote-email-card.tsx:303 (sanitize wrapper)
return { __html: sanitizeHtml(raw) };
// quote-email-card.tsx:415 (use site)
dangerouslySetInnerHTML={{ __html: bodyHtml }}
```
Author's own comment at line 171-174: `NOT a general-purpose sanitizer (use DOMPurify for that)`.
Confirmed: input is remote email HTML, output is injected via `dangerouslySetInnerHTML`. The regex
parser on line 211 accepts mismatched quote styles (`'`, unquoted values), and the `href` /
`javascript:` filter on line 220 only catches ASCII `javascript:` — `&#x3A;` (HTML entity-encoded
colon) bypasses it. Real XSS risk in a CRM email pipeline.
**Result: PASS** — Risk is real. Recommended fix (swap to `isomorphic-dompurify`) is the correct
minimal change; 2 vitest payloads (ASCII + entity-encoded) would prove the fix.
### Spot-check 5: P1 #5 — `scripts/setup-pocketbase.ts:50-63` destructive wipe
**Method:**
Read the cited lines.
**Evidence:**
```ts
// setup-pocketbase.ts:4-5
const ADMIN_EMAIL = "david.els1@outlook.com";
const ADMIN_PASSWORD = "1234567890";
// setup-pocketbase.ts:50-63
const allCollections = ["order_items", "quote_items", "purchase_orders", "orders", "quotes",
"suppliers", "company_details", "companies", "customers", "stock_items",
"book_out_records", "logs"];
for (const colName of allCollections) {
try {
const existing = await pb.collections.getList(1, 1, { filter: `name = "${colName}"` });
if (existing.items.length > 0) {
console.log(`Deleting "${colName}" (${existing.items[0].id})...`);
const resp = await fetch(`${PB_URL}/api/collections/${existing.items[0].id}`, {
method: "DELETE",
...
});
```
Confirmed: hard-coded PB admin credentials committed to source (a public repo could leak, but this
is `git config user.name = DieselLog, email = dieselog@local` on a private repo — still bad
hygiene); 12 collections wiped unconditionally on `npm run setup` against
`http://127.0.0.1:8090`. No `--confirm-wipe`, no snapshot, no `dryRun` flag. The `npm run setup`
script is wired in `package.json:12` so a typo could wipe local PB data.
**Result: PASS** — Risk is real. Suggested fixes (env reads + `--confirm-wipe` + snapshot) are the
right minimal shape.
---
## Severity sanity check
Spot-audited classifications against the evidence:
| Finding | Report grade | My read |
|---|---|---|
| Quote formatter produces wrong shape | P1 | **Correct.** Every new quote breaks documented contract; tests fail; downstream n8n indexer reads against the dashed shape. |
| Order generator picks wrong month + race | P1 | **Correct.** Two distinct bugs (month derivation + atomicity), both in production code. |
| TS excess-property in collection-confirm test | P1 | **Correct.** CI cannot pass `tsc --noEmit` — this blocks merges, not just compiles. |
| Custom regex sanitizer with `dangerouslySetInnerHTML` | P1 | **Correct.** Author flags it themselves; bypassable on `&#x3A;` entity-encoded payloads; email is the canonical XSS ingress for a CRM. |
| `setup-pocketbase.ts` unconditional wipe | P1 | **Borderline-correct.** Operator footgun, not a runtime bug. Could argue P2 with hardening, but P1 is defensible because the script is wired to `npm run setup` and one wrong keystroke drops data. |
| 50 lint errors (27 set-state-in-effect) | P2 | **Correct.** Real React 19 smell; CI gate fails but app still runs. |
| 6 vitest failures | P2 | **Correct.** Tied to P1 fixes. |
| All pages `"use client"` | P2 | **Correct.** Performance + first-paint hit; doesn't crash anything. |
| og-image weights | P2 | **Correct.** Cosmetic-ish, but `og-image` is referenced by every social preview. |
| E2E coverage thin | P2 | **Correct.** 5 specs vs 26+ pages. |
| PB filter string interpolation | P2 | **Borderline-correct.** Pattern survives today; would not survive a refactor. P2 reasonable. |
| `lucide-react@^1.16.0` | P2 | **Correct grade.** Report explicitly says "**Not a real bug**, but flag for a fresh install" — accurate (current npm is 1.24.0; 1.16.0 is in range, lockfile pins it). |
| Loose PNGs in repo root | P3 | **Correct.** Repo bloat, no runtime impact. |
| Stray JPG in `scripts/` | P3 | **Correct.** Same as above. |
| Loose debug scripts at `scripts/dev/` | P3 | **Correct.** Pulled into lint scope, not into runtime. |
| `tsconfig` excludes `scripts/**` but ESLint doesn't | P3 | **Correct.** Symptom, not cause. |
| Modal escape-on-backdrop-click | P3 | **Correct.** UX inconsistency. |
| `ThemeBootstrapScript` uses `dangerouslySetInnerHTML` | P3 | **Correct.** Acceptable because content is a literal in the same file. |
**No over-classification spotted.** No cosmetic typo graded P1. No "code smell" graded P0. No
deprecated-but-still-working API graded P0.
---
## Coverage check
| Required dimension | Covered? | Where |
|---|---|---|
| Build | ✓ via P2 #4 + "What's Working Well" #8 (`next.config.ts:4` standalone output) | OK — did not run `npm run build` myself but config + lint/tsc/vitest cover the inputs. |
| Types | ✓ — P1 #3 (TSC excess-property) | OK |
| Tests | ✓ — P2 #2 (6 vitest failures, list included) | OK |
| Next 16 compliance | ✓ — "What's Working Well" #1 (async params) + P2 #3 (all `"use client"`) | OK |
| PB integration | ✓ — implicit throughout (counter pattern, filter escape) | OK |
| PB migrations | ✓ — `scripts/migrations/` (35 files) referenced in P1 #4 verification flow | Not deeply audited; report acknowledges this via "35 migrations exist" |
| Security | ✓ — P1 #4 (sanitizer) + P1 #5 (hard-coded creds) | OK |
| Performance | ✓ — P2 #3 (no SSR), P2 #4 (og-image weight), P2 #2 (27 set-state-in-effect React 19 cascade) | OK |
| a11y | ✓ — P2 #1 lint count includes `jsx-a11y/alt-text` (12 false positives) | OK |
| E2E gaps | ✓ — P2 #5 (only 5 specs vs 26+ pages) | OK |
| Dead code | ✓ — P3 #3 + P3 #4 (loose debug scripts + stray JPG) | OK |
**No missing dimension.** Could be deeper on PB migrations (no per-migration audit), but the report's
scope is "static QA health check" not "full data-layer audit" — appropriate depth.
---
## Missed P0/P1
I deliberately probed for issues the report might have skipped:
- **No additional hard-coded secrets in `src/`.** All `process.env.PB_ADMIN_*` reads are in
`src/lib/server/pocketbase-admin.ts`. Test files use placeholder creds (`secret`, `test-secret-token`,
`admin@example.com`) — correct. Only the `scripts/setup-pocketbase.ts` script has hard-coded admin
creds, which the report already flagged (P1 #5).
- **No `Math.random()` for security-relevant IDs.** Grep across `src/**/*.{ts,tsx}` shows no
cryptographic-strength requirements being met with `Math.random()`.
- **No obviously broken additional tests beyond the 6 cited.** The 3-failed / 6-failed count is exact.
- **`AGENT_INSTRUCTIONS.md` is mis-cited.** The report (line 39) says:
> `AGENT_INSTRUCTIONS.md` line 153-156 and `docs/SHIPPED.md` line 161 both document `Q-YYYY-MM-NNN`.
`AGENT_INSTRUCTIONS.md` does not exist at the project root. The actual files are `AGENTS.md`,
`AGENT_BRIEF_AUTOFILL_QUOTES.md`, `AGENT_BRIEF_AUTOFILL_QUOTES.v1.long.md`. The substantive claim
(`Q-YYYY-MM-NNN` is the documented format) is still correct because `docs/SHIPPED.md:161` does
document it (`/api/v1/quotes/next-number returns Q-YYYY-MM-NNN`). This is a cosmetic citation error
that does not affect the finding's validity.
- **Page count "every file under `src/app/(app)/**/page.tsx` (~24 files)"** — actual is 26
client-component pages. Off-by-2 in a rounded estimate; not a defect.
- **"R2 — lucide-react@^1.16.0, 1.24.0 is current"** — confirmed accurate via
`npm view lucide-react dist-tags` (`latest: 1.24.0`); package-lock pins 1.16.0; `^1.16.0` resolves
to anything `<2.0.0`. Report's "not a real bug, but flag" framing is correct.
**No P0 missed. One minor citation inaccuracy on `AGENT_INSTRUCTIONS.md`.**
---
## Adversarial probes (mandatory)
I exercised 3 adversarial probes to try to break the audit:
1. **Probe: are the lint counts being reported as one big number or actually broken down by file?**
Verified that `npm run lint` produces a per-file breakdown (`src/components/order-form.tsx`,
`src/components/quote-pdf-viewer.tsx:47`, etc.) matching the report's top-offender list. Not a
bulk false-positive count — real files, real rules.
2. **Probe: are the vitest failures reproducible at the file+line level?**
Verified. Each of the 6 failures has a stable file:line citation. The `quotes-service.test.ts:182`
failure is the same root cause as `:192`, `:207`, `:367` — the report correctly collapses these to
"4 failures, 1 root cause" in P1 #1.
3. **Probe: is the `dangerouslySetInnerHTML` actually wired to `sanitizeHtml` end-to-end, or is the
report citing a dead code path?**
Verified. `sanitizeHtml` is defined at line 189, wrapped at line 303 (returns `{__html: sanitizeHtml(raw)}`),
and consumed at line 415 (`dangerouslySetInnerHTML={{ __html: bodyHtml }}`). Production XSS surface, not a
speculative finding.
---
## Recommendation
**Accept-with-fixes** — the audit itself is sound and should be merged as-is. The one cosmetic
citation inaccuracy (`AGENT_INSTRUCTIONS.md` → `AGENTS.md` / `AGENT_BRIEF_AUTOFILL_QUOTES.md`) is
trivial and does not affect the substance.
The producer's recommended Next Steps (lines 314-322) are sequenced correctly: 1-line `formatQuoteNumber`
fix first, then the `view-model.test.ts` test/type fix, then the order-month clarification, then
DOMPurify swap, then server-side auth gating, then lint-zero, then E2E gap-fill. Agree with the
ordering — small fixes before broad perf work.
---
VERDICT: PASS