Skip to content

Pre-run findings — defects found while writing the E2E suites

No test has been executed. Every item below was found by reading source in order to write an accurate test case, on 2026-08-19. That makes them cheap and it makes them unconfirmed: each needs a run to become a defect report with a repro. They are recorded here so they are not lost between "someone noticed" and "someone filed it".

Routing column = the module whose registry should carry the fix. None has been filed as an item yet — that is a decision for whoever owns the backlog, not for the test harness.


Status after the 2026-08-19 fix pass

FindingState
P0 #1 audit hash chainfixed — canonical recursive key sort; new test writes through Postgres and verifies from storage (fails 2/3 against the old hash, so it is a real regression test). Seed workaround removed. Pre-existing rows: decision recorded as dev-only re-seed, no re-hash script.
P0 #2 validationSummaryfixed both ends — API exposes it on every permit-detail payload; the review screen renders it. An invented client-side view model the API never sent was deleted. Scope is safety readings only and the UI now says so.
P0 #3 reject signatureclaim removed, not honoured — the API accepts { reason } only, so nothing was invented. Success copy no longer claims the signature was logged. Filed as GAPS.md row V5. Still unfiled in the API repo's feature_list.jsonCONTEXT.md §6 wants it mirrored there.
4 QR never renderedfixed — QR now rendered, gated on the live GET /:id/qr rather than on qrIssuedAt. Verified: a CLOSED permit keeps qrIssuedAt but the endpoint answers 403 PERMIT_NOT_ACTIVE, so the obvious gate would have shown a broken card.
5 CSV bypasses filterfixed — narrowing extracted to one helper used by both the fetch and the export, so they cannot drift again.
6 backend English on screenfixed at the root, not the symptoms — the defect was HandleLoading.ts's default error callback, leaving all 11 call sites one missing argument from leaking. Default now routes through mapError.
7 wizard stubsno fix needed, by design. Unchanged.
8 hardcoded Thaifixed and swept — beyond the two reported: ProfileCard.vue, and eleven route meta.title literals that render as the browser tab title.
9 paginationfixed — queue and audit pagers restored; All Permits is now fully server-driven, sorting included. See the correction below.
10 CSV exports selectionfixed — both exports offered, with a warning when the server returns fewer rows than it reports.
11, 12, 13, 15withheld deliberately — product decisions. See the triage table in REVIEW-2026-08-19.md.
14 offline queuefixed — promoted to a Pinia store; useOfflineQueue() kept as an adapter so all five call sites read unchanged.

Correction to finding 9, from the agent that fixed it: my "no pager below 1024px" was wrong.Paginate.vue does have a mobile pager (flex lg:hidden). Only the rows-per-page selector and the row total were lg-only. The real defect in that row was the second half — All Permits sending no page/limit and being silently bounded by the server's default page size.

Nothing here has been executed as a test run. These are code-level fixes with unit and smoke evidence. The manual E2E suites remain unrun.


P0 — integrity and safety rules that do not hold

#FindingRepo / routing
1The audit hash chain fails verification against the database. Prisma stores payload as Postgres jsonb, which normalises object key order; computeHash() hashes the key order it was handed. Any audit row with a multi-key payload therefore fails verifyAuditChain() when recomputed from storage — this is production behaviour, not a fixture artefact: PERMIT_CLOSED writes { signature, checklist } and CERT_BLOCKED writes { workerName, reason }. audit-log.spec.ts misses it because it verifies in-memory rows that never round-trip through Postgres. The chain is the product's tamper-detection mechanism, so "the audit log is append-only and verifiable" is currently not demonstrable. Fix is canonical key sorting in computeHash(), which rewrites every existing hash.smart-work-permit-api — already noted in its session-handoff.md
2The Safety Officer review screen cannot display the backend's verdict, because nothing fetches it. validationSummary is not in the typed contract and no provider requests it. The product rule is that this screen displays the server's pass/fail and never recomputes it — a screen that never receives the verdict cannot display it. SO-REVIEW-004 is written to run today: with no verdict in the payload the screen must render no pass/fail rows, and any green/red row proves client-side recomputation.smart-work-permit-frontendsafety-officer, plus a contract question for the API
3Reject captures an e-signature and throws it away. The reject flow validates a typed signature, then sends { reason } only — the name never leaves the browser, while the success copy states it was logged. On a safety rejection that is an audit-trail claim the system does not honour.smart-work-permit-frontendsafety-officer

P1 — the UI states something untrue, or a rule is unenforced

#FindingRepo / routing
4Approve announces a QR that is never rendered. PermitService.qr() is not called from any Safety Officer page; "QR issued" is only verifiable via qrIssuedAt in the approve response.smart-work-permit-frontendsafety-officer
5History CSV export bypasses the table's own filter. useHistory.exportCsv maps response.data directly and skips the ARCHIVE_STATUSES narrowing useFetchHistory applies. With status "all", the table shows CLOSED/EXPIRED while the CSV also carries DRAFT/PENDING/ACTIVE. Predicted from source; CT-HISTORY-009 asserts CSV-matches-table. Note GAPS.md row B explains why client-side narrowing exists — it does not license the export to skip it.smart-work-permit-contractor-frontendhistory
6Backend English reaches the screen on three paths. useLogout, and ResetPasswordPage's useResetPassword / useCheckToken, call handleLoading with no error callback and fall through to a default that toasts the backend's own message. Every other screen passes mapError. Cheapest trigger: an invalid reset token. This violates a stated cross-repo invariant (CONTEXT.md § 2).smart-work-permit-contractor-frontendplatform
7Wizard steps 3–6 are stubs whose schemas always validate (z.object({})), so the wizard walks to step 6, nothing ever blocks Next, and Submit enables then writes a console.info. Not a defect — the screens are PMT-006009, unbuilt — but it is a trap: a tester sees a complete-looking wizard with no validation. Recorded in the plan and in CT-WIZARD.md so nobody files it as broken.plan/doc only — no fix needed

P2 — real, smaller

#FindingRepo / routing
8Hardcoded Thai strings that stay Thai in the en UI: GasLog.schema.ts validation messages, and All Permits' rows-per-page label.smart-work-permit-frontendinspector, safety-officer
9SO-QUEUE and SO-AUDIT render no pager at all (page permanently 1, limit 10). All Permits' pager is hidden lg:flex — no pager below 1024px — and that screen sends no page/limit, so the register is bounded by the server's default page size.smart-work-permit-frontendsafety-officer
10CSV export on All Permits exports the selection, not the filtered view (the control only appears with rows selected). Reasonable behaviour, but not what the spec says.smart-work-permit-frontendsafety-officer
11Risk-map pins are coloured by permit type, not status; status appears only in the tap-through modal. Pin position is a hash of free-text location, so it carries no spatial meaning.smart-work-permit-frontendsafety-officer (or a product call)
12Entrant register is "who is inside now", not an in/out log: rows are {workerName, checkedInAt}, direction is hardcoded IN, and a successful check-out is observed as the row disappearing. No out-timestamp is retained.smart-work-permit-frontendinspector (or a product call)
13Wizard draft does not rehydrate: useWizard is page-scoped with no load-on-mount and no draft id in the route, so a reload gives an empty wizard while the draft sits server-side — and continuing creates a second draft. There is no draft-edit entry point at all. Whether it should resume is a product question.smart-work-permit-contractor-frontendpermit
14useOfflineQueue() is per-component rather than a store, so the nav badge holds its own instance and may not update on an enqueue made elsewhere.smart-work-permit-frontendinspector
15Audit-log date filter sends a bare YYYY-MM-DD against UTC storage with +7 display, so boundary rows are undefined behaviour. Contract question rather than a defect.cross-repo — CONTEXT.md § 3

Open questions raised by the suites

  • Does a session cookie survive the Contractor app's client-side role refusal? Login returns 200 and LoginPage rejects on user.role; whether the cookie is cleared is unverified (CT-AUTH-002).
  • Should a wizard draft resume on reload (finding 13), and should History's filter persist across reload? Neither is specified by any item.
  • The dashboard's "expiring soon" panel: GAPS.md api-adds 10 means the client's fallback date-math puts an already-expired certificate into the expiring-soon list, since a past date is trivially within 30 days.

Documentation drift corrected on 2026-08-19

Found while writing these suites; all fixed, listed so nobody re-reports them:

  • smart-work-permit-frontend/AGENTS.md called the Safety Officer and Inspector modules "scaffold only — placeholders" (they are ~2,000 and ~1,300 lines of shipped screens), said of the main flow "None of it is built yet", and called the locale switcher "still planned — PLT-005" when it ships in DefaultLayout.vue.
  • This plan claimed "the wizard cannot get past step 2" — wrong, and in the dangerous direction (see finding 7).
  • The reference suites used five-level relative paths that resolve above the workspace root; four is correct.
  • The plan's environment section told testers to docker compose up -d in the API repo; that compose file defines only the api service and references env vars that are not set. Corrected.
  • smart-work-permit-contractor-frontend/CLAUDE.md still says ./init.sh "is red today" (it is green) and that the history module is not built (it is registered and feat-003 is done). Not yet fixed — that repo's docs were not in this pass's scope.