fontkit caches glyphs per font for the whole process and pdfkit embeds composite components with no code points, so setting ':' before '.' in Poppins corrupted every later render in the same process (text layer showed '1,200;00', line breaks shifted). primeFontGlyphs() now creates the component glyphs with their cmap code point at font registration, awaited by renderCore and browser init. Regression test and Quirk 13 added; the warm-up workarounds in the template tests are removed.
8.5 KiB
Voiced: handoff
Written 2026-10-04. Voiced is the invoicing app built by Bongbetic for vendor Arun P (Tauri v2, React 19, IBM Carbon, @react-pdf/renderer 4.9.0, SQLite via rusqlite).
Where things are
| Item | Location |
|---|---|
| Repository (execute from here) | /media/Toto/Documents/bongbetic/invoice |
| Approved plan | /home/soubarna/.claude/plans/take-a-look-at-logical-sunbeam.md |
| Working branch | phase-c-templates (remote origin, git.bongbetic.com/xavierk/Voiced) |
| Template specs (13, measured from Canva PDFs) | docs/templates/ (README.md plus 01–13) |
| Canva source PDFs (never commit) | templates/Modern Neutral Invoice Template/1.pdf–13.pdf |
| Bongbetic logos | /media/Toto/Documents/bongbetic/Logo/bongbetic-brand/ |
| Full prior conversation | /home/soubarna/.claude/projects/-media-Toto-Documents-bongbetic-invoice/1cd7ecdc-a943-4728-bbf5-efb12a36b04e.jsonl |
Continue all work from /media/Toto/Documents/bongbetic/invoice, on branch phase-c-templates. Read the plan first. It holds the phase breakdown, quirk list, GST rules, ERPNext design, risks and the Verification section. Create new phase branches from the previous one (the existing branches are stacked: phase-0-hotfix → phase-a-foundation → phase-b-engine → phase-c-templates). Never stage templates/ or .commandcode/.
Progress
The plan totals about 108 developer-days (0: 1.5, A: 10.5, B: 33, C: 34, D: 5, E: 12, F: 10, G: 2). Phases 0, A, B and C hold about 79 of those days of scope, roughly 73 %. D to G remain, about 29 days of scope.
| Phase | Scope | State |
|---|---|---|
| 0 | Header-overlap and logo hotfix | Done, pushed (phase-0-hotfix) |
| A | DB migrations M1–M5, issue/cancel lifecycle, GST derivation with integer-paise maths, drafts, shortcuts | Done, pushed (phase-a-foundation) |
| B | Render engine (worker, pdf.js preview, searchable and flattened export, archive store, history re-export, unattended self-test) | Done, pushed (phase-b-engine) |
| C | Logo pipeline and Branding settings, template picker, Classic plus all 13 Canva templates, glyph-cache engine fix | Done. Templates pushed through 5795087; the glyph fix is in the commit that follows it |
| D | Font import for paid fonts, Void font packaging | Not started |
| E | Workflow QoL | Not started |
| F | ERPNext integration | Not started |
| G | Release | Not started |
Templates implemented: Classic, Neutral, Ledger (family), Band (family), Northline family (Purple Pop, Citrus Split), and the standalone Monolith, Cobalt Stripe, Marble and Highlighter. That is 14 in total. Each has tests, an "Implementation notes" section in its spec, and a thumbnail in public/templates/.
Other gates last run at 5795087: npm run fonts:verify OK (11 families, 33 fonts); npm run logo:measure idempotent.
Glyph-cache fix (done)
fontkit caches glyph objects per font for the whole process, and pdfkit creates a composite glyph's components at embed time with empty code points. Poppins Regular's ":" contains ".", so setting ":" before any "." corrupted the period for every later render in that process (amounts extracted as "₹1,200;00", some line breaks shifted). The render worker lives for the whole app session, so the app was affected, not only the tests. Every bundled face has composite glyphs, not just Poppins.
Fix: primeFontGlyphs() in src/pdf/fonts/register.ts loads every registered face and creates, with their cmap code point, only the glyphs that composites use as components (private-use code points skipped, lowest code point wins). It is memoised and awaited in renderCore (src/pdf/render/core.ts) and in initBrowserRendering (src/pdf/render/browserInit.ts). A first attempt that primed every cmap glyph broke Inter's contextual hyphen (the hyphen in AP/2026-001 vanished from the text layer), so do not widen it. It uses fontkit private APIs (_getBaseGlyph, _decode, _cmapProcessor, _glyphs): re-check it on any fontkit upgrade.
Cost: about 620 ms once per process under Node. Per-render cost is unchanged. Worker init time was not measured separately; the whole real-webview self-test took 3.4 s.
Regression coverage: src/pdf/fonts/glyphPriming.test.tsx (new) and Quirk 13 in src/pdf/quirks.test.tsx. The warm-up workarounds were removed from the Highlighter test and the standalone sweep. Monolith-then-Highlighter and Highlighter-then-Monolith now give identical layout fingerprints. No golden changed.
Last verified gates (with the fix, 2026-10-04): npm run build OK; npm test 46 files, 1539 passed, 6 skipped, 385 s; real-webview self-test ok: true with both fingerprint-vs-golden checks passing.
Next steps, in order
- Small template fixes from the C6a/C6b reports:
- Monolith and Cobalt Stripe: when only the closing block moves to page 2, the page-1 squeeze can fall a few points short. Marble and Highlighter already have the fix.
- Monolith: the flow notes block can collapse to zero-height lines when it starts with less than a line of room. Marble guards against this.
- Phase D:
- Font import for Now, Gotham and Open Sauce overrides: a
user_fontsstore, with WOFF2 and variable fonts refused. - Void xbps font install to
/usr/share/fonts/voicedwith licences,font_dirsplus INSTALL/REMOVE scripts that runfc-cache. Verify thatxbps-createhonours INSTALL/REMOVE. - Final visual review of all 14 templates against Canva.
- Font import for Now, Gotham and Open Sauce overrides: a
- Phase E: clients page (structured addresses, GST category), item presets, invoice detail view, History DataTable with filters, payments with TDS, settings tabs and toasts, backup/restore (db, assets, archive, fonts, with a round-trip test), CSV and JSON export.
- Phase F: ERPNext (Rust
integrations/erpnext, reqwest 0.13.5 with rustls ring, config UI, push as draft or submit, PDF attachment, idempotency, payment entries,voiced.invoice.v1JSON export). End-to-end test with podman frappe_docker, with and without India Compliance. - Phase G: README (data paths, fonts and licences, ERPNext recipe), version bump, xbps template refresh, NSIS build within the size budget, self-test on both installed builds.
Open decisions and manual checks
- Canva licence for re-creating the templates is unconfirmed. Ship only in this vendor's build until it is.
- Wordmark replaces the whole logo lockup (a toggle is planned).
- A CA should confirm the SAC and signature rules.
- Open Sauce One is not bundled (Poppins 800 stands in). Plex and Noto are installed as identical duplicates of Void's packages.
- Picker family label: Monolith, Cobalt Stripe, Marble and Highlighter show as "Standalone". Change if wanted.
- Monolith shows the supplier's full address only on the last page of a multi-page invoice (as in Canva). Say if page 1 should carry it too.
- Not yet checked: whether the app surfaces layout audit errors to the user.
- Never tested: Windows/WebView2 (no Windows machine); the app window UI has only been exercised headless, never opened visually (Branding UI and template picker need a look); the B4/B5 manual checklists on an installed build.
- The P2 extras are deferred (copies, watermarks, PDF/A, accent override, print-friendly mode, Wide/Custom margins, user textures, SVG logo import, PNG flatten, UTGST labels beyond the supplier-state rule, CESS, composition and SEZ).
How to run things
- Build:
npm run build. Tests:npm test(about 7 minutes; run on a quiet machine, since load can starve the sweep tests). - Fonts:
npm run fonts:verify. Logo placeholders:npm run logo:measure(must leaveslots.generated.tsunchanged on re-run). Thumbnails:npm run thumbnails. - Template fidelity:
scripts/templates/compare.mjsrenders Canva and Voiced side by side (default output/tmp/invoice-c6b). - Real-webview self-test: build with
npm run tauri -- build --debug --no-bundle --features custom-protocol, then runbash /tmp/run-selftest.sh. The report goes to/tmp/voiced-e2e/report.json. If/tmp/run-selftest.shis gone, recreate it from the self-test notes in the plan (it launches the built binary with the selftest config and waits for the report). - Void package:
packaging/void/build-xbps.sh. Windows NSIS: cross-compile with cargo-xwin (see the earlier commit "Document the Linux -> Windows NSIS cross-compile setup").
Working conventions
- Chat uses caveman mode. Commits, docs and this file are normal English.
- Explore with the
code-exploreragent, implement withcode-runner. Usepodman, not docker. Usenpx ctx7@latestfor library docs. - Commit locally after the gates pass, then push the feature branch. Do not relaunch an agent the user stopped without asking.