Date: 2026-11-13 · Starting commit e286e93 · Phase 1–4 feature set, pre-release hardening pass.
- Evidence:
index.htmlplaces<dialog id="modal">as a sibling of<div id="app">, but the delegated click handler was bound to#app. Anydata-*button inside a dialog never reached it — the template line "Add line" / "Remove line" buttons did nothing, and the failure was invisible (no JS error). The pre-existing "remove line works" check passed only because it asserted a no-op. - Fix: delegated handler moved to
document, the single ancestor of both the page and its dialogs. The workspace test's mock DOM was updated to modeldocument.addEventListener, which the real DOM has always provided. - Verified: template add/remove line now change the row count 1→2→1; full UI audit 251 checks / 0 failures.
- Evidence: the reminder dialog opened empty (
dialog.innerHTML.length === 0) after submitting the previous dialog.<dialog>.close()fires itscloseevent asynchronously; clearingmodal.innerHTMLinside that handler ran aftertextDialog()had already populated the replacement dialog, erasing it. - Fix: clear
modal.innerHTMLat the start ofmodalForm()instead of in the close handler. Each new dialog now begins from a clean slate regardless of when the old close event lands. - Verified: reminder → message dialog renders the full text in a real browser.
- Evidence: browser console on saving a template. The template functions were added to
core.jsbut never added to theapp.jsimport list. - Fix: import completed; template create/use/edit/delete verified end-to-end (18/18 browser checks).
- Evidence: using a template opened the editor with
price=350000.00for a R3 500 line. The editor draft stores human units and multiplies by 100 on load; the template's stored cents were written straight into it. - Fix: convert with
decimal()when writing the draft. Verified the editor shows3500.00and the resulting invoice carries the correct amount.
- Evidence:
parseBackupthrewInvalid logo…on backups written beforelogo/accentexisted.s.business.logo !== nullis true forundefined, and the empty-business comparison used strictJSON.stringifyequality. - Fix: normalise with
s.business={...emptyState().business,...s.business}and use loose!= nullchecks, so pre-branding backups validate and gainnulldefaults.
- Evidence: a real browser kept serving the previous
core.jsfrominvoiceflow-v048, so fixes appeared not to work.sw.jsprecaches./core.jsand./app.jsunder a name that only changed when someone remembered to change it. - Fix: cache key bumped to
invoiceflow-v049;activatealready deletes every otherinvoiceflow-*cache. Verified the browser then served the new modules.
- Evidence: real-browser reproduction.
new-invoiceranroute={page:'invoice',id:newId}; editLines=[]; clearDraft();—clearDraft()executed afterroutewas reassigned, sodraftKey()(which reads the current route) deleted the draft keyed to the saved invoice id instead of the keyed-to-undefineddraft being edited. The stale draft survived, was reloaded on the next "New invoice", andpricingFields()calledbreakdown()on it. - Fix:
clearDraft()now runs beforerouteis reassigned. Independently, the guard inpricingFieldswas tightened fromi?toi?.lines?so an empty draft cannot reachbreakdown(). - Verified: second, third, fourth and fifth invoice creations all succeed with zero JS errors (
tests/harness/stress-audit.mjs).
I-005 — Settings page threw ReferenceError: when is not defined after any encrypted backup (P1, FIXED 2026-11-13)
- Evidence: control audit stack
backupLabel@app.js:23:314 ← settingsPage ← renderPage ← render.backupLabel()declaredconst wheninside atryblock but referenced it in thereturnoutside that block, so the function threw wheneverstate.lastBackupAtwas set — i.e. after any encrypted backup. Reachable fromnew-invoice, which redirects to Settings when the business name is empty. - Fix: hoisted to
let when=''before thetry. Swept the whole codebase for the sametry{const …}scoping pattern; this was the only offender. - Verified:
#last-backupnow rendersLast backup: just now (13 Nov 2026)instead of throwing.
- Evidence: rendered document read
Payments recorded R 0,00andCredit notes R 0,00on every unpaid invoice, printedValue excluding VAT/VAT 0%to non-VAT businesses, and carried the internal note "Draft only — not yet issued." inside the customer-facing document. - Fix: zero-value rows are now conditional; the VAT breakdown is suppressed for non-VAT businesses; the internal draft note was removed (the title already carries
· DRAFT). Payment instructions now repeat the due date, matching standard invoice layout. - Verified by rendering the document and reading it back.
- Evidence:
package.json: 0.4.0vs README/PRODUCT-DETAILS.md: 0.4.0vs service-worker cacheinvoiceflow-v048-phase4-20261112. The cache key had been bumped for Phase 4 while the packaged version was not. - Fix: aligned to 0.4.8 across
package.json,README.md,PRODUCT-DETAILS.mdand the service-worker cache key (invoiceflow-v048). Verifiedpackage.jsonparses andnpm run checkpasses.
- Evidence: file absent at the 2026-11-06 register;
.gitignorecarried an exception for it that nothing satisfied. - Fix:
.env.exampleis now committed and documents that the static app needs no variables, with reserved notes for future server-side features.
- Evidence:
npm run test:uiwas not executable at the 2026-11-06 register. - Fix:
vitestis a dev dependency andnpm run test:uiruns 3/3 UI tests;npm testnow runs 135 domain tests plus those 3.
- Evidence: real-browser measurement at 768 px showed
.cash-chartscrollWidth 236 px against a 205 px client width, withtable/thead/trelements extending past the viewport. Cause:.chart-group{flex:1}had nomin-width:0and.barwas fixed at13px, so the six month-groups could not shrink below their content size. - Fix:
min-width:0on.chart-group;.barchanged toflex:1 1 0; max-width:13px; min-width:2px. Verified by a 229-check responsive sweep across 11 pages and 10 viewport widths with zero overflow.
- Evidence: the browser audit existed only in a temporary scratch directory that is pruned after 24 hours, so no one else could re-run the verification.
- Fix:
tests/harness/browser-harness.mjsandtests/harness/ui-audit.mjsadded to the repository withnpm run test:visual(live site) andnpm run test:visual:local(servesdist/itself). The harness covers the responsive sweep, dark mode, the editor draft guard, the expense-photo dialog, detail views, accessibility naming and offline readiness.
- Local IndexedDB only — no cloud sync/account/auto-backup; README warns to export per-workspace backups.
- Offline/PWA limits: service worker needs HTTPS + first successful load; recurring drafts only generate while browser is open.
- VAT numbers format-checked only (
^4\d{9}$), not SARS-verified; no tax advice. - Android not Play-ready: unsigned if
ANDROID_UPLOAD_KEYSTORE_*env unset;minifyEnabled false. - CIPC reminder estimates the annual return at the end of the calendar year; the exact anniversary must be confirmed on the CIPC website.
src/lib/validation.jswas removed on 2026-11-13. It duplicated email validation already performed bycheckPartyindist/core.json every save, and itssanitizeInputwas a naive angle-bracket strip that must never be used as an XSS defence. Production escaping is theesc()helper indist/app.js. The vitest UI suite now tests the realvalidateStatepath instead.