diff --git a/docs/multizone-routing-audit-results.md b/docs/multizone-routing-audit-results.md new file mode 100644 index 000000000..086948874 --- /dev/null +++ b/docs/multizone-routing-audit-results.md @@ -0,0 +1,395 @@ +# Multizone Routing Audit Results + +Use this file for summarized findings from parallel audit sessions. Keep the +row status in `website/src/data/multizoneRoutingReview.json` in sync with these +findings so the preview page remains useful to the team. + +## Session Status + +| Session | Scope | Status | Notes | +| --- | --- | --- | --- | +| Session A - direct rewrite calculators | Oregon Kicker, WATCA, Keep Your Pay Act, Working Parents | Audited | All four are multizone-ready for routing; WATCA and Working Parents have polish follow-ups. | +| Session B - Vercel research interactives | Public Vercel-backed research apps | Audited | Most zones need path-scoped asset/base path work before host rewrites; Marriage and student loan decisions are deferred. | +| Session C - static/GitHub Pages/legacy embeds | Static or GitHub Pages interactives | Audited | Most durable static apps need zone rebuilds with PolicyEngine base paths plus host rewrites; local article/static artifacts can remain iframe/static exceptions. | +| Session D - special routes | API docs, TAXSIM, model docs, slides, bespoke routes | Audited | TAXSIM, slides, API docs, and California are ready/near-ready; model, ads, and AI inequality need zone work; state tracker remains a Modal proxy exception. | +| Session E - lower-priority/unlisted | Not listed in research or ownership unclear | Audited | Remaining `apps.json` rows are now covered; most are older Vite/CRA/Next embeds needing zone-side public-path work. | + +## Session B - Vercel Research Interactives + +Audited on April 17, 2026. These apps are shown in research and should be +treated as multizone targets by default unless noted otherwise. + +| App | Public path | Origin/source | Source repo | Recommendation | Summary | +| --- | --- | --- | --- | --- | --- | +| Scotland income tax analysis dashboard | `/uk/scotland-income-tax-reform` | `https://scotland-income-tax-reform.vercel.app/` | `PolicyEngine/scotland-income-tax-reform` | `multizone-zone-work` | Next dashboard has no `basePath` or `assetPrefix`; origin public path returns 404 and root HTML emits `/_next/static` assets. Host has no rewrite. | +| Introducing the PE-84 | `/us/pe84` | `https://april-fools-2026-two.vercel.app/calculator` | `PolicyEngine/april-fools-2026` | `multizone-zone-work` | Next app has empty `next.config.ts`, root `/_next/static` assets, root-relative icon metadata, and `/us/pe84` returns 404. Host has no rewrite. | +| Student loan visualisation | `/uk/student-loan-visualisation` | `https://student-loan-visualisation.vercel.app/` | `PolicyEngine/student-loan-visualisation` | `simple-rewrite` | Single-file static HTML with external D3/fonts and inline data; no app router or local asset bundle found. A base rewrite to the root document is likely sufficient if the team keeps this as a static article-style interactive. | +| Energy price shock | `/uk/energy-price-shock` | `https://energy-price-shock.vercel.app/` | `PolicyEngine/energy-price-shock` | `multizone-zone-work` | Vite app uses default root base and deployed HTML emits `/assets/...`; origin public path returns 404. Needs path-scoped Vite `base` or a dedicated asset prefix plus host rewrites. | +| Council tax to land value tax reform dashboard | `/uk/uk-land-value-tax` | `https://uk-land-value-tax.vercel.app/` | `PolicyEngine/uk-land-value-tax` | `multizone-zone-work` | Next dashboard has no `basePath` or `assetPrefix`; origin public path returns 404 and root HTML emits `/_next/static` assets. Host has no rewrite. | +| UK Spring Statement 2026 analysis dashboard | `/uk/spring-statement-2026` | `https://uk-spring-statement-2026-policy-engine.vercel.app/` | `PolicyEngine/uk-spring-statement-2026` | `multizone-zone-work` | Next static export emits root `/_next/static` assets, metadata lacks first-party canonical/OG URL, and origin public path returns 404. Host has no rewrite. | +| Cliff Watch | `/us/cliff-watch` | `https://cliff-watch.vercel.app/` | `PolicyEngine/cliff-watch` | `multizone-zone-work` | Vite app uses `base: "/"`, emits `/assets/...`, and origin public path returns 404. It calls `https://api.policyengine.org` directly but also has root-relative `/api/...` fallback calls that need scrutiny under the website host. | +| Marriage incentive calculator | `/us/marriage` | `https://marriage-zeta-beryl.vercel.app/` | `PolicyEngine/marriage` | `needs-investigation` | Defer migration decision. The repo is one shared US/UK app: it has UK calculator logic via `?country=uk`, but the deployed Next `basePath`, assets, metadata, and nav are US-scoped at `/us/marriage`. Tackle US and UK marriage routing together rather than in the straightforward batch. | +| The SALTernative | `/us/salternative` | `https://salt-amt-calculator.vercel.app/?embedded=true` | `PolicyEngine/salt-amt-calculator` | `multizone-zone-work` | Vite app currently serves `/us/salternative` through SPA fallback, but HTML emits root `/assets/...`, title is still generic, and `vite.config.ts` uses `base: "/"`. API calls use Modal absolute URLs. | +| Two-child limit calculator | `/uk/two-child-limit-comparison` | `https://uk-two-child-limit-app.vercel.app` | `PolicyEngine/uk-two-child-limit-app` | `multizone-zone-work` | Vite app uses default root base, emits `/assets/...`, origin public path returns 404, and app code fetches root-relative `/data/all-results.csv`. Needs path-scoped assets and data URL handling before host rewrites. | + +### Session B Host Findings + +- No Session B public paths currently have host rewrites in `website/next.config.ts`. +- For the Next zones that currently emit root `/_next/static` assets, add zone + `basePath` or a separate `assetPrefix` before adding host rewrites to avoid + collisions with the website app and other zones. +- For Vite zones that emit root `/assets/...`, set `base` to the public path or + a dedicated zone asset prefix and audit any root-relative data/API calls. +- `PolicyEngine/marriage` should be deferred from the straightforward batch. + It is one shared app with both US and UK calculator logic, but its production + routing and metadata are US-path scoped. The duplicate UK `apps.json` row for + `marriage` is not covered by the US `/us/marriage` base path, so US and UK + marriage should be handled in one follow-up routing decision. + +## Session D - Special Routes + +Audited on April 21, 2026. These routes do not all follow the normal +`apps.json` research interactive path; several already have bespoke host +rewrites or dedicated website iframe pages. + +| Surface | Public path | Origin/source | Source repo | Recommendation | Summary | +| --- | --- | --- | --- | --- | --- | +| Household API docs | `/us/api` | `https://household-api-docs-policy-engine.vercel.app/us/api/` | `PolicyEngine/household-api-docs` | `multizone-ready` | Existing reference zone. Host has beforeFiles page rewrites plus `/_zones/household-api-docs/:path*`; zone uses production-only `assetPrefix` and disables it during `next dev`. | +| TAXSIM emulator | `/us/taxsim` | `https://policyengine-taxsim-policy-engine.vercel.app/us/taxsim` | `PolicyEngine/policyengine-taxsim` | `multizone-ready` | Next static export already has `basePath: "/us/taxsim"`, helper logic for public data assets, first-party canonical/OG metadata, and host rewrites. Minor follow-up: root icon metadata uses `/policyengine.png`. | +| PolicyEngine model documentation | `/:countryId/model` | `https://policyengine-model-phi.vercel.app/?country=:countryId` | `PolicyEngine/policyengine-model` | `multizone-zone-work` | Current host afterFiles rewrite injects country query, but the zone has no `basePath`, emits root `/_next/static` assets, and only has minimal title metadata. Needs asset/base path strategy and first-party metadata before formal multizone. | +| PolicyEngine slides | `/slides` | `https://policyengine-slides.vercel.app/slides` | `PolicyEngine/policyengine-slides` | `multizone-ready` | Next app hardcodes `basePath: "/slides"` and live HTML emits `/slides/_next/static/...`; host afterFiles rewrites already cover base and nested paths. Follow up with local-dev basePath escape hatch if desired. | +| Plugin blog | `/plugin-blog` | `https://policyengine.github.io/plugin-blog/` | `PolicyEngine/plugin-blog` | `simple-rewrite` | Static GitHub Pages surface is already path-scoped under `/plugin-blog/assets/...` and host rewrites are sufficient. Keep as simple static rewrite unless product requirements change. | +| Ads transparency dashboard | `/us/ads-dashboard` | `https://policyengine-ads-dashboard.vercel.app?embedded=true` | `PolicyEngine/policyengine-ads-dashboard` | `multizone-zone-work` | Currently a dedicated website iframe page. Vite config uses `base: process.env.BASE_URL || "/"`; live HTML emits root `/assets/...` and `/vite.svg`. Needs PolicyEngine public path or asset-prefix build before host rewrites. | +| AI and inequality | `/:countryId/ai-inequality` and `/:countryId/ai-inequality/income-shift` | `https://ai-inequality-theta.vercel.app` | `PolicyEngine/ai-inequality` | `multizone-zone-work` | Currently dedicated iframe pages with country query handling. Create React App build uses `homepage: "."` and live HTML emits root `/static/...`, `/favicon.svg`, and `/manifest.json`. Needs a route/base strategy for both main and income-shift paths. | +| State legislative tracker | `/:countryId/state-legislative-tracker` | `https://policyengine--state-legislative-tracker.modal.run/` | `PolicyEngine/state-legislative-tracker` | `simple-rewrite` | Modal/Vite app is not a normal Vercel/Next zone. Current beforeFiles proxy plus `/_tracker` asset rewrite and root logo rewrites are the working pattern. Keep direct proxy unless the app moves to static/Next hosting. | +| California wealth tax | `/us/california-wealth-tax` and `/us/california-wealth-tax/embed` | `https://california-wealth-tax.vercel.app/us/california-wealth-tax/embed` | `PolicyEngine/california-wealth-tax` | `multizone-ready` for embed route; host work for top-level route | Embed zone defaults `basePath` to `/us/california-wealth-tax/embed`, emits path-scoped `_next` assets, and has first-party metadata for `/us/california-wealth-tax`. The website top-level route still wraps it in `SyncedAppIframe` for query syncing. | + +### Session D Host Findings + +- Special routes fall into three patterns rather than one: + path-mounted Next zones, static rewrites, and non-zone proxy exceptions. +- TAXSIM, slides, API docs, and California demonstrate valid first-party path + ownership patterns, but use different asset strategies: `basePath`, + `assetPrefix`, and embed-path `basePath`. +- Model docs should not be treated as a finished zone despite having a host + rewrite; it currently emits root `/_next` assets and relies on country query + injection. +- State legislative tracker should remain a direct Modal proxy unless its + deployment model changes. Its extra root asset rewrites are a signal that it + is not yet a clean zone. +- Dedicated iframe pages for ads dashboard and AI inequality are conversion + candidates, but both need zone-side asset/public-path work before host + rewrites. + +### Rubric Finding: Proxy Exceptions + +Not every first-party route should be forced into the same multizone shape. +Modal apps and static GitHub Pages surfaces can be durable direct rewrites when +their deployment model is intentionally not a Next/Vercel zone. The audit should +still record why they are exceptions and what would have to change to promote +them later. + +## Session E - Lower-Priority And Previously Unlisted Apps + +Audited on April 21, 2026. This session covered older, duplicate, not-research, +or previously unassigned `apps.json` rows. After this session, every app row has +a shared review entry. + +| App | Public path | Origin/source | Source repo | Recommendation | Summary | +| --- | --- | --- | --- | --- | --- | +| Social Security taxation reform dashboard | `/us/taxation-of-benefits-reforms` | `https://crfb-tob-impacts.vercel.app/` | `PolicyEngine/crfb-tob-impacts` | `multizone-zone-work` | Next static export already supports env-driven `basePath`/`assetPrefix`, but current deployment has empty base path, root `/_next` assets, and public path returns 404. | +| ACA Reforms Calculator | `/us/aca-reforms-calculator` | `https://aca-calc.vercel.app/?embedded=true` | `PolicyEngine/ACA-Calc` | `multizone-zone-work` | Vite app serves the public path through SPA fallback, but emits root `/assets`. Treat this as the canonical ACA route if migrated. | +| ACA Reforms Calculator legacy slug | `/us/aca-calc` | `https://aca-calc.vercel.app/?embedded=true` | `PolicyEngine/ACA-Calc` | `simple-rewrite` | Duplicate row for the same ACA app. Prefer redirect/canonicalization to `/us/aca-reforms-calculator` rather than a second zone. | +| Child Tax Credit calculator | `/us/child-tax-credit-calculator` | `https://ctc-calculator.vercel.app/?embedded=true` | `PolicyEngine/ctc-calculator` | `multizone-zone-work` | Older static app uses relative `script/...` assets and public path returns 404. Needs source modernization or path-scoped static build. | +| 2024 election CTC comparison | `/us/child-tax-credit-2024-election-calculator` | `https://vance-harris-ctc-comparison.vercel.app/?embedded=true` | `PolicyEngine/vance-harris-ctc-comparison` | `multizone-zone-work` | Vite app serves the public path through SPA fallback but emits root `/assets`; needs path-scoped build. | +| GiveCalc | `/us/givecalc` | `https://givecalc.vercel.app/?embedded=true` | `PolicyEngine/givecalc` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets`; needs path-scoped build. | +| UK 2024 manifestos comparison | `/uk/2024-manifestos` | `https://uk-2024-manifestos-comparison.vercel.app/?embedded=true` | `PolicyEngine/uk-2024-manifestos-comparison` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets` plus `/vite.svg`; needs path-scoped build. | +| State EITCs and CTCs | `/us/state-eitcs-ctcs` | `https://us-state-eitcs-ctcs.vercel.app/?embedded=true` | `PolicyEngine/us-state-eitcs-ctcs` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets`; needs path-scoped build. | +| 2024 election personal impact calculator | `/us/2024-election-calculator` | `https://2024-election-dashboard.vercel.app/?embedded=true` | `PolicyEngine/2024-election-dashboard` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets`; needs path-scoped build. | +| Rhode Island Child Tax Credit calculator | `/us/rhode-island-ctc-calculator` | `https://ri-ctc-calculator-policy-engine.vercel.app/` | `PolicyEngine/ri-ctc-calculator` | `multizone-zone-work` | Next app has no `basePath`; public path returns 404 and root HTML emits `/_next` assets. | +| UK student loan deductions calculator | `/uk/uk-student-loan-calculator` | `https://uk-student-loan-calculator.vercel.app/` | `PolicyEngine/student-loan-calculator` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets` plus `/vite.svg`; needs path-scoped build. | +| Scottish Budget 2026-27 | `/uk/scottish-budget-2026-27` | `https://post-scottish-budget-dashboard.vercel.app/` | `PolicyEngine/scottish-budget-2026-2027` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets`; needs path-scoped build. | +| UK local areas dashboard | `/uk/local-areas-dashboard` | `https://local-area.vercel.app/` | `PolicyEngine/uk-local-areas-dashboard` | `multizone-zone-work` | Older Next/pages app public path returns 404 and root HTML emits `/_next`; needs basePath/assetPrefix work. | +| UK Autumn Budget 2025 analysis dashboard | `/uk/autumn-budget-2025` | `https://uk-autumn-budget-dashboard.vercel.app/` | `PolicyEngine/uk-autumn-budget-dashboard` | `multizone-zone-work` | Vite app public path returns 404 and root HTML emits `/assets` plus `/vite.svg`; needs path-scoped build. | +| UK public services spending analysis dashboard | `/uk/public-services-spending` | `https://uk-public-services-imputation.vercel.app` | `PolicyEngine/uk-public-services-imputation` | `multizone-zone-work` | CRA/static dashboard serves the public path but emits root `/static` assets; needs `PUBLIC_URL` or equivalent path strategy. | +| Marriage incentive calculator - UK row | `/uk/marriage` | `https://marriage-zeta-beryl.vercel.app/` | `PolicyEngine/marriage` | `needs-investigation` | Duplicate UK row for the shared marriage app. Keep deferred with the US marriage audit because production routing is currently US-scoped and UK behavior is query-driven. | + +### Session E Host Findings + +- The remaining unreviewed rows are mostly older Vite/CRA apps or root-mounted + Next apps. They should not be first in the migration order. +- Canonicalize duplicates before migration. The ACA duplicate and UK marriage + duplicate should not become independent zones unless the team explicitly wants + separate public products. +- Several Vite apps return 404 at the intended PolicyEngine public path, while + ACA and the CTC election comparison serve paths through SPA fallback but still + emit root `/assets`. Both cases require zone-side public-path work before host + rewrites. +- `crfb-tob-impacts` is a useful lower-priority pilot candidate later because + its Next config already supports env-driven `basePath` and `assetPrefix`. + +### Rubric Finding: Inventory Completion + +The audit review JSON now has an entry for every `apps.json` app row. Future app +additions should include an audit-session/status entry when the app is added, or +the review page should flag missing review metadata automatically. + +## Oregon Kicker Refund + +- Recommendation: `multizone-ready` +- Public path: `/us/oregon-kicker-refund` +- Origin: `https://oregon-kicker-refund.vercel.app` +- Source repo: `PolicyEngine/oregon-kicker-refund` +- Current host setup: `beforeFiles` base and nested rewrites in `website/next.config.ts` +- Current zone setup: Next `basePath` defaults to `/us/oregon-kicker-refund` +- Target zone setup: path-scoped `basePath` +- Host status: pass; host rewrites cover base and nested paths before the generic `[slug]` route +- Zone status: pass; repo has explicit path-mounted config +- Asset status: pass; live HTML emits `/us/oregon-kicker-refund/_next/static/...` +- Dev setup status: pass; `NEXT_PUBLIC_BASE_PATH=""` disables the base path for local dev +- Metadata status: pass; canonical, Open Graph URL, and sitemap point to `https://policyengine.org/us/oregon-kicker-refund` +- API status: pass; app calls `https://api.policyengine.org` directly +- Blockers: none found +- Follow-ups: use this as the baseline for direct rewrite calculator audits + +## Household API Docs + +- Recommendation: `multizone-ready` +- Public path: `/us/api` +- Origin: `https://household-api-docs-policy-engine.vercel.app` +- Source repo: `PolicyEngine/household-api-docs` +- Current host setup: `beforeFiles` rewrite plus `/_zones/household-api-docs/:path*` asset proxy +- Current zone setup: static export with production-only `assetPrefix` +- Target zone setup: scoped asset prefix under `/_zones/household-api-docs` +- Host status: pass; website has page and asset rewrites +- Zone status: pass based on merged `PolicyEngine/household-api-docs#11` +- Asset status: pass; production assets use `/_zones/household-api-docs` +- Dev setup status: pass; `assetPrefix` is disabled during `next dev` +- Metadata status: partially audited; terms PR adds canonical metadata for new terms routes +- API status: not applicable for this docs surface +- Blockers: none found for multizone pattern +- Follow-ups: use this as the reference for zones that need a separate asset prefix rather than path-scoped `_next` assets + +## PolicyEngine 2025 Year in Review - US + +- Recommendation: `multizone-zone-work` +- Public path: `/us/2025-year-in-review` +- Origin: `https://policyengine.github.io/2025-year-in-review` +- Source repo: `PolicyEngine/2025-year-in-review` +- Current host setup: dedicated website page at `website/src/app/[countryId]/2025-year-in-review/page.tsx` that embeds GitHub Pages; no host rewrite +- Current zone setup: Vite/React static site with `base: "/2025-year-in-review/"`; `/2025-year-in-review/us?embed=true` currently returns a GitHub Pages SPA fallback with HTTP 404 before client redirect +- Target zone setup: static zone built for `/us/2025-year-in-review` or a scoped asset prefix with host rewrites for the country path and assets +- Host status: fail for multizone; host currently serves an iframe page, not a zone rewrite +- Zone status: fail for multizone; zone is built for the GitHub Pages project path, not the PolicyEngine country path +- Asset status: fail for multizone; live HTML emits `/2025-year-in-review/assets/...` +- Dev setup status: partial; Vite base is explicit, but no PolicyEngine base-path build mode was found +- Metadata status: partial; live root HTML uses PolicyEngine OG URLs but hardcodes the US public URL +- API status: pass; no API calls found in the static app audit +- Blockers: rebuild zone for `/us/2025-year-in-review` or add a static asset-prefix strategy; replace dedicated iframe page with beforeFiles base and nested rewrites +- Follow-ups: decide whether this remains one shared country-aware zone or splits into country-specific builds + +## PolicyEngine 2025 Year in Review - UK + +- Recommendation: `multizone-zone-work` +- Public path: `/uk/2025-year-in-review` +- Origin: `https://policyengine.github.io/2025-year-in-review` +- Source repo: `PolicyEngine/2025-year-in-review` +- Current host setup: dedicated website iframe page shared with the US route +- Current zone setup: Vite/React static site with `base: "/2025-year-in-review/"`; `/2025-year-in-review/uk?embed=true` returns the GitHub Pages SPA fallback with HTTP 404 before client redirect +- Target zone setup: static zone built for `/uk/2025-year-in-review` or a country-aware zone with a scoped asset prefix +- Host status: fail for multizone; host currently serves an iframe page +- Zone status: fail for multizone; zone is not mounted at the PolicyEngine country path +- Asset status: fail for multizone; assets are under `/2025-year-in-review/assets/...` +- Dev setup status: partial; explicit Vite base exists, but no PolicyEngine build mode was found +- Metadata status: fail for UK; source metadata/OG URL points to `https://policyengine.org/us/2025-year-in-review` +- API status: pass; no API calls found in the static app audit +- Blockers: same zone and host rewrite work as the US route, plus country-correct metadata +- Follow-ups: make metadata derive from the country route before promoting the UK route to first-party multizone + +## TANF Calculator + +- Recommendation: `multizone-zone-work` +- Public path: `/us/tanf-calculator` +- Origin: `https://policyengine.github.io/tanf-calculator/` +- Source repo: `PolicyEngine/tanf-calculator` +- Current host setup: generic `/[countryId]/[slug]` iframe wrapper from `apps.json`; no host rewrite +- Current zone setup: Vite/React static build from `frontend/` with `base: "/tanf-calculator/"` and output to `docs` +- Target zone setup: static zone built for `/us/tanf-calculator` or a separate scoped asset prefix +- Host status: fail for multizone; host URL returns the website iframe wrapper +- Zone status: fail for multizone; zone is built for GitHub Pages project path only +- Asset status: fail for multizone; live HTML emits `/tanf-calculator/assets/...`, and favicon is a relative `policyengine-logo.png` asset that must remain path-scoped after any public-path rebuild +- Dev setup status: partial; Vite base is explicit, but no PolicyEngine base-path build mode was found +- Metadata status: fail; only a basic `` was found, with no canonical/OG ownership +- API status: pass in audit scope; no direct `api.policyengine.org` calls were found by code search +- Blockers: add PolicyEngine path-aware build config and host beforeFiles base/nested rewrites +- Follow-ups: add first-party metadata if promoted to multizone + +## Salary Sacrifice Cap Analysis Tool + +- Recommendation: `multizone-zone-work` +- Public path: `/uk/uk-salary-sacrifice-tool` +- Origin: `https://policyengine.github.io/uk-salary-sacrifice-analysis/` +- Source repo: `PolicyEngine/uk-salary-sacrifice-analysis` +- Current host setup: generic `/[countryId]/[slug]` iframe wrapper from `apps.json`; no host rewrite +- Current zone setup: Vite/React app under `app/`; GitHub Pages build uses `base: "/uk-salary-sacrifice-analysis/"`, while `app/vercel.json` builds Vercel output with `--base /` +- Target zone setup: build with `/uk/uk-salary-sacrifice-tool` or a scoped asset prefix, then add host rewrites +- Host status: fail for multizone; host currently serves the generic iframe wrapper +- Zone status: fail for multizone; neither current GitHub Pages base nor Vercel root base matches the public path +- Asset status: fail for multizone; GitHub Pages HTML emits `/uk-salary-sacrifice-analysis/assets/...` +- Dev setup status: partial; build modes exist, but not a PolicyEngine public-path mode +- Metadata status: fail; only a basic title was found, with no canonical/OG ownership +- API status: pass in audit scope; no direct `api.policyengine.org` calls were found by code search +- Blockers: align app slug/path with deployed base path and add host rewrites +- Follow-ups: decide whether to keep source origin on GitHub Pages or use the existing Vercel build path for the zone + +## Automating Tax And Benefit Policy Modeling With Multi-Agent AI + +- Recommendation: `iframe` +- Public path: `/us/encode-policy-multi-agent-ai` +- Origin: `PolicyEngine website static asset` +- Source repo: not found as a standalone PolicyEngine repo; bundled in `website/public/assets/posts/encode-policy-multi-agent-ai` +- Current host setup: generic iframe wrapper to local static asset `/assets/posts/encode-policy-multi-agent-ai/index.html` +- Current zone setup: bundled static SPA artifact with relative `./assets/...` JS/CSS +- Target zone setup: keep as local static iframe unless the article interactive gets an owning source repo and durable app lifecycle +- Host status: pass for current iframe/static pattern +- Zone status: not applicable for multizone; this is a bundled article artifact, not a separately hosted zone +- Asset status: partial for current static artifact; JS/CSS are colocated relative `./assets/...`, but the artifact still references root `/vite.svg` for the favicon +- Dev setup status: not audited; no source repo found +- Metadata status: partial; static HTML has title but no canonical/OG metadata in the artifact +- API status: pass; local artifact audit did not find API calls +- Blockers: no standalone source repo or deployment surface to promote +- Follow-ups: keep iframe/static, or reconstruct source ownership before considering multizone + +## OBBBA Household Impact Explorer + +- Recommendation: `iframe` +- Public path: `/us/obbba-household-explorer` +- Origin: `https://policyengine.github.io/obbba-household-by-household` +- Source repo: `PolicyEngine/obbba-household-by-household` +- Current host setup: generic iframe wrapper from `apps.json`; this row points at the same origin as `obbba-household-by-household` +- Current zone setup: SvelteKit static export configured for `/obbba-household-by-household` +- Target zone setup: keep as a legacy/alias iframe unless the team wants this slug as a canonical first-party route +- Host status: pass for current iframe; fail for multizone because there is no rewrite +- Zone status: fail for this slug; source app is not built for `/us/obbba-household-explorer` +- Asset status: fail for multizone; live assets are under `/obbba-household-by-household/_app/...` +- Dev setup status: partial; repo supports `BASE_PATH`, but not for this alias slug by default +- Metadata status: not found in source audit +- API status: pass in audit scope; no direct `api.policyengine.org` calls were found by code search +- Blockers: duplicate/legacy slug ownership is unclear +- Follow-ups: canonicalize this row to the household-by-household route or remove it from app inventory if it is no longer needed + +## The One Big Beautiful Bill Act, Household By Household + +- Recommendation: `multizone-zone-work` +- Public path: `/us/obbba-household-by-household` +- Origin: `https://policyengine.github.io/obbba-household-by-household/` +- Source repo: `PolicyEngine/obbba-household-by-household` +- Current host setup: generic `/[countryId]/[slug]` iframe wrapper from `apps.json`; no host rewrite +- Current zone setup: SvelteKit static export with `paths.base = process.env.BASE_PATH || "/obbba-household-by-household"` +- Target zone setup: build with `BASE_PATH=/us/obbba-household-by-household` or adopt a scoped asset prefix, then add host rewrites +- Host status: fail for multizone; host currently serves iframe wrapper +- Zone status: partial; repo already documents the PolicyEngine base path mismatch and supports `BASE_PATH`, but the live GitHub Pages build is still project-path scoped +- Asset status: fail for current multizone; live HTML emits `/obbba-household-by-household/_app/...` and `/obbba-household-by-household/favicon.png`, which are project-path scoped rather than PolicyEngine-path scoped +- Dev setup status: pass/partial; environment-driven base path exists +- Metadata status: not found in source audit +- API status: pass in audit scope; no direct `api.policyengine.org` calls were found by code search +- Blockers: rebuild/deploy for PolicyEngine public path and add beforeFiles base/nested host rewrites +- Follow-ups: use this as the reference pattern for SvelteKit static exports that can be rebuilt with a PolicyEngine `BASE_PATH` +- Rubric Finding: static Vite/SvelteKit zones need the same path-scoped asset checks as Next zones, but the implementation knobs are `base` and SvelteKit `kit.paths.base` rather than `basePath`/`assetPrefix`. + +## OBBBA Scatter Plot Explorer + +- Recommendation: `needs-investigation` +- Public path: `/us/obbba-scatter` +- Origin: `https://policyengine.github.io/obbba-scatter` +- Source repo: not found as a standalone PolicyEngine repo; code search found older `PolicyEngine/policyengine-app` applet references and related OBBBA code in `PolicyEngine/obbba-household-by-household` +- Current host setup: specialized `obbba-iframe` app row; no host rewrite +- Current zone setup: unknown; deployed origin returns HTTP 404 +- Target zone setup: unknown until source/deployment ownership is identified +- Host status: pass for current wrapper route; fail for multizone because no rewrite exists +- Zone status: fail/blocked; origin currently returns 404 +- Asset status: blocked; no live app HTML/assets available +- Dev setup status: blocked; source repo unknown +- Metadata status: blocked +- API status: blocked +- Blockers: deployed source URL is 404 and owning repo is unclear +- Follow-ups: identify whether this should point to `PolicyEngine/obbba-household-by-household`, an old `policyengine-app` applet, or be removed from inventory + +## SNAP District Map + +- Recommendation: `multizone-zone-work` +- Public path: `/us/snap-district-map` +- Origin: `https://policyengine.github.io/snap-district-map/` +- Source repo: `PolicyEngine/snap-district-map` +- Current host setup: generic `/[countryId]/[slug]` iframe wrapper from `apps.json`; no host rewrite +- Current zone setup: Vite/React repo with `base: "/snap-district-map/"`, but the live GitHub Pages HTML references `/src/main.jsx` instead of built `/snap-district-map/assets/...` +- Target zone setup: first fix static build/deploy, then build for `/us/snap-district-map` or use a scoped asset prefix with host rewrites +- Host status: fail for multizone; host serves iframe wrapper +- Zone status: fail; live deployment appears to serve unbuilt Vite source +- Asset status: fail; critical JS is `/src/main.jsx` with bare React imports, which is not a production static bundle +- Dev setup status: partial; Vite base exists, but production deployment does not appear to use it +- Metadata status: fail; only title was found, with no canonical/OG ownership +- API status: pass in audit scope; no direct `api.policyengine.org` calls were found by code search +- Blockers: repair GitHub Pages production build before multizone migration work +- Follow-ups: after deployment is fixed, add PolicyEngine base-path build and beforeFiles host rewrites + +## Keep Your Pay Act + +- Recommendation: `multizone-ready` +- Public path: `/us/keep-your-pay-act` +- Origin: `https://keep-your-pay-act.vercel.app` +- Source repo: `PolicyEngine/keep-your-pay-act` +- Current host setup: `beforeFiles` base and nested rewrites in `website/next.config.ts` +- Current zone setup: Next `basePath` defaults to `/us/keep-your-pay-act` +- Target zone setup: path-scoped `basePath` +- Host status: pass; `https://www.policyengine.org/us/keep-your-pay-act` returns HTTP 200 +- Zone status: pass; `frontend/next.config.js` defaults `NEXT_PUBLIC_BASE_PATH` to `/us/keep-your-pay-act` +- Asset status: pass; live HTML emits `/us/keep-your-pay-act/_next/static/...`, `/us/keep-your-pay-act/favicon.svg`, and `/us/keep-your-pay-act/policyengine-logo-teal.png`; representative host CSS asset returned HTTP 200 +- Dev setup status: pass; `NEXT_PUBLIC_BASE_PATH=""` disables the base path for local dev +- Metadata status: pass; canonical, Open Graph URL, and sitemap point to `https://policyengine.org/us/keep-your-pay-act` +- API status: pass; app calls `https://api.policyengine.org` directly +- Blockers: none found +- Follow-ups: none for multizone readiness + +## Working Parents Tax Relief Act + +- Recommendation: `multizone-ready` +- Public path: `/us/working-parents-tax-relief-act` +- Origin: `https://wptra.vercel.app` +- Source repo: `PolicyEngine/working-parents-tax-relief-act` +- Current host setup: `beforeFiles` base and nested rewrites in `website/next.config.ts` +- Current zone setup: Next `basePath` defaults to `/us/working-parents-tax-relief-act` +- Target zone setup: path-scoped `basePath` +- Host status: pass; `https://www.policyengine.org/us/working-parents-tax-relief-act` returns HTTP 200 +- Zone status: pass; `frontend/next.config.js` defaults `NEXT_PUBLIC_BASE_PATH` to `/us/working-parents-tax-relief-act` +- Asset status: pass for critical assets; live HTML emits `/us/working-parents-tax-relief-act/_next/static/...` and representative host CSS asset returned HTTP 200 +- Dev setup status: pass; `NEXT_PUBLIC_BASE_PATH=""` disables the base path for local dev +- Metadata status: pass for canonical and Open Graph URL; sitemap points to `https://policyengine.org/us/working-parents-tax-relief-act` +- API status: pass; app calls `https://api.policyengine.org` directly +- Blockers: none found for multizone routing +- Follow-ups: path-scope the favicon; live HTML currently renders `<link rel="icon" href="/favicon.svg"/>`, which may resolve to the website host favicon under multizone routing + +## Working Americans' Tax Cut Act + +- Recommendation: `multizone-ready` +- Public path: `/us/watca` +- Origin: `https://working-americans-tax-cut-act-one.vercel.app` +- Source repo: `PolicyEngine/working-Americans-tax-cut-act-` +- Current host setup: `beforeFiles` base and nested rewrites in `website/next.config.ts` +- Current zone setup: Next `basePath` is hardcoded to `/us/watca` +- Target zone setup: path-scoped `basePath` +- Host status: pass; `https://www.policyengine.org/us/watca` returns HTTP 200 +- Zone status: pass for routing; `frontend/next.config.ts` sets `basePath: "/us/watca"` and `NEXT_PUBLIC_BASE_PATH: "/us/watca"` +- Asset status: pass; live HTML emits `/us/watca/_next/static/...` and representative host CSS asset returned HTTP 200 +- Dev setup status: follow-up; unlike Oregon and Keep Your Pay, the base path is hardcoded and does not provide a `next dev` escape hatch +- Metadata status: follow-up; `frontend/app/layout.tsx` has title and description, but no first-party canonical, Open Graph URL, or sitemap found +- API status: pass; app calls `https://api.policyengine.org` directly +- Blockers: none found for multizone routing +- Follow-ups: add `NEXT_PUBLIC_BASE_PATH=""` local-dev handling and policyengine.org canonical/Open Graph/sitemap metadata + +## Rubric Finding: Root Public Assets + +Session A found a non-blocking but reusable check: public assets outside +`/_next`, especially favicons, can still leak to root paths even when critical +Next assets are path-scoped. Future audits should check favicons, logos, Open +Graph images, and downloadable assets, not only JS/CSS chunks. diff --git a/docs/multizone-routing-audit.md b/docs/multizone-routing-audit.md new file mode 100644 index 000000000..b4dc0e082 --- /dev/null +++ b/docs/multizone-routing-audit.md @@ -0,0 +1,416 @@ +# Multizone Routing Audit + +This audit determines what work is needed to serve PolicyEngine interactives as +multi-zone apps. The organization currently prefers multizone routing for +interactives by default, because most are independent product surfaces with +their own pages, components, and app chrome. + +Use `iframe` or `simple-rewrite` only when there is a concrete reason the app +should not be promoted to a multizone surface. + +Use this file as the shared rubric for parallel audits. If an app reveals a +new routing pattern that applies beyond that single app, add a "Rubric Finding" +to the audit result so the criteria can be updated deliberately. + +## Inputs + +Record these fields before evaluating an app: + +- App title +- Slug +- Country +- Public path, such as `/us/oregon-kicker-refund` +- Current source/origin URL +- Source repository, if known +- Current `apps.json` entry +- Current host routing entry in `website/next.config.ts`, if any +- Assigned audit session +- Audit status + +## Classification + +Use one of these recommendations. Prefer a multizone classification unless the +audit finds a concrete blocker or a durable reason to keep the app embedded. + +- `multizone-ready`: The host and zone already satisfy the required checks. +- `multizone-host-work`: The zone is path-aware, but the host rewrite is + missing, incomplete, or in the wrong rewrite phase. +- `multizone-zone-work`: The host can route the app, but the zone needs changes + such as `basePath`, asset prefixing, metadata, or API path fixes. +- `simple-rewrite`: A direct rewrite is enough for a limited surface, and the + team explicitly does not want to treat it as a durable first-party app. +- `iframe`: The app is a true embed, static artifact, legacy tool, or otherwise + has a concrete reason not to become a first-party route. +- `needs-investigation`: The source repo, framework, or deployed behavior is not + clear enough to classify. + +For non-Next/non-Vercel surfaces such as Modal apps or static GitHub Pages +content, record whether the current direct rewrite is an intentional proxy +exception. Do not force those into the same implementation shape as Next zones +unless the deployment model is also changing. + +## Parallel Session Plan + +Use these sessions to split work without duplicating effort. Each session should +update the relevant row in `website/src/data/multizoneRoutingReview.json` with +its `auditSession`, `auditStatus`, and summary comment. + +Every `apps.json` app row should have a matching review entry. When new apps are +added, add the review metadata in the same PR or explicitly mark the row as +`not-started`. + +### Session A - Direct Rewrite Calculators + +Start here because these are closest to Oregon Kicker and are likely the first +conversion batch. + +- `app:us:oregon-kicker-refund` +- `app:us:watca` +- `app:us:keep-your-pay-act` +- `app:us:working-parents-tax-relief-act` + +### Session B - Vercel Research Interactives + +These are public research/product surfaces that currently appear mostly as +generic iframe apps. Audit them as multizone targets by default. + +- `app:uk:scotland-income-tax-reform` +- `app:us:pe84` +- `app:uk:student-loan-visualisation` +- `app:uk:energy-price-shock` +- `app:uk:uk-land-value-tax` +- `app:uk:spring-statement-2026` +- `app:us:cliff-watch` +- `app:us:marriage` +- `app:us:salternative` +- `app:uk:two-child-limit-comparison` + +### Session C - Static, GitHub Pages, And Legacy Embeds + +These may still become first-party routes, but the likely work is different: +static hosting migration, path ownership, or deciding that iframe is the right +exception. + +- `app:us:2025-year-in-review` +- `app:uk:2025-year-in-review` +- `app:us:tanf-calculator` +- `app:uk:uk-salary-sacrifice-tool` +- `app:us:encode-policy-multi-agent-ai` +- `app:us:obbba-household-explorer` +- `app:us:obbba-household-by-household` +- `app:us:obbba-scatter` +- `app:us:snap-district-map` + +### Session D - Special Routes Outside apps.json + +These routes have bespoke host behavior and should be audited separately from +the generic app inventory. + +- `route:api` +- `route:taxsim` +- `route:model` +- `route:slides` +- `route:plugin-blog` +- `route:ads-dashboard` +- `route:ai-inequality` +- `app:us:state-legislative-tracker` +- `app:us:california-wealth-tax` + +### Session E - Lower-Priority Or Unlisted Interactives + +These are not currently shown in the research listing or need ownership +confirmation before prioritizing conversion. + +- `app:us:taxation-of-benefits-reforms` +- `app:us:aca-reforms-calculator` +- `app:us:aca-calc` +- `app:us:child-tax-credit-calculator` +- `app:us:child-tax-credit-2024-election-calculator` +- `app:us:givecalc` +- `app:uk:2024-manifestos` +- `app:us:state-eitcs-ctcs` +- `app:us:2024-election-calculator` +- `app:us:rhode-island-ctc-calculator` +- Any additional `apps.json` rows not listed above + +## Host Checks + +These checks apply to `policyengine-app-v2`, mainly under `website/`. + +1. The host has a rewrite for the public base path: + + ```ts + { + source: "/us/example-app", + destination: "https://example-app.vercel.app/us/example-app" + } + ``` + +2. The host has a rewrite for nested paths and path-scoped assets: + + ```ts + { + source: "/us/example-app/:path*", + destination: "https://example-app.vercel.app/us/example-app/:path*" + } + ``` + +3. The rewrite is in the correct phase. App zones that must beat + `website/src/app/[countryId]/[slug]/page.tsx` should be in `beforeFiles`. + +4. If the zone emits assets under a separate prefix, such as + `/_zones/example-app`, the host has a matching asset rewrite. + +5. The website does not define another page that should own the same public + path. + +6. Research/app links use hard navigation for cross-zone surfaces. In the + current website this is handled by `isApp` links using plain `<a>` tags. + +7. Sitemap and metadata ownership is coherent. The public app URL should be + discoverable, and canonical URLs should not point users to a Vercel origin. + +## Zone Checks + +These checks apply to the app being routed to. + +1. The deployed origin serves the public path: + + ```txt + https://example-app.vercel.app/us/example-app + ``` + +2. The app is path-aware. For a Next app, this usually means `basePath` is set + to the public path, or the deployed HTML clearly emits path-prefixed assets. + +3. Critical assets are path-scoped or separately prefixed. Prefer: + + ```txt + /us/example-app/_next/static/... + ``` + + or: + + ```txt + /_zones/example-app/_next/static/... + ``` + + Treat root `/_next/static/...` assets as a risk unless the host has an + explicit zone asset rewrite and no collision risk. + + Also inspect public assets such as favicons, logos, Open Graph images, and + downloadable files. Root paths like `/favicon.svg` may resolve to the host + website instead of the zone unless they are path-scoped or explicitly + rewritten. + +4. The app hydrates through the host URL. There should be no failed critical + JavaScript or CSS requests. + +5. Refresh works on the public path. If the app has nested routes, refresh those + paths through the host as well. + +6. Internal links and client navigation do not leak users to the zone origin, + unless the link is intentionally external. + +7. Canonical, Open Graph, sitemap, and robots metadata use the first-party + PolicyEngine URL when the app is meant to appear first-party. + +8. API calls are intentional. Absolute external APIs are usually fine. Root + relative calls like `/api/...` need scrutiny because they may hit the website + host instead of the zone. + +9. Local development behavior is understood. A local-only override for + `basePath` is useful, but not required for production multizone readiness. + +10. Dev-only asset/base path behavior is documented if needed. A useful pattern + is to disable `assetPrefix` or `basePath` during `next dev`, while keeping + production and preview builds path-scoped. + +## Required Pass Criteria + +An app can be considered `multizone-ready` only if these pass: + +- Host has base and nested rewrites for the public path. +- Host rewrite priority beats conflicting website routes. +- Zone origin returns HTTP 200 for the public path. +- Host URL returns HTTP 200 for the public path. +- Critical JS/CSS assets load through the host URL. +- Zone assets do not collide with another app's `/_next` assets. +- Refreshing the public path returns the app, not a website 404 or iframe + wrapper. + +## Recommended Checks + +These should pass before promoting more apps in the same pattern: + +- Canonical URL points to `policyengine.org`. +- Open Graph URL points to `policyengine.org`. +- Sitemap/robots behavior is coherent. +- Known nested routes refresh through the host. +- Internal app links stay on the first-party path. +- API calls do not accidentally target the website host. +- Dev server behavior is ergonomic enough for app owners. For example, the app + can run at localhost root during `next dev` while production assets stay + path-scoped. + +## Dev Environment Pattern + +The merged household API docs PR +`PolicyEngine/household-api-docs#11` provides a useful pattern for multizone +dev ergonomics. It makes the zone asset prefix production-only: + +```js +import { PHASE_DEVELOPMENT_SERVER } from 'next/constants.js'; + +export default function nextConfig(phase) { + const isDev = phase === PHASE_DEVELOPMENT_SERVER; + + return { + output: 'export', + assetPrefix: isDev ? undefined : '/_zones/household-api-docs', + images: { + unoptimized: true, + }, + trailingSlash: true, + }; +} +``` + +Use this pattern when a zone needs scoped production assets but app developers +should still be able to run the zone directly during local development. This is +similar in spirit to Oregon Kicker's local override: + +```js +const basePath = process.env.NEXT_PUBLIC_BASE_PATH === "" + ? undefined + : process.env.NEXT_PUBLIC_BASE_PATH || "/us/oregon-kicker-refund"; +``` + +For audits, record whether the app already has a local-dev escape hatch for +`basePath` or `assetPrefix`. Missing dev ergonomics should usually be a +follow-up, not a blocker, unless it prevents the app team from developing the +zone independently. + +## Static SPA Path Pattern + +Vite, SvelteKit static export, and GitHub Pages apps usually do not use Next +`basePath` or `assetPrefix`, but they still need equivalent path-scoped assets +before promotion to a first-party route. + +For Vite apps, inspect `base` in `vite.config.*`. A GitHub Pages project base +such as: + +```js +export default defineConfig({ + base: "/example-project/", +}); +``` + +is not sufficient for a PolicyEngine country route such as +`/us/example-project` unless the host also proxies `/example-project` assets. +Prefer adding a PolicyEngine build mode: + +```sh +vite build --base /us/example-project/ +``` + +For SvelteKit static exports, inspect `kit.paths.base` in `svelte.config.*`. +An environment-driven base path is useful: + +```js +paths: { + base: process.env.BASE_PATH || "/example-project", +} +``` + +Then the production PolicyEngine build can set: + +```sh +BASE_PATH=/us/example-project npm run build +``` + +For both patterns, verify live HTML emits assets under the intended public path +or under a separately proxied prefix. Root or GitHub Pages project-path assets +should be treated as zone work until the host rewrites and deployment mode are +explicit. + +## Useful Commands + +Find the app in the website host: + +```sh +rg -n "example-app|rewrites|beforeFiles" website/next.config.ts website/src/data/apps.json +``` + +Find the source repo: + +```sh +gh api 'search/repositories?q=org:PolicyEngine+example-app' --jq '.items[] | {name,full_name,html_url,description}' +``` + +Search source code in the org: + +```sh +gh api 'search/code?q=org:PolicyEngine+example-app' --jq '.items[] | {name,path,repository:.repository.full_name,html_url}' +``` + +Read a Next config from GitHub: + +```sh +gh api 'repos/PolicyEngine/example-app/contents/frontend/next.config.js?ref=main' --jq '.content' | base64 -d +``` + +Check deployed response headers: + +```sh +curl -sI https://example-app.vercel.app/us/example-app +``` + +Fetch deployed HTML for asset and metadata inspection: + +```sh +curl -s https://example-app.vercel.app/us/example-app +``` + +## Audit Result Template + +Use this format for each audited app: + +```md +## App Title + +- Recommendation: multizone-ready | multizone-host-work | multizone-zone-work | simple-rewrite | iframe | needs-investigation +- Public path: /us/example-app +- Origin: https://example-app.vercel.app +- Source repo: PolicyEngine/example-app +- Current host setup: beforeFiles rewrite | afterFiles rewrite | iframe route | dedicated page | none +- Current zone setup: basePath | assetPrefix | root app | static site | unknown +- Target zone setup: basePath | assetPrefix | path-scoped assets | unknown +- Host status: pass/fail summary +- Zone status: pass/fail summary +- Asset status: pass/fail summary +- Dev setup status: pass/fail summary +- Metadata status: pass/fail summary +- API status: pass/fail summary +- Blockers: none | list concrete blockers +- Follow-ups: concrete next actions +- Rubric Finding: only include if this app reveals a reusable pattern not covered by the rubric +``` + +## Baseline Example: Oregon Kicker Refund + +- Recommendation: `multizone-ready` +- Public path: `/us/oregon-kicker-refund` +- Origin: `https://oregon-kicker-refund.vercel.app` +- Source repo: `PolicyEngine/oregon-kicker-refund` +- Current host setup: `beforeFiles` base and nested rewrites in + `website/next.config.ts` +- Current zone setup: Next `basePath` defaults to + `/us/oregon-kicker-refund` +- Asset status: emits assets under + `/us/oregon-kicker-refund/_next/static/...` +- Metadata status: canonical, Open Graph URL, and sitemap point to + `https://policyengine.org/us/oregon-kicker-refund` +- API status: calls `https://api.policyengine.org` directly +- Blockers: none found +- Follow-ups: use as the baseline for future multizone candidates diff --git a/website/src/app/[countryId]/dev-tools/multizone-routing/MultizoneRoutingClient.tsx b/website/src/app/[countryId]/dev-tools/multizone-routing/MultizoneRoutingClient.tsx new file mode 100644 index 000000000..38597fe40 --- /dev/null +++ b/website/src/app/[countryId]/dev-tools/multizone-routing/MultizoneRoutingClient.tsx @@ -0,0 +1,426 @@ +"use client"; + +import { useMemo, useState } from "react"; +import type { CSSProperties } from "react"; +import { IconSearch } from "@tabler/icons-react"; +import { Input } from "@/components/ui/input"; +import { colors, spacing, typography } from "@policyengine/design-system/tokens"; +import type { RecommendationKind, RoutingInventoryRow } from "./routingInventory"; + +type AuditStatus = "not-started" | "in-progress" | "audited" | "blocked"; + +export type TeamInput = { + recommendation: RecommendationKind; + auditSession?: string; + auditStatus?: AuditStatus; + comment: string; +}; + +const RECOMMENDATION_LABELS: Record<RecommendationKind, string> = { + iframe: "Keep iframe/static", + rewrite: "Simple rewrite", + multizone: "Multi-zone", +}; + +const STATUS_LABELS: Record<AuditStatus, string> = { + "not-started": "Not started", + "in-progress": "In progress", + audited: "Audited", + blocked: "Blocked", +}; + +function badgeStyle(kind: RecommendationKind): CSSProperties { + const palette: Record< + RecommendationKind, + { bg: string; color: string; border: string } + > = { + iframe: { + bg: colors.primary[50], + color: colors.primary[800], + border: colors.primary[200], + }, + rewrite: { + bg: "#FFF7ED", + color: colors.text.warning, + border: "#FED7AA", + }, + multizone: { + bg: "#F5F3FF", + color: "#6D28D9", + border: "#DDD6FE", + }, + }; + const selected = palette[kind]; + return { + display: "inline-flex", + alignItems: "center", + border: `1px solid ${selected.border}`, + borderRadius: "999px", + padding: "2px 8px", + backgroundColor: selected.bg, + color: selected.color, + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.semibold, + whiteSpace: "nowrap", + }; +} + +function statusBadgeStyle(status: AuditStatus): CSSProperties { + const palette: Record< + AuditStatus, + { bg: string; color: string; border: string } + > = { + "not-started": { + bg: colors.gray[50], + color: colors.text.secondary, + border: colors.border.light, + }, + "in-progress": { + bg: "#EFF6FF", + color: "#1D4ED8", + border: "#BFDBFE", + }, + audited: { + bg: "#ECFDF5", + color: "#047857", + border: "#A7F3D0", + }, + blocked: { + bg: "#FEF2F2", + color: "#B91C1C", + border: "#FECACA", + }, + }; + const selected = palette[status]; + return { + display: "inline-flex", + alignItems: "center", + border: `1px solid ${selected.border}`, + borderRadius: "999px", + padding: "2px 8px", + backgroundColor: selected.bg, + color: selected.color, + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.semibold, + whiteSpace: "nowrap", + }; +} + +export default function MultizoneRoutingClient({ + rows, + reviewInputs, +}: { + rows: RoutingInventoryRow[]; + reviewInputs: Record<string, TeamInput>; +}) { + const [search, setSearch] = useState(""); + const [country, setCountry] = useState("all"); + const [kind, setKind] = useState<RecommendationKind | "all">("all"); + const [origin, setOrigin] = useState("all"); + + const mergedRows = useMemo( + () => + rows.map((row) => ({ + ...row, + teamRecommendation: + reviewInputs[row.id]?.recommendation ?? row.recommendationKind, + auditSession: reviewInputs[row.id]?.auditSession ?? "Unassigned", + auditStatus: reviewInputs[row.id]?.auditStatus ?? "not-started", + teamComment: reviewInputs[row.id]?.comment ?? "", + })), + [reviewInputs, rows], + ); + + const filteredRows = useMemo(() => { + const term = search.trim().toLowerCase(); + return mergedRows.filter((row) => { + const haystack = [ + row.title, + row.slug, + row.country, + row.path, + row.currentSetup, + row.recommendedSetup, + row.framework, + row.deployed, + row.source, + row.notes, + row.auditSession, + row.auditStatus, + row.teamComment, + ] + .join(" ") + .toLowerCase(); + return ( + (!term || haystack.includes(term)) && + (country === "all" || row.country === country) && + (kind === "all" || row.teamRecommendation === kind) && + (origin === "all" || row.origin === origin) + ); + }); + }, [country, kind, mergedRows, origin, search]); + + const counts = useMemo( + () => ({ + total: rows.length, + apps: rows.filter((row) => row.origin === "apps.json").length, + rewrites: rows.filter((row) => /rewrite|proxy/i.test(row.currentSetup)) + .length, + multizone: mergedRows.filter( + (row) => row.teamRecommendation === "multizone", + ).length, + audited: mergedRows.filter((row) => row.auditStatus === "audited") + .length, + }), + [mergedRows, rows], + ); + + return ( + <div + style={{ + padding: spacing["3xl"], + display: "flex", + flexDirection: "column", + gap: spacing["2xl"], + }} + > + <div className="tw:grid tw:grid-cols-1 tw:md:grid-cols-4 tw:gap-lg"> + {[ + ["Total rows", counts.total], + ["apps.json rows", counts.apps], + ["Team multizone picks", counts.multizone], + ["Audited", counts.audited], + ].map(([label, value]) => ( + <div + key={label} + style={{ + backgroundColor: colors.white, + border: `1px solid ${colors.border.light}`, + borderRadius: spacing.radius.container, + padding: spacing.xl, + }} + > + <div + style={{ + color: colors.primary[800], + fontSize: typography.fontSize["3xl"], + fontWeight: typography.fontWeight.bold, + lineHeight: 1, + }} + > + {value} + </div> + <div + style={{ + marginTop: spacing.xs, + color: colors.text.secondary, + fontSize: typography.fontSize.sm, + }} + > + {label} + </div> + </div> + ))} + </div> + + <div + style={{ + backgroundColor: colors.primary[50], + border: `1px solid ${colors.primary[200]}`, + borderRadius: spacing.radius.container, + padding: spacing.xl, + color: colors.primary[800], + fontSize: typography.fontSize.sm, + lineHeight: typography.lineHeight.relaxed, + }} + > + Team input is shared through{" "} + <code>website/src/data/multizoneRoutingReview.json</code>. Edit that + file in this PR branch to update the Audit session, Audit status, Team + input, and Team comment columns; the preview will reflect committed + changes after redeploy. + </div> + + <div + className="tw:flex tw:flex-col tw:lg:flex-row tw:gap-md" + style={{ alignItems: "stretch" }} + > + <label + className="tw:flex tw:items-center tw:gap-sm tw:flex-1" + style={{ + backgroundColor: colors.white, + border: `1px solid ${colors.border.light}`, + borderRadius: spacing.radius.element, + paddingLeft: spacing.md, + }} + > + <IconSearch size={18} color={colors.text.tertiary} /> + <Input + value={search} + onChange={(event) => setSearch(event.target.value)} + placeholder="Search title, slug, setup, framework, source, comments..." + style={{ + border: "none", + boxShadow: "none", + }} + /> + </label> + + <select + value={country} + onChange={(event) => setCountry(event.target.value)} + > + <option value="all">All countries</option> + <option value="us">US</option> + <option value="uk">UK</option> + <option value="global">Global</option> + </select> + + <select + value={kind} + onChange={(event) => + setKind(event.target.value as RecommendationKind | "all") + } + > + <option value="all">All recommendations</option> + <option value="iframe">Keep iframe/static</option> + <option value="rewrite">Simple rewrite</option> + <option value="multizone">Multi-zone</option> + </select> + + <select + value={origin} + onChange={(event) => setOrigin(event.target.value)} + > + <option value="all">All sources</option> + <option value="apps.json">apps.json</option> + <option value="route">Other route/rewrite</option> + </select> + </div> + + <div + style={{ + overflow: "auto", + backgroundColor: colors.white, + border: `1px solid ${colors.border.light}`, + borderRadius: spacing.radius.container, + }} + > + <table + style={{ + width: "100%", + minWidth: "1650px", + borderCollapse: "collapse", + }} + > + <thead> + <tr> + {[ + "App title", + "Country", + "Current setup", + "Suggested setup", + "Audit session", + "Audit status", + "Team input", + "Team comment", + "Framework", + "Deployed", + "Source", + "Notes", + ].map((heading) => ( + <th + key={heading} + style={{ + position: "sticky", + top: 0, + zIndex: 1, + backgroundColor: colors.gray[50], + borderBottom: `1px solid ${colors.border.light}`, + padding: `${spacing.sm} ${spacing.md}`, + textAlign: "left", + color: colors.gray[700], + fontSize: typography.fontSize.xs, + fontWeight: typography.fontWeight.bold, + textTransform: "uppercase", + }} + > + {heading} + </th> + ))} + </tr> + </thead> + <tbody> + {filteredRows.map((row) => ( + <tr key={row.id}> + <td style={cellStyle}> + <strong>{row.title}</strong> + <div style={subtleMonoStyle}>{row.path}</div> + <div style={subtleMonoStyle}>{row.id}</div> + </td> + <td style={cellStyle}> + <span style={badgeStyle("iframe")}> + {row.country.toUpperCase()} + </span> + </td> + <td style={cellStyle}>{row.currentSetup}</td> + <td style={cellStyle}> + <span style={badgeStyle(row.recommendationKind)}> + {row.recommendedSetup} + </span> + </td> + <td style={cellStyle}>{row.auditSession}</td> + <td style={cellStyle}> + <span style={statusBadgeStyle(row.auditStatus)}> + {STATUS_LABELS[row.auditStatus]} + </span> + </td> + <td style={cellStyle}> + <span style={badgeStyle(row.teamRecommendation)}> + {RECOMMENDATION_LABELS[row.teamRecommendation]} + </span> + </td> + <td style={cellStyle}> + {row.teamComment || ( + <span style={{ color: colors.text.tertiary }}> + No shared comment yet + </span> + )} + </td> + <td style={cellStyle}>{row.framework}</td> + <td style={cellStyle}>{row.deployed}</td> + <td style={{ ...cellStyle, ...subtleMonoStyle }}> + {row.source.startsWith("http") ? ( + <a href={row.source}>{row.source}</a> + ) : ( + row.source + )} + </td> + <td style={{ ...cellStyle, color: colors.text.secondary }}> + {row.notes} + </td> + </tr> + ))} + </tbody> + </table> + </div> + </div> + ); +} + +const cellStyle: CSSProperties = { + borderBottom: `1px solid ${colors.border.light}`, + padding: `${spacing.md} ${spacing.md}`, + verticalAlign: "top", + fontSize: typography.fontSize.sm, + lineHeight: typography.lineHeight.normal, +}; + +const subtleMonoStyle: CSSProperties = { + marginTop: spacing.xs, + color: colors.text.secondary, + fontFamily: + "SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace", + fontSize: typography.fontSize.xs, + wordBreak: "break-word", +}; diff --git a/website/src/app/[countryId]/dev-tools/multizone-routing/page.tsx b/website/src/app/[countryId]/dev-tools/multizone-routing/page.tsx new file mode 100644 index 000000000..cdda79b59 --- /dev/null +++ b/website/src/app/[countryId]/dev-tools/multizone-routing/page.tsx @@ -0,0 +1,35 @@ +import type { Metadata } from "next"; +import HeroSection from "@/components/static/HeroSection"; +import reviewInputs from "@/data/multizoneRoutingReview.json"; +import { colors, spacing } from "@policyengine/design-system/tokens"; +import MultizoneRoutingClient from "./MultizoneRoutingClient"; +import type { TeamInput } from "./MultizoneRoutingClient"; +import { getRoutingInventoryRows } from "./routingInventory"; + +export const metadata: Metadata = { + title: "Multizone routing strategy", + description: + "Review PolicyEngine interactive apps and decide which should remain iframe embeds, use simple rewrites, or become multi-zone surfaces.", +}; + +export default function MultizoneRoutingPage() { + return ( + <div + style={{ + minHeight: `calc(100vh - ${spacing.layout.header})`, + display: "flex", + flexDirection: "column", + backgroundColor: colors.background.tertiary, + }} + > + <HeroSection + title="Multizone routing strategy" + description="Review interactive app routes, collect team input, and decide which surfaces should remain iframe embeds, use simple rewrites, or become multi-zone apps." + /> + <MultizoneRoutingClient + rows={getRoutingInventoryRows()} + reviewInputs={reviewInputs as Record<string, TeamInput>} + /> + </div> + ); +} diff --git a/website/src/app/[countryId]/dev-tools/multizone-routing/routingInventory.ts b/website/src/app/[countryId]/dev-tools/multizone-routing/routingInventory.ts new file mode 100644 index 000000000..edcd3be13 --- /dev/null +++ b/website/src/app/[countryId]/dev-tools/multizone-routing/routingInventory.ts @@ -0,0 +1,304 @@ +import appsData from "@/data/apps.json"; + +export type RecommendationKind = "iframe" | "rewrite" | "multizone"; +export type InventoryOrigin = "apps.json" | "route"; + +export interface RoutingInventoryRow { + id: string; + origin: InventoryOrigin; + title: string; + slug: string; + country: string; + path: string; + currentSetup: string; + recommendedSetup: string; + recommendationKind: RecommendationKind; + framework: string; + deployed: string; + source: string; + notes: string; +} + +interface AppEntry { + type: string; + slug: string; + title: string; + source: string; + countryId: string; + displayWithResearch?: boolean; +} + +const MULTIZONE_CANDIDATES = new Set([ + "keep-your-pay-act", + "oregon-kicker-refund", + "working-parents-tax-relief-act", + "watca", +]); + +const DIRECT_REWRITE_SLUGS = new Set([ + "keep-your-pay-act", + "oregon-kicker-refund", + "working-parents-tax-relief-act", + "watca", + "state-legislative-tracker", +]); + +const STATIC_SLUGS = new Set([ + "2025-year-in-review", + "tanf-calculator", + "uk-salary-sacrifice-tool", + "obbba-household-explorer", + "obbba-household-by-household", + "obbba-scatter", + "snap-district-map", +]); + +function publicPath(app: AppEntry) { + return `/${app.countryId}/${app.slug}`; +} + +function deployedFromSource(source: string) { + if (source.startsWith("/assets/")) return "PolicyEngine static asset"; + if (source.startsWith("/")) return "PolicyEngine route + rewrite"; + + const hostname = new URL(source).hostname; + if (hostname.endsWith("vercel.app")) return "Vercel"; + if (hostname === "policyengine.github.io") return "GitHub Pages"; + if (hostname.endsWith(".modal.run")) return "Modal"; + if (hostname.endsWith(".streamlit.app")) return "Streamlit Cloud"; + return hostname; +} + +function frameworkFromApp(app: AppEntry) { + if (app.source.includes("streamlit.app")) return "Streamlit"; + if (app.source.startsWith("/assets/")) return "Bundled static SPA"; + if (app.source.startsWith("/")) return "PolicyEngine/rewritten embed"; + if (app.source.includes("policyengine.github.io")) { + return "Static site/SPAs on GitHub Pages"; + } + if (MULTIZONE_CANDIDATES.has(app.slug)) { + return "Likely Next.js/Vercel path app; verify owning repo"; + } + if (app.source.includes("vercel.app")) { + return "Unknown Vercel app; verify owning repo"; + } + if (app.source.includes("modal.run")) { + return "Modal app; framework unclear"; + } + return "Unknown; verify owning repo"; +} + +function currentSetup(app: AppEntry) { + if (app.slug === "california-wealth-tax") { + return "Dedicated website page with iframe; embed path rewrites to Vercel"; + } + if (DIRECT_REWRITE_SLUGS.has(app.slug)) { + return "Direct rewrite/proxy for public path; apps.json fallback exists"; + } + if (app.type === "obbba-iframe") { + return "Specialized OBBBA iframe wrapper from apps.json"; + } + if (app.source.startsWith("/assets/")) { + return "Generic website iframe wrapper to local static asset"; + } + return "Generic website iframe wrapper from apps.json"; +} + +function recommendationForApp(app: AppEntry): { + label: string; + kind: RecommendationKind; + notes: string; +} { + if (MULTIZONE_CANDIDATES.has(app.slug)) { + return { + label: "Multi-zone candidate", + kind: "multizone", + notes: "Durable policy calculator with first-party public path.", + }; + } + + if (app.slug === "california-wealth-tax") { + return { + label: "Keep dedicated rewrite now; multizone if it grows beyond embed mode", + kind: "rewrite", + notes: "Has custom query syncing via SyncedAppIframe.", + }; + } + + if (app.slug === "state-legislative-tracker") { + return { + label: "Keep direct rewrite/proxy; formalize if root assets keep leaking", + kind: "rewrite", + notes: "Has extra tracker asset rewrites today.", + }; + } + + if (STATIC_SLUGS.has(app.slug)) { + return { + label: "Audit for multi-zone/static route ownership", + kind: "multizone", + notes: "Static hosting can work, but audit first-party route ownership before keeping iframe.", + }; + } + + if (!app.displayWithResearch) { + if (app.source.includes("vercel.app")) { + return { + label: "Audit for multi-zone if kept as product surface", + kind: "multizone", + notes: "Not shown in research listing; lower priority unless it remains public.", + }; + } + + return { + label: "Keep iframe", + kind: "iframe", + notes: "Not shown in research listing.", + }; + } + + if (app.source.includes("vercel.app")) { + return { + label: "Multi-zone target; audit zone readiness", + kind: "multizone", + notes: "Shown in research listing and deployed on Vercel.", + }; + } + + return { + label: "Keep iframe", + kind: "iframe", + notes: app.displayWithResearch ? "Shown in research listing." : "Not shown in research listing.", + }; +} + +const EXTRA_ROWS: RoutingInventoryRow[] = [ + { + id: "route:ads-dashboard", + origin: "route", + title: "PolicyEngine ads transparency dashboard", + slug: "ads-dashboard", + country: "us", + path: "/us/ads-dashboard", + currentSetup: "Dedicated website iframe page", + recommendedSetup: "Keep iframe", + recommendationKind: "iframe", + framework: "Unknown Vercel app; verify owning repo", + deployed: "Vercel", + source: "https://policyengine-ads-dashboard.vercel.app?embedded=true", + notes: "Route exists outside apps.json.", + }, + { + id: "route:ai-inequality", + origin: "route", + title: "AI and inequality", + slug: "ai-inequality", + country: "global", + path: "/:countryId/ai-inequality", + currentSetup: "Dedicated website iframe page", + recommendedSetup: "Keep iframe unless it becomes a product surface", + recommendationKind: "iframe", + framework: "Unknown Vercel app; verify owning repo", + deployed: "Vercel", + source: "https://ai-inequality-theta.vercel.app", + notes: "Country is passed as a query parameter for non-US.", + }, + { + id: "route:model", + origin: "route", + title: "PolicyEngine model documentation", + slug: "model", + country: "global", + path: "/:countryId/model", + currentSetup: "afterFiles rewrite to Vercel", + recommendedSetup: "Simple rewrite is fine; multizone if docs need owned assets/routes", + recommendationKind: "rewrite", + framework: "Unknown Vercel docs app; verify owning repo", + deployed: "Vercel", + source: "https://policyengine-model-phi.vercel.app/", + notes: "Rewrite injects country query.", + }, + { + id: "route:api", + origin: "route", + title: "Household API docs", + slug: "api", + country: "us", + path: "/us/api", + currentSetup: "beforeFiles rewrite plus scoped _zones asset proxy", + recommendedSetup: "Already closest to multi-zone; keep formal zone asset prefix", + recommendationKind: "multizone", + framework: "Likely Next.js/Vercel docs app with assetPrefix", + deployed: "Vercel", + source: "https://household-api-docs-policy-engine.vercel.app/us/api/", + notes: "Uses /_zones/household-api-docs asset proxy.", + }, + { + id: "route:taxsim", + origin: "route", + title: "TAXSIM emulator", + slug: "taxsim", + country: "us", + path: "/us/taxsim", + currentSetup: "afterFiles rewrite to Vercel", + recommendedSetup: "Simple rewrite now; multizone if it becomes a durable app shell", + recommendationKind: "rewrite", + framework: "Unknown Vercel app; verify owning repo", + deployed: "Vercel", + source: "https://policyengine-taxsim-policy-engine.vercel.app/us/taxsim", + notes: "Rewrite-only surface; not listed in apps.json.", + }, + { + id: "route:slides", + origin: "route", + title: "PolicyEngine slides", + slug: "slides", + country: "global", + path: "/slides", + currentSetup: "afterFiles rewrite to Vercel", + recommendedSetup: "Keep simple rewrite", + recommendationKind: "rewrite", + framework: "Unknown Vercel app; verify owning repo", + deployed: "Vercel", + source: "https://policyengine-slides.vercel.app/slides", + notes: "Utility surface outside country route tree.", + }, + { + id: "route:plugin-blog", + origin: "route", + title: "Plugin blog", + slug: "plugin-blog", + country: "global", + path: "/plugin-blog", + currentSetup: "afterFiles rewrite to GitHub Pages", + recommendedSetup: "Keep simple rewrite/static hosting", + recommendationKind: "iframe", + framework: "Static site on GitHub Pages", + deployed: "GitHub Pages", + source: "https://policyengine.github.io/plugin-blog/", + notes: "Static content surface rather than calculator app.", + }, +]; + +export function getRoutingInventoryRows(): RoutingInventoryRow[] { + const appRows = (appsData as AppEntry[]).map((app) => { + const recommendation = recommendationForApp(app); + return { + id: `app:${app.countryId}:${app.slug}`, + origin: "apps.json" as const, + title: app.title, + slug: app.slug, + country: app.countryId, + path: publicPath(app), + currentSetup: currentSetup(app), + recommendedSetup: recommendation.label, + recommendationKind: recommendation.kind, + framework: frameworkFromApp(app), + deployed: deployedFromSource(app.source), + source: app.source, + notes: recommendation.notes, + }; + }); + + return [...appRows, ...EXTRA_ROWS]; +} diff --git a/website/src/app/[countryId]/dev-tools/page.tsx b/website/src/app/[countryId]/dev-tools/page.tsx index 977d8c9d7..c49a2ac6e 100644 --- a/website/src/app/[countryId]/dev-tools/page.tsx +++ b/website/src/app/[countryId]/dev-tools/page.tsx @@ -54,6 +54,12 @@ const DEV_TOOL_CARDS = [ description: "Check the current status and availability of PolicyEngine API endpoints.", }, + { + slug: "multizone-routing", + title: "Multizone routing strategy", + description: + "Review interactive app routes and collect team input on iframe, rewrite, and multizone ownership.", + }, ]; export const metadata: Metadata = { diff --git a/website/src/data/multizoneRoutingReview.json b/website/src/data/multizoneRoutingReview.json new file mode 100644 index 000000000..6d02f6784 --- /dev/null +++ b/website/src/data/multizoneRoutingReview.json @@ -0,0 +1,290 @@ +{ + "app:uk:2025-year-in-review": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "Static Vite/GitHub Pages app is built for /2025-year-in-review, not /uk/2025-year-in-review; UK metadata also points to the US PolicyEngine URL. Needs zone base-path work plus host rewrites." + }, + "app:uk:2024-manifestos": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Older Vite embed. Public path returns 404 and root HTML emits /assets plus /vite.svg; needs path-scoped Vite base or asset prefix before first-party routing." + }, + "app:uk:autumn-budget-2025": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite dashboard. Public path returns 404 and root HTML emits /assets plus /vite.svg; needs path-scoped build and host rewrites." + }, + "app:uk:energy-price-shock": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Vite zone uses root asset paths and origin /uk/energy-price-shock returns 404; needs path-scoped base or dedicated asset prefix before host rewrites." + }, + "app:uk:local-areas-dashboard": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Older Next/pages dashboard. Public path returns 404 and root HTML emits /_next assets; needs basePath/assetPrefix and host rewrites." + }, + "app:uk:marriage": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Duplicate UK row for shared marriage app. Defer with US marriage: current production routing is US-scoped at /us/marriage while UK behavior uses ?country=uk." + }, + "app:uk:public-services-spending": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "CRA/static dashboard serves the public path but emits root /static assets; needs PUBLIC_URL or asset-prefix work before host rewrites." + }, + "app:uk:scotland-income-tax-reform": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Next zone has no basePath/assetPrefix, emits root /_next/static assets, and origin /uk/scotland-income-tax-reform returns 404; zone work needed before host rewrites." + }, + "app:uk:scottish-budget-2026-27": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite dashboard. Public path returns 404 and root HTML emits /assets; needs path-scoped Vite base and host rewrites." + }, + "app:uk:spring-statement-2026": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Next static export emits root /_next/static assets, lacks first-party canonical/OG metadata, and origin /uk/spring-statement-2026 returns 404." + }, + "app:uk:student-loan-visualisation": { + "recommendation": "rewrite", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Single-file static HTML with external D3/fonts and inline data; a simple rewrite to the root document is likely sufficient if kept as a static article-style interactive." + }, + "app:uk:two-child-limit-comparison": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Vite zone emits root /assets paths, origin /uk/two-child-limit-comparison returns 404, and code fetches root-relative /data/all-results.csv." + }, + "app:uk:uk-land-value-tax": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Next zone has no basePath/assetPrefix, emits root /_next/static assets, and origin /uk/uk-land-value-tax returns 404; zone work needed before host rewrites." + }, + "app:uk:uk-salary-sacrifice-tool": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "Vite app currently emits /uk-salary-sacrifice-analysis/assets on GitHub Pages and has no host rewrite. Needs a PolicyEngine public-path build or scoped asset prefix before multizone." + }, + "app:uk:uk-student-loan-calculator": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite app. Public path returns 404 and root HTML emits /assets plus /vite.svg; needs path-scoped build and host rewrites." + }, + "app:us:2025-year-in-review": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "Dedicated website iframe page wraps a Vite/GitHub Pages app built for /2025-year-in-review. Promote by rebuilding for /us/2025-year-in-review or adding scoped asset proxy plus beforeFiles rewrites." + }, + "app:us:cliff-watch": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Vite zone currently emits root /assets paths and origin /us/cliff-watch returns 404; also audit root-relative /api fallbacks before adding host rewrites." + }, + "app:us:california-wealth-tax": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Embed zone is path-scoped and metadata points to /us/california-wealth-tax. Promote by replacing the dedicated iframe page with a first-party route or keeping SyncedAppIframe only if query-sync embedding remains required." + }, + "app:us:2024-election-calculator": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Older Vite embed. Public path returns 404 and root HTML emits /assets; needs path-scoped build and host rewrites." + }, + "app:us:aca-calc": { + "recommendation": "rewrite", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Duplicate legacy slug for ACA calculator. Same origin as aca-reforms-calculator; prefer redirect/canonicalize to one public route rather than creating a second zone." + }, + "app:us:aca-reforms-calculator": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite app serves the public path via SPA fallback but emits root /assets; choose this as the canonical ACA route and add path-scoped build plus host rewrites." + }, + "app:us:child-tax-credit-2024-election-calculator": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite app serves the public path via SPA fallback but emits root /assets; needs path-scoped build before host rewrites." + }, + "app:us:child-tax-credit-calculator": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Older static app with script/ relative assets and public path 404. Needs source modernization/path-scoped static build before first-party routing." + }, + "app:us:encode-policy-multi-agent-ai": { + "recommendation": "iframe", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "Bundled local article artifact under website/public/assets/posts with colocated JS/CSS and no standalone source repo found; note root /vite.svg favicon. Keep iframe/static unless source ownership is reconstructed." + }, + "app:us:givecalc": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite app. Public path returns 404 and root HTML emits /assets; needs path-scoped build and host rewrites." + }, + "app:us:keep-your-pay-act": { + "recommendation": "multizone", + "auditSession": "Session A - direct rewrite calculators", + "auditStatus": "audited", + "comment": "Multizone-ready: repo defaults basePath to /us/keep-your-pay-act, live HTML emits path-scoped _next assets, metadata/sitemap use policyengine.org, and API calls go directly to api.policyengine.org." + }, + "app:us:marriage": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Defer migration decision. This is one shared US/UK app: UK logic exists via ?country=uk, but basePath, assets, metadata, and nav are US-scoped at /us/marriage. Handle US and UK marriage routing together." + }, + "app:us:obbba-household-by-household": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "SvelteKit static export already supports BASE_PATH, but live GitHub Pages assets and favicon are scoped to /obbba-household-by-household. Rebuild with BASE_PATH=/us/obbba-household-by-household and add host rewrites." + }, + "app:us:obbba-household-explorer": { + "recommendation": "iframe", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "Legacy/alias row points to the household-by-household origin but uses a different public slug. Keep iframe or canonicalize/remove rather than making a separate zone." + }, + "app:us:obbba-scatter": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "blocked", + "comment": "Current source URL https://policyengine.github.io/obbba-scatter returns 404 and no standalone PolicyEngine repo was found. Need source/deployment ownership before classification can be finalized." + }, + "app:us:oregon-kicker-refund": { + "recommendation": "multizone", + "auditSession": "Session A - direct rewrite calculators", + "auditStatus": "audited", + "comment": "Baseline pilot: zone repo already defaults basePath to /us/oregon-kicker-refund, emits path-scoped assets, and uses policyengine.org metadata." + }, + "app:us:pe84": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Next zone has empty next.config.ts, emits root /_next/static assets, uses root-relative icon metadata, and origin /us/pe84 returns 404." + }, + "app:us:salternative": { + "recommendation": "multizone", + "auditSession": "Session B - Vercel research interactives", + "auditStatus": "audited", + "comment": "Vite zone serves /us/salternative via SPA fallback but still emits root /assets paths and has generic title metadata; path-scoped base or asset prefix needed." + }, + "app:us:rhode-island-ctc-calculator": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Next app has no basePath and public path returns 404; root HTML emits /_next assets. Needs basePath/assetPrefix and first-party metadata." + }, + "app:us:snap-district-map": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "GitHub Pages deployment appears to serve unbuilt Vite source (/src/main.jsx) even though repo has base /snap-district-map. Fix production build, then add PolicyEngine base-path build and host rewrites." + }, + "app:us:state-legislative-tracker": { + "recommendation": "rewrite", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Modal/Vite app works through direct beforeFiles proxy plus /_tracker and root logo rewrites. Keep as direct proxy unless it moves to a first-party static/Next zone." + }, + "app:us:state-eitcs-ctcs": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Vite app. Public path returns 404 and root HTML emits /assets; needs path-scoped build and host rewrites." + }, + "app:us:tanf-calculator": { + "recommendation": "multizone", + "auditSession": "Session C - static/GitHub Pages/legacy embeds", + "auditStatus": "audited", + "comment": "Vite/GitHub Pages app emits /tanf-calculator/assets and host currently uses generic iframe wrapper. Needs /us/tanf-calculator build or scoped asset proxy plus beforeFiles rewrites." + }, + "app:us:taxation-of-benefits-reforms": { + "recommendation": "multizone", + "auditSession": "Session E - lower-priority/unlisted", + "auditStatus": "audited", + "comment": "Next static export supports env-driven basePath/assetPrefix, but current deployment has empty base path, root /_next assets, and public path 404. Set NEXT_PUBLIC_BASE_PATH=/us/taxation-of-benefits-reforms and add host rewrites." + }, + "app:us:working-parents-tax-relief-act": { + "recommendation": "multizone", + "auditSession": "Session A - direct rewrite calculators", + "auditStatus": "audited", + "comment": "Multizone-ready with a small polish follow-up: repo defaults basePath to /us/working-parents-tax-relief-act, assets and metadata are path/first-party, but favicon currently renders as /favicon.svg." + }, + "app:us:watca": { + "recommendation": "multizone", + "auditSession": "Session A - direct rewrite calculators", + "auditStatus": "audited", + "comment": "Multizone-ready for routing: repo hardcodes basePath /us/watca and host/assets work. Follow up to add policyengine.org canonical/OG/sitemap metadata and a next-dev basePath escape hatch." + }, + "route:api": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Reference zone: household-api-docs uses production-only assetPrefix /_zones/household-api-docs and disables it during next dev." + }, + "route:ads-dashboard": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Currently a dedicated iframe page. Vite app emits root /assets and /vite.svg, so it needs BASE_URL/public-path work before first-party multizone routing." + }, + "route:ai-inequality": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Currently dedicated iframe pages with country query handling. CRA app emits root /static assets and has subroute /income-shift, so it needs a public-path strategy before host rewrites." + }, + "route:model": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Current afterFiles rewrite injects country query but origin emits root /_next assets and has no basePath. Needs basePath/assetPrefix and first-party metadata before formal multizone." + }, + "route:plugin-blog": { + "recommendation": "rewrite", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Static GitHub Pages site is already path-scoped under /plugin-blog/assets and host has afterFiles rewrites. Keep simple rewrite/static hosting." + }, + "route:slides": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Multizone-ready for routing: Next app hardcodes basePath /slides and live HTML emits /slides/_next assets. Follow up with local-dev basePath escape hatch if desired." + }, + "route:taxsim": { + "recommendation": "multizone", + "auditSession": "Session D - special routes", + "auditStatus": "audited", + "comment": "Multizone-ready: Next static export uses basePath /us/taxsim, custom data asset helper, first-party metadata, and host rewrites. Minor follow-up: root icon metadata may resolve to host root." + } +}