Unreleased
Unreleased
Phase 4 — Acquisition Surface — fully shipped this session
(Waves 0–6 of 6). UAT 13/13 pass with 0 issues. Awaiting
/gsd-complete-phase 4 to roll into [0.4.0].
Fixed
-
PWA service worker serving stale builds —
CacheFirstwith 30-day TTL on all JS/CSS/HTML meant users were stuck on old builds after deployment. Switched toStaleWhileRevalidate(serve cached, refresh in background; 7-day TTL), addedcleanupOutdatedCaches: true,skipWaiting: true, andclientsClaim: trueso new SWs activate immediately across all open tabs. Addedno-cache, no-storeheader forsw.jsandworkbox-*.jsinvercel.jsonso browsers always check for SW updates instead of serving a stale worker file from Vercel's CDN. -
Magic link login broken (cross-browser) — PKCE flow (
flowType:'pkce') requires thecode_verifierto be in the same browser storage where the magic link was requested. When users click the link from an email client that opens a WebView or different browser, the verifier is absent and the exchange silently fails, showing "The login link is invalid or has expired." RevertedflowTypeto'implicit'insupabase.tsso Supabase redirects with tokens in the URL hash, which works across any browser or context. -
Magic link login broken (double exchange) —
AuthCallback.tsxcalledexchangeCodeForSession()manually 100ms after mount, butdetectSessionInUrl:truealready auto-exchanges the code on Supabase client init. The second call failed with "code already used",setError()fired aftersetSuccess(), and the error UI replaced the success state. Fixed by removing the manual exchange call entirely, subscribing toonAuthStateChangeas the primary path, and adding a 4-secondgetSession()fallback guarded by ahandledflag to prevent double-firing. -
CI: backend migration test shim —
back/tests/sql/00_test_auth_shim.sqlnow creates theanonrole alongsideauthenticatedandservice_role, fixingERROR: role "anon" does not existwhen CI runs Supabase migrations against a plain Postgres 15 container. -
CI: SSG build missing Supabase env — added placeholder
VITE_SUPABASE_URL,VITE_SUPABASE_ANON_KEY, andVITE_API_URLenv vars to the build step in.github/workflows/ssg-checks.yml, preventing the prerender crash caused by thesupabase.tsmodule-load guard. -
AuthContext session-expiry tests — two tests were asserting
sessionExpiringafter 25 min, a leftover from a previous 30-min idle timeout; corrected the timer advance to the real threshold (IDLE_TIMEOUT_MS − WARNING_BEFORE_TIMEOUT_MS= 7h 45m) so both tests pass against the current 8h / 15min constants.
Added
- US 1040-NR treaty-rate auto-lookup: the
/api/forms/f1040nr/generateendpoint now resolves the Schedule NEC dividend rate from the filer's residence country (new optionalresidence_countryon the request, falling back to citizenship/foreign-address) viaservices.tax_calc.resolve_rate. The rate is applied only when a real treaty exists; countries with no US treaty keep the 30% statutory rate, and a per-dividendtreaty_ratealways wins. (ITIN/W-7 and Form 1042-S guidance is surfaced on the cover sheet via thef1040nrregistry note.) - Form filability status: a per-form registry (
back/services/form_status.py) classifies every generated form as an official fileable form, a worksheet to transcribe into an official e-filing, or an application letter. The PDF disclaimer cover sheet now shows a matching status banner, aGET /api/forms/{form_code}/statusendpoint exposes it, and the Claims page shows a status chip. Stops presenting non-fileable reconstructions (ES Modelo 210, DE Anlage KAP, UK SA106, FR 2047, IT, PL, CH, etc.) as official filings. vite-react-ssg+@mdx-js/rollupbuild pipeline; 7 public routes (/,/pricing,/security,/sources,/privacy,/terms,/changelog) prerendered with title, meta, OG/Twitter, JSON-LD, and persistent disclaimer.- All 12 legacy programmatic-SEO routes (
/tax-guide/*,/popular-*,/recent-interest-bonds, etc.) ported onto the same SSG pipeline — 455 prerendered HTML files (was 15) with strictly richer content vs the legacyprerender.mjstemplate-injection approach. - Single-pipeline
sitemap.xml(front/scripts/build-sitemap.mjs) covering all 455 routes. - Landing page rewritten (
Index.tsx): SSR-safe Hero with worked-example metric, TrustStrip, ValueProp, HowItWorks, FAQ, dual disclaimer (hero-adjacent + persistent footer), Organization + WebSite + SoftwareApplication JSON-LD. LandingCalculatorWidget(4-input embedded calculator) calling/api/calc/refundanonymously with localStorage-persisted inputs.- Anonymous-to-authenticated claim migration:
migrateAnonClaim.ts(extracted async function, idempotent viaSet<userId>guard) wired intoAuthCallback(await-before-redirect),AuthContext.onAuthStateChange(fire-and-forget), andAppShell(defense-in-depth). ?next=redirect allow-list ({checkout, dashboard}) protecting the post-auth navigation path./sourcespage with build-time citation table generated fromback/data/rates/{treaty_rates.json,deadlines.json}provenance fields (build-sources-table.mjsprebuild step)./changelogpage parses repo-rootCHANGELOG.mdviabuild-changelog.mjsprebuild; per-entry JSON-LDArticleobjects./securityMDX page with infrastructure table and vulnerability-reporting section./privacyand/terms(TSX) with full GDPR-aware policy / ToS copy./pricingpage with Free vs Pro comparison table generated frompricing.ts(mirrors backendPLAN_LIMITS), 6-entry FAQ accordion, Product + Offer JSON-LD, signed-in-aware CTA: anonymous →/register?next=checkout, authenticated → BillingPage Stripe Checkout.- Cross-language
PLAN_LIMITSdrift detection: Python pytest atback/tests/services/test_plan_limits_sync.pyreadsfront/src/content/pricing.snapshot.json, asserts keysets match backendentitlements.PLAN_LIMITS. CI fails on drift. - BillingPage auto-fires Stripe Checkout when mounted with
?next=checkoutquery, completing the post-auth conversion path. - 14 build-artifact validators (
check-prerender.mjs,check-jsonld.mjs,check-sitemap.mjs,check-csr-routes.mjs,check-disclaimer.mjs,check-mdx-pages.mjs,check-mobile-overflow.mjs) wired into thenpm run verify:prerenderaggregate and a new.github/workflows/ssg-checks.ymlCI workflow. - Frontend test scaffolding for V-01..V-14 + V-07b validation anchors (apiClient anonymous-Authorization-header pin, widget submission, localStorage persistence, anon-claim migration ordering, pricing rows derivation).
- Mobile-360 V-12 layout assertion (Plan 04-09 / D-15 / H1): Playwright
- chromium installed,
front/playwright.config.tsandfront/tests/mobile-360.spec.tsiterating the 10 D-15 surfaces at 360x640 and assertingscrollWidth <= innerWidthplus pricing-CTA tap-target >= 44px (M4 / WCAG 2.2 AA). Newnpm run test:mobilescript. Structural-overflow lint (check-mobile-overflow.mjs) is green and bundled intoverify:prerender.
- chromium installed,
Changed
- Migrated all programmatic-SEO routes from the legacy template-injection
prerender.mjsscript ontovite-react-ssg. - Pinned
react-helmet-asyncto^1.3.0forvite-react-ssgcompatibility (v2 ESM/CJS interop fails with the current SSG runner). - i18n SSR preload: server-side EN namespace mirroring under
front/src/i18n/locales-en/to prevent translation-key leakage into prerendered HTML on programmatic-SEO routes. - Build script: added
prebuild(sources + changelog generators) andpostbuild(sitemap) steps;buildnow usesvite-react-ssg build.
Fixed
- FR 2047 / UK SA106 foreign tax credit cap: the credit is now capped at the
residence country's tax on the foreign income (FR: impôt français
correspondant; UK: UK tax on the income), preventing over-claiming. The FR/UK
endpoints accept an optional
residence_tax_on_income; when provided, the credit shown is the lower of the treaty-rate foreign tax and that figure, with both lines displayed. When omitted, the figure is shown with a prominent limitation note rather than silently overstated. - Remaining-forms audit (DK/FI + template-fillers, web-verified): FI Form 16 retitled to 16B ("Selvitys ulkomaantuloista (pääomatulot)", code 3062) with a credit-cap caveat (it was a bare "Form 16"); SE se-dividend reclassified as a worksheet (the default endpoint emits a reconstruction; the real SKV 3740 is an XFA PDF needing Adobe Reader); DK 06.030 registry note now points to the online portal (udbytterefusion.skat.dk), the residence-cert/ DKK-bank requirements, and the 5-year deadline. The template-filler field maps (NL a12119, CA NR7-R, AU NAT, JP 250, FI 6167) were verified to reference only real AcroForm fields — no silent drops.
- Residence-credit forms audit (web-verified against official sources): the
form-status registry now carries the verified channel/deadline/caveats for
IT/PL/AT/BE/IE/NL/SE/NO, and the generators were corrected where they
misstated the form or credit:
- SE: relabelled K4 (SKV 2104, securities-sales form) → SKV 2703 "Avräkning av utländsk skatt", the actual foreign-tax-credit form.
- BE: flagged that QFIE is not available on foreign dividends (interest/ royalties only, narrow France-treaty exception) — dividends are taxed at 30% on the net and reported in code 1444/2444.
- IT: flagged that Quadro CE is for business income; a retail dividend credit (art. 165 TUIR) goes on Quadro CR when taxed ordinarily, and gets no credit under the 26% imposta sostitutiva regime.
- NO RF-1147: flagged as retired from income year 2023 (credit now entered in the online skattemelding; valid for 2022 and earlier).
- Registry caveats added for PL (treaty credit capped at min(treaty,19%), not "ulga abolicyjna"; PIT-38 for dividends), AT (E 1kv, KZ 863/998, 15% cap), IE (Form 12/myAccount for sub-€5,000), NL (Te verrekenen belasting, tweede- limiet cap + carry-forward).
- DE Anlage KAP (
de_anlage_aus_generator.py): the German foreign-dividend withholding-tax credit generator produced the wrong form (Anlage AUS). It now produces an Anlage KAP worksheet with the correct lines (Zeile 19 foreign capital income, Zeile 41 creditable foreign tax), cites ELSTER/the Finanzamt (www.elster.de) instead of the incorrectwww.bzst.de, and is framed as a transcription aid for ELSTER.generate_de_anlage_kap_pdfadded as the preferred alias (legacy symbol/endpoint kept). - NO RF-1534 (
rf1534_generator.py,main.py): the VPS/nominee securities-account number (required by Skatteetaten) was hardcoded empty; it is now avps_accountfield threaded through the endpoint and filled on each dividend row. Docstring andtemplates/no/instructions.txtnote that RF-1534 is the legacy paper form and the current route is RF-1552e/1553e/1554e via rdt.skatteetaten.no (5-year deadline, residence certificate required). - US 1040-NR Schedule NEC (
us_1040nr_generator.py,main.py): US-source dividends paid to a non-resident are FDAP income taxed at the treaty rate on Schedule NEC, not effectively-connected income. The generator previously put them on line 3b (graduated rates) with withholding on line 25b (1099) and never produced Schedule NEC — wrong tax and wrong refund. Dividends now flow to Schedule NEC (grouped by treaty rate; a missing rate falls back to the 30% statutory column), the NEC tax lands on line 23a, the US tax withheld on line 25g (Form 1042-S), and the filled Schedule NEC page is appended to the output. The US endpoint now accepts atreaty_rateper dividend to drive the rate column. Refund correctly equals withheld tax minus treaty-rate tax. - PDF-form accuracy (from the 2026-05-29 official-form audit):
- ES Modelo 210 (
model_210_generator.py): no longer stamps a fabricated random "Número de justificante" (that receipt number is assigned by AEAT on filing); and the IRNR rate is derived from residence (EU/EEA 19%, others 24%) instead of a blind 19% default. - FR 2047 (
fr_2047_generator.py): foreign-dividend tax credit box corrected from8TKto8VL(8TK is the salaries/pensions method); dropped spurious2BH. - UK SA106 (
gb_sa106_generator.py): replaced non-existent box labelsF1–F4/F20–F22with the real SA106 references (columns A–F, boxes 5/6/2).
- ES Modelo 210 (
- PDF rendering across the 11 WHT-reclaim generators
(
back/pdf_services/{bg,cz,ee,hr,hu,lt,lv,ro,rs,si,sk}_whtr_generator.py): table-cell figures were clipped by a crude character slice (val[:int(w/2.0)]) that dropped digits and currency codes (e.g.300.000,0,125.430,00 C); numeric columns are now right-aligned with width-aware fitting (fit) and shrink-to-fit (shrink_to_fit) so amounts are never truncated. Totals row no longer overflows the page edge, and section-label banner text is no longer vertically clipped. - Non-Latin-1 glyphs (Czech/Polish/Hungarian/Baltic — ě č ř ł ő ű ā…)
rendered as empty "tofu" boxes under Helvetica. Bundled DejaVu Sans
(
back/pdf_services/fonts/) via a sharedback/pdf_services/_pdf_fonts.pymodule (idempotent registration, graceful Helvetica fallback); all 11 WHT-reclaim generators now use it so titles, names, and claimant data render correctly. Regression tests added inback/tests/test_pdf_visual.py. - B2 (route-table conflict): all
front/src/routes.tsxwrites confined to Wave 1 (Plans 04-02 + 04-03) by pre-registering all 7 public routes with placeholder modules upfront. Wave 2+ plans modify only page-module bodies — structurally impossible to conflict on the route table. - B3 (anonymous-claim migration on
?next=checkoutpath): AuthCallback now awaitsmigrateAnonClaimbefore anywindow.location.assignto Stripe Checkout, ensuring ACQ-03 success criterion #6 holds on the highest-value conversion path. - H6: every public prerendered route composes the canonical
LandingFooter, not just/.check-disclaimer.mjsextended to assert presence on all 7 routes. - D-15 mobile audit-and-patch (Plan 04-09): all decorative blur orbs in
marketing sections (FAQSection, Footer, HowItWorksSection,
StatisticsSection, TaxCalculator, TaxCreditsVsRefundsSection,
TestimonialsSection, TrustSection) gated to
md:breakpoints withpointer-events-none, eliminating any chance of off-screen layout bleed at 360px. Genuine false positives (modal-bound widths,max-w-[Npx]regex collisions, sub-360px Selects, decorative 1px separator) recorded infront/.mobile-overflow-allowlistwith per-entry rationale.
Removed
- Legacy
front/scripts/prerender.mjspost-build template-injection script (Plan 04-08, D-01 single pipeline). vite-react-ssg is now the sole producer of prerendered HTML across all 455 public routes. - Legacy
front/scripts/generate-sitemap.mjsscript.dist/sitemap.xmlis produced exclusively byfront/scripts/build-sitemap.mjswalking the SSG build output (postbuild step). - Static
front/public/sitemap.xml. The build-timedist/sitemap.xmlis the single canonical sitemap. build:legacynpm script (and itsvite build && node scripts/prerender.mjsinvocation).npm run buildis the only build entry point.