- 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
17 KiB
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:
// 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")}`;
}
// quotes-service.test.ts:182 — what the rest of the codebase expects
expect(n).toBe(`Q-${year}-${month}-001`);
// 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:
// 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):
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:
// 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:
// 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: — : (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:
// 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 : 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/. Allprocess.env.PB_ADMIN_*reads are insrc/lib/server/pocketbase-admin.ts. Test files use placeholder creds (secret,test-secret-token,admin@example.com) — correct. Only thescripts/setup-pocketbase.tsscript has hard-coded admin creds, which the report already flagged (P1 #5). -
No
Math.random()for security-relevant IDs. Grep acrosssrc/**/*.{ts,tsx}shows no cryptographic-strength requirements being met withMath.random(). -
No obviously broken additional tests beyond the 6 cited. The 3-failed / 6-failed count is exact.
-
AGENT_INSTRUCTIONS.mdis mis-cited. The report (line 39) says:AGENT_INSTRUCTIONS.mdline 153-156 anddocs/SHIPPED.mdline 161 both documentQ-YYYY-MM-NNN.AGENT_INSTRUCTIONS.mddoes not exist at the project root. The actual files areAGENTS.md,AGENT_BRIEF_AUTOFILL_QUOTES.md,AGENT_BRIEF_AUTOFILL_QUOTES.v1.long.md. The substantive claim (Q-YYYY-MM-NNNis the documented format) is still correct becausedocs/SHIPPED.md:161does 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.0resolves 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:
-
Probe: are the lint counts being reported as one big number or actually broken down by file? Verified that
npm run lintproduces 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. -
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:182failure is the same root cause as:192,:207,:367— the report correctly collapses these to "4 failures, 1 root cause" in P1 #1. -
Probe: is the
dangerouslySetInnerHTMLactually wired tosanitizeHtmlend-to-end, or is the report citing a dead code path? Verified.sanitizeHtmlis 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