Repository navigation
Stage 12A: Sign in to app v3 and save work to the account - #1252
Merged
Merged
Conversation
- BASE_URL now comes from VITE_API_URL (calculator-app: NEXT_PUBLIC_API_URL), defaulting to https://api.policyengine.org, so staging builds can use the staging API. - next.config.ts maps NEXT_PUBLIC_API_URL, NEXT_PUBLIC_AUTH_ENABLED and NEXT_PUBLIC_AUTH0_{DOMAIN,CLIENT_ID,AUDIENCE} (or their VITE_* spellings) onto import.meta.env.VITE_* for the shared app code. - Add @auth0/auth0-react to app and calculator-app. - Document the calculator environment in calculator-app/.env.example. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign-in (inert unless NEXT_PUBLIC_AUTH_ENABLED=true and Auth0 is configured):
- libs/auth: AuthProvider wraps @auth0/auth0-react (memory cache, rotating
refresh tokens with fallback, audience + offline_access) and loads the
PolicyEngine user from GET /v2/me for US/UK; useAuthStatus() exposes
status, user, signIn, signOut and getAccessToken. Without the provider,
or with sign-in off, status is 'disabled'.
- Mount the provider in [countryId]/providers.tsx and add the
/[countryId]/callback route that completes sign-in and returns to
appState.returnTo (same-origin paths only).
Account storage:
- v2UserClient: bearer-token fetch wrapper for ${BASE_URL}/v2/..., parsing
the {status, message, result} envelope and throwing V2ApiError(status).
- useAssociationBackend(): API stores only when signed in with a loaded
PolicyEngine user AND the country is US or UK; otherwise localStorage,
unchanged. Queries wait while the session is restored, and query keys
carry the PolicyEngine user ID (or 'anonymous') so caches split.
- Rewrite ApiPolicyStore/ApiHouseholdStore/ApiSimulationStore/ApiReportStore
and their adapters for /v2/user-{policies,households,simulations,reports}:
numeric legacy_*_id <-> string IDs, the association UUID as id,
list/get/create/update/delete, never a user_id. The API report store has
no createWithId (shared-report saves POST {legacy_report_id}), and local
sur- IDs resolve through this browser's import map or fall back to
localStorage so old links keep working.
- Replace every hard-coded isLoggedIn = false and the local simulation
store in createReportSimulations; pass the session's user ID instead of
MOCK_USER_ID. Saved geographies stay in localStorage for everyone; drop
the unused ApiGeographicStore and dead association payload types.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…abase path
Signed in (US/UK):
- Reforms are /v2/user-policies rows. Saving materializes the parameters
as a v1 policy (POST /{country}/policy, which dedupes) and stores
{legacy_policy_id, name, baseline, provenance}; parameter edits create a
new v1 policy and repoint legacy_policy_id; reading rebuilds parameters
from the v1 policy with the app's policy -> parameter conversion. A reform
kept only in this browser is added to the account when saved.
- Flagship report records are /v2/user-reports rows with the v3 fields
(title <-> name, sourceNote, provisions incl. dated intervals, validation,
reformId <-> user_policy_id). runFlagshipReport writes exactly one
association right after the v1 report exists; the report page reads the
account record for provenance on other devices.
Signed out (and outside the US and UK) the v3 stores use localStorage
exactly as before, now without first trying the never-provisioned
/api/reforms and /api/reports routes.
Remove those Next route handlers, calculator-app/src/db (Drizzle schema,
migrations, postgres-js client), DATABASE_URL, the PGlite route tests, the
drizzle/postgres/neon/pglite dependencies and db:* scripts, and the
storeBackend switch they relied on.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- AuthMenu: "Sign in" when signed out, a user menu (initial, My account, Sign out) when signed in, nothing when sign-in is disabled. Shown in the calculator header (desktop and mobile) and in the flagship top bar, which replaces that header when the flagship shell is on. - Sidebar "My account" links to the account page when sign-in is enabled. - Account page (/[countryId]/account, also replacing the Vite router placeholder): user details, "Download my data" (GET /v2/me/export), "Delete my account" (confirm, DELETE /v2/me, sign out), and saving this device's work to the account. - First sign-in import (US/UK): when the browser holds saved work the account lacks, offer "Save the work on this device to your account?". Gathers the policy/household/simulation/report associations, reforms, flagship records and provenance stash; materializes reforms without a v1 policy; merges a flagship run's sur-/lfr- records into one report; POSTs /v2/me/imports per country in chunks of at most 500 items (and about 1 MB); reports Canada/Nigeria/Israel items as staying on the device; persists the id_map (per user) so old local links resolve; and never deletes local data. "Not now" is remembered. - README: how sign-in and saved work behave, and the variables to set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Account policy rows include policies saved from the legacy builder. v3 reforms always carry provenance and legacy policies never do, so the reform store now reads only rows with provenance. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@auth0/auth0-react 2.27.0 resolved @auth0/auth0-spa-js 2.28.3 (published 2026-10-09) and @auth0/auth0-auth-js 1.16.1 (2026-10-02). Override them to 2.27.0 and 1.15.0, both published in September. Re-locked with the pinned bun 1.2.21. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The API reports items it cannot parse as skipped with a null client_ref. Keep them in the summary but out of the per-ref skip bookkeeping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This was referenced Oct 12, 2026
Merged
Closed
Saved-work queries wait until the session is restored, because until then it is not known whether saved work is in the account or in this browser. A waiting TanStack query is neither loading nor holding data, so report pages said "Report not found", flagship sur- links were marked unresolvable, and lists showed their empty state. With sign-in enabled this hit signed-out visitors too. The association hooks now go through useSessionGatedQuery, which reports a query that is waiting for the session as loading. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
If GET /v2/me failed (after one retry) the session became 'authenticated'
with no PolicyEngine user, so the store switch quietly chose
localStorage: the header still showed the user menu, new saves stayed on
this device, and nothing retried until a remount.
- A US or UK session whose PolicyEngine user could not be loaded is now
'account_error'. It gets the account stores backed by a client that
rejects every call with AccountUnavailableError, so lists and reports
show an error and saves fail with a clear message instead of going to
localStorage. A flagship run is refused up front, as it is while the
session is still being restored.
- /v2/me retries three times with backoff (1s, 2s, 4s), then again when
the window regains focus or the network comes back, and on
retryAccount.
- The user menu marks the avatar, says the account could not be loaded,
and offers "Try again" alongside "Sign out"; the account page offers
both too.
- selectAssociationBackend takes the user as-is, removing the
`{ id } as MeItem` cast.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An item the import sent that came back in neither id_map nor skipped
(the server reports unparseable items without a client_ref) stayed
pending, so LocalDataImportPrompt reopened on every visit, and closing
it after an import recorded nothing.
- Every item the server did not save is now recorded as skipped, with
the server's reason or as invalid, and appears once in the summary.
- Closing the prompt after an import ran ("Done", or closing after a
failure) is remembered like "Not now"; the account page still offers
what is left.
- The summary only says that what an item refers to no longer exists for
unknown_legacy_reference skips; other skips read "could not be saved".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed in, each account store handled an ID that only this browser knows (saved while signed out, never imported) differently: the report store read and updated the local copy, the policy, household and simulation stores answered 404, and the reform store created a fresh row without recording the rf- mapping, so a later import added the reform again. One policy now lives in AccountAssociationRoutes and every store follows it: an imported local ID resolves to its account row; any other local ID is read from this browser; changing it copies it into the account under its local ID as client_ref and records the mapping (on a 409, the existing row with that client_ref is updated instead); deleting it deletes this browser's copy. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…owser - A signed-in run stashed its title, source note and provisions in localStorage under the v1 report ID, where whoever used this browser next would see them on that report. Signed-in runs now keep that provenance in memory for the session; the account record carries it everywhere else. - Signing out clears that stash and the draft reform, which may hold an account reform loaded for editing. - Import bookkeeping stays filed per PolicyEngine user. Items one account imported from this browser are no longer offered to a different account that signs in here; deleting the importing account releases them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The client's fallback for a 422 said "Something this refers to no longer
exists", but the Public API also answers 422 for request validation, and
the 409 fallback said every conflict was "managed elsewhere", although a
duplicate client_ref conflicts too. Both now name the likely causes
instead of asserting one. (The `{ id } as MeItem` cast went with the
account-error change.)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
While the session was being restored (and /v2/me now retries for up to about 7s), the store hooks handed out the localStorage stores, so a signed-in user's save in that window landed in this browser. - The auth context gains settledSession(): it answers once the status leaves 'loading', with the state the session settled in. useAssociationBackendResolver turns that into the backend for the country. - Every saved-work store the hooks hand out (policies, households, simulations, reports, reforms, flagship report records) now forwards each call to the store for the settled session. That covers the association mutations and the direct writers: the report builder's submission and modify hooks, useCreateReport, ReportAdjustPanel, DraftHeader and the Reforms page. useSaveSharedReport and the flagship run resolve the backend the same way; the run no longer errors while the session restores. - Signed-out visitors wait only for the restore, then save locally. - The v2 client asks for the access token at request time, so a request that waited carries the signed-in user's token. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
useCreatePolicy, useCreateHousehold and useCreateSimulation create the v1 artifact and then save its association, catching an association failure with console.error only. That was harmless while saves went to localStorage; with account storage a 413, a 503 or an unavailable account means the item silently is not saved. The failure is now reported through reportSaveFailure and shown by SaveFailureNotice, a FloatingAlert mounted once in each app shell so it stays up after the builder dialog closes. The builder still carries on with the created artifact. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- deferredStore replaces the six hand-written forwarding stores. It forwards each listed method to the store for the settled session, with that store as `this`; the method map's type requires every method. - useSavedWorkStore replaces the four near-identical use*AssociationStoreForMode bodies and backs the reform and flagship report stores too. The test-only resolveStore is gone; the selection tests now check which store is used by what it calls. - The account report store implements createWithId as create (the account assigns IDs), so createWithId is no longer optional and the create mutation needs no casts. - The local delete methods name their callback parameters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed in, the reforms list fetched every reform's v1 policy through Promise.all, so one 5xx or 429 failed the whole list: "Yours" showed its error state and ReportAdjustPanel aborted its run. Every list load also refetched every v1 policy. - ApiReformStore.findByUser settles each reform separately. A reform whose v1 policy does not load is listed with parametersUnavailable set and no parameters. - The Reforms page labels that card, explains it in the detail view with a "Try again" that reloads the list, and disables run, save, Build and duplicate for it, which would otherwise act on an empty reform. Delete still works. - ReportAdjustPanel never matches such a reform, so it saves the adjusted set as a new reform and runs. - v1 policies never change, so useReformStore fetches each one through the query cache, keyed by country and policy ID. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssion - AccountAssociationRoutes.writeCountry is the one rule for where a write goes: the item's country when it has account storage, otherwise the store's. The four association stores use it, and so now do the reform store (including where it materializes the v1 policy) and the flagship report store, which wrote in the page's country. - promote's 409 recovery lists and patches in the country it created in, not the page's. - useUserReportById listed saved simulations, policies and households under the report row's own user ID: '' until the row loaded, firing full account list calls under an empty key, and 'anonymous' for a local report read while signed in, caching account rows under the local user. It now lists them for the session's user, once the report row has loaded. (The report-by-ID key itself is unchanged.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Reforms and builder policies share the account's /v2/user-policies rows. Signed out they live in separate stores, so the legacy Policies list and the report builder never showed reforms; signed in, they did. UserPolicyAdapter.isReform (the row carries provenance) now decides, for both lists: the policy store's list leaves reforms out, the reform store lists only reforms. Lookups by v1 policy ID and by association ID are unchanged and still find reform rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The import left out anything another account had imported from this browser. Anyone signed out on the browser can already see those items, so hiding them protected nothing, but it kept a second person from claiming their own signed-out work once someone else had imported it. Each account is now offered the browser's signed-out items it has not imported (or had skipped) itself; readRefsImportedByAnyone is gone. The prompt and account page say these are items saved on this browser while signed out, and the prompt says anyone using the browser signed out can see them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t skips - A write waiting for the session to be restored used to wait forever if the auth provider unmounted first (for example on a country switch, which remounts it). Its waiters now fail with SessionClosedError, which says the save did not happen and to try again. - The import summary gives skips with the Public API's client_ref_conflict reason (the account already holds the item under another country) their own line instead of a bare "could not be saved". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
anth-volk
marked this pull request as ready for review
October 14, 2026 21:29
…new flagship work Brought over from main: comparing a reform with a saved reform (Build's baseline picker, the run, the report's labels and adjust panel), growth settings on provisions, the versioned report schema for stored records and run details (with the unreadable-details state), the report load error, not-found and retry states, phone layouts, the bill filters and the chart fixes. Kept from this branch: the account-backed stores. Account report rows go through the same report schema as browser records; the compared reform is saved as a link (baselineReformId), and its name comes from that reform's own record. Neither the reform nor the baseline policy carries a label. The calculator-app report routes and their tests stay removed, along with the schema helpers that only served them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A report's link is now its /v2/user-reports row ID, not the v1 report's. Whoever opens it, signed in, signed out or another user, reads the saved report through the Public API's credential-free /v2/shared-reports route and sees the owner's title, source note, provisions, budget window, comparison and validation pin over the v1 report the row points at. Only the session that ran it links the reform it was run from and pins the validation. Identical changes share one v1 report, so the v1 report's number never named one run. Where accounts exist, such a link is an old one and no longer opens. Without accounts (sign-in off, or outside the US and UK) nothing is shared, so the link stays the v1 report's number. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…audit and report re-runs Brought over from main: the v3 audit fixes (#1273, #1274, #1277, #1278, #1280, #1281, #1283, #1284, #1285), run progress from the API, a report that opens from its stored output and records a failed run, a wait for the reform policy, the report year switcher and All years view (#1292), dated edits in the adjust panel (#1294), each country's own draft, and Your reforms' read-only reform detail with a parameters table, "View results", "Re-run" and the delete prompt (#1288), with the other merged fixes (#1289, #1290, #1293, #1295, #1296). How the conflicts were resolved: - Report page: a link is still a saved report's ID, read through the credential-free shared-report route, with the v1 report's number only without accounts. The v1 report it names (the shared row's legacy_report_id) gets main's run lifecycle: stored output, recorded failures, the household guard, the wait for the reform policy (for any link that resolved), the unknown-baseline state and the year switcher. - Reforms page and ReformReports: main's read-only detail, parameters table, delete prompt, View results and Re-run, on this session's stores and session-scoped query keys. "View results", "Open" and "View full impact report" go to reportLink(report), never the v1 report's number. The detail keeps this branch's guards: a reform whose changes did not load cannot run and says so, a reform that changes nothing cannot run, and the run sends what Build would. A record without a year reads it from the API's record. - Build's run bar: still "Save and run", which saves first and runs nothing when the save fails; it now saves only the values that differ from current law, leaves a reused reform (and its name) as it is, and names a budget window's years. A new reform has nothing to save when nothing differs. - Adjust panel: main's dated edits, budget window and unknown-baseline guard on this session's reform store; recompute still waits until a value differs from current law. - draftReform: loadProvisionsIntoDraft takes both the saved reform it edits and main's window and source. - Tests: main's run-hook tests sit beside this branch's store tests; the run bar test that ran a report after a failed save is dropped, as a failed save runs nothing here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Your reforms' Re-run and "View full impact report" came from main reading a report's details by its v1 report's number, which finds nothing for an account report: its details are kept under its link (the row ID), and only in the session that ran it. A re-run of one ran this year's report against current law, and an existing report of the same values never reopened. - Re-run reads the run's details by the report's link first. Without them it takes the year from the v1 report, the budget window from the record (budgetWindowYears) and the comparison from the record's baselineReformId: the baseline policy the report ran (from its simulation), named from the saved reform. When that policy can't be read it uses the saved reform's own policy, and when neither is known it runs nothing rather than compare with current law. The new run is linked to the same saved reform. A report kept in this browser still reads its details by the v1 report's number, which is its link. - "View full impact report" reads an account report's year from the v1 report before looking for one to reopen, and counts a report linked to a compared reform as one against a saved reform. - The report list names the reform an account report was compared with from this session's saved reforms when the run's details are gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and report years Two things main now keeps in this browser outlived a signed-in session, where the next person on a shared browser would find them: - Each country's draft (#1284) is set aside under its own key when another country's is started. Signing out, a signed-out load after a session that expired, and another account signing in cleared only the draft in hand; they now clear every draft, as they did before there was more than one. - The years asked for in a report's budget window (#1292) are kept in this browser, keyed by the report's link. A saved report's link opens the report for anyone, so those years now stay in memory for the page session instead; a link without accounts keeps them as main does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… main's naming and reform badges main gained #1297 to #1306 (name before run, the run bar and window in the sidebar, library status, district details, calibration notes, household add button, scorecard notes, merge leftovers). Three files conflicted: - DraftHeader: kept this branch's save-then-run mutation in place of main's runAndKeep, and took main's change to it: an unnamed reform runs as UNTITLED_REFORM (draft.label.trim()). main's "Name it" box and taller years picker merged as they are. - ReportAdjustPanel: took main's Select imports for its window picker; the panel keeps this branch's useReformStore instead of getReformStore. - Reforms.page: a reform card keeps this branch's "Changes could not be loaded" eyebrow and gains main's latest-report status badge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An account report whose compared reform was deleted (the Public API sets its baseline_user_policy_id to null), or a recompute of someone else's report, has no saved-reform link. Re-run then compared with current law, 'View full impact report' could open it as the reform's own report, and Your reforms dropped 'Compared with'. The report's baseline simulation still shows the policy it ran: when that is not current law, re-run keeps it, the match skips the report, and the row reads 'Compared with a saved reform'. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
'Save and run' read the draft from the latest render when the save answered (TanStack Query v5 updates a pending mutation's options), so an edit made while saving ran values the linked saved reform did not hold. The draft and its comparison are now fixed when the button is pressed and passed as the mutation's variables. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With sign-in off, and outside the US and UK, every v3 page wrote a pe-browser-user-id it never sent. It is now made only for a signed-out session in the US or UK; otherwise the client sends no credential. Where crypto.randomUUID is missing (an insecure context, older Safari) the ID is built from crypto.getRandomValues instead of throwing during render, and a stored value the Public API would refuse is replaced. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A page that loaded signed out deleted every draft an account had left, but a restore that only failed (a timeout, a lost connection, Safari's cross-site limits) signs the account back in on the next load. The drafts are now held out of sight for that account instead: they come back when it signs in again, and go when another account signs in, on sign-out, or after seven days. A signed-out session still never sees them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
clampStoreText cut by UTF-16 code units, so a name over 255 characters with an emoji at the cut sent half of one, which the Public API refuses with 422 after the run's v1 report exists. It now counts and cuts code points, as the API counts characters. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…'s value coercion main gained #1305 (type and check policy values as v2 does), #1307 and #1308. Three files conflicted: - runReport: createReformPolicy keeps this branch's signature without a label (names stay on the user's records) and takes #1305's parameters argument and coercePeriods. RunFlagshipReportArgs and the run keep both accountReportStore and parameters. - useBillEconomy: createReformPolicy(countryId, provisions, parameters). - useRunFlagshipReport: keeps both the report store and the parameters. #1305's createReformPolicy tests drop the title argument; their assertions are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Current law reads 0 until the model's metadata loads, and the report list renders before then. Comparing a report's baseline policy with that 0 made every current-law report read 'Compared with a saved reform' until the metadata arrived, or for good if it failed. A current-law ID that is not a real policy now leaves the comparison unknown, for every caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1253
Summary
Stage 12A of the unified API migration, app side. App v3 (the flagship workspace, US and UK) gets a standard sign-in flow using Auth0 social sign-on, and every v3 reform and report is saved through the Public API: signed in to the account, signed out to this browser's unauthenticated user.
Only app v3 gets accounts. The legacy calculator is unchanged from
mainand stays browser-only. Canada, Nigeria and Israel stay browser-only too, and so does v3 with accounts off. Everything sits behindNEXT_PUBLIC_AUTH_ENABLED, which is off by default.?code&stateonce handled, so a reload does not replay a used code./v2/user-policiesrows withprovenance, each pointing at a v1 policy./v2/user-reportsrows with the v3 fields (title, source note, provisions with their growth settings, validation, the reform it was run from, the saved reform it was compared with, budget-window years), each pointing at a v1 report. The report page rebuilds its header, budget window and comparison from the row, so they survive a reload and open on any device; the comparison's name is the linked saved reform's, looked up by the Public API, so no name is stored on the report.user_id.pe-browser-user-id) with no expiry and sent asPolicyEngine-Unauthenticated-User. The Public API keeps the work under an unauthenticated user with that ID, created on its first write. The UUID is the credential for that work, so it never goes in a URL or a log.POST /v2/me/upgradewith the session and the browser's user. A first sign-in converts that user into the account (same user, so every saved reform and report stays); an existing account takes over its work. The browser then starts over with a new UUID, so the next person signed out here starts empty. A failed upgrade keeps the UUID (so the next sign-in tries again) and offers "Try again".reforms,pe-flagship-reports,pe-flagship-report-meta), as onmain; saving a reform creates its v1 policy first and keeps the policy ID. Nothing from that storage is copied into the Public API.sur-links, which only ever resolved in the browser that created them./v2/user-reportsrow ID. Whoever opens it (signed in, signed out, or another user) reads the row through the Public API's credential-free/v2/shared-reports/{id}and sees the owner's title, source note, provisions, budget window, comparison and validation pin over the v1 report the row points at./[country]/account, in the v3 shell) offers "Download my data" and "Delete my account"; on ca, ng and il it points to the US and UK calculators instead of offering sign-in. The sign-in upgrade notice is a v3 panel under the top bar, and the top bar's "More" menu sits with Build and Reforms.BASE_URLreadsNEXT_PUBLIC_API_URL/VITE_API_URL, defaulting to production, so staging builds talk to the staging API.api/{reforms,reports},calculator-app/src/db,DATABASE_URL, PGlite tests), including the edit-token ownership that landed onmainon 13–14 October (libs/flagship/ownedItems, migration0003_anonymous-ownership). v3 storage is the Public API (or the browser with accounts off), so that path is not merged.maina third time (15 October): the v3 audit (App v3: fix where the feature PRs overlap #1273, Validation: baseline comparisons for the programs the reform changes #1274, Reforms: tracker bill stages stay the same on every load #1277, Bill feed: complete stages, matching run settings and named fiscal notes #1278, Fix v3 bill budget label, reform delete, chart labels and accessibility #1280, Fix v3 report content: shared-link dates, NaN labels, people wording, downloads, phone names, district decimals #1281, Report: run and report lifecycle fixes from the v3 audit #1283, Build: fixes from the v3 audit #1284, v3 run bar: hold the time estimate inside the API's reported stage #1285), run progress from the API, a complete report opening from its stored output and a failed run recorded on its v1 report, the wait for the reform policy, the report year switcher and All years view (Report: switch a budget window report between its years #1292), dated edits in the adjust panel (Report: adjust each period of a multi-year reform; keep the window #1294), each country's own draft, and Your reforms' read-only reform detail with its parameters table, "View results", "Re-run" and delete prompt (Reforms: view or re-run a saved report; parameters as a table #1288). A report opened by its link loads the v1 report the saved report names (legacy_report_id) and gets the same stored output and run lifecycle as a link without accounts. Adapted to this PR:reportLink), never the v1 report's number.budgetWindowYears) and comparison (the row'sbaselineReformId: the baseline policy the report ran, named from the saved reform), and links the new run to the same saved reform. A report kept in this browser still reads its run details by the v1 report's number, which is its link. When the comparison's policy can't be found, nothing runs rather than compare with current law. "View full impact report" reopens an account report of the same values the same way.mainagain (15 October): comparing a reform with a saved reform (Build's baseline picker, the run, the report's labels and adjust panel), growth settings on provisions, the versioned report schema for stored records and run details (and the "details can't be read" state), report load errors with not-found and retry, phone layouts, the bill filters and the chart fixes. Account rows are read through the same report schema as browser records. The baseline picker lists this session's saved reforms (the account's or this browser's). Neither the reform nor the baseline policy carries a label. The calculator-app report routes, their tests, and the schema helpers that only served them stay removed.main(14 October): the run bar (year and budget window, population with states and congressional districts), indexing in a pop-up, the editable title; "a run keeps its reform" (reuse a saved reform with the same values), reports listed under each saved reform, in-place edits that keep dated values, a saved reform's report run in the reform's own year; the report page's renaming, population and "Edit in Build" in the adjust panel, progress bar, budget window and no-change notice. Store methods followmain's names (findMine(countryId),findByApiReportId(apiReportId)), and names sent to the Public API are clamped to its 255-character limit (notes to 10,000) so a long bill title can't fail a save.@auth0/auth0-spa-js2.27.0,@auth0/auth0-auth-js1.15.0).Environment
Auth0 (
policyengine.uk.auth0.com, custom domainauth.policyengine.orgpending DNS) has two v3 single-page apps, each with Google, Microsoft and GitHub sign-in and no password sign-up:aHhxCvUHwCwCNZekW4yRX6hiGCF9kFYS) for this branch's preview alias (policyengine-calculator-next-git-feat-v3-u-70c7ce-policy-engine.vercel.app) andhttp://localhost:3001;LZBiZVIVG3sLzX66neu2RsE7rUDBDHjX) forhttps://app.policyengine.org.Both allow
<origin>/us/callbackand<origin>/uk/callback, log out to<origin>/usand<origin>/uk, and rotate refresh tokens (30 days absolute, 7 days idle). The audience ishttps://api.policyengine.org/, now with offline access.Vercel (
policyengine-calculator-next):NEXT_PUBLIC_AUTH0_DOMAIN=auth.policyengine.org,NEXT_PUBLIC_AUTH0_CLIENT_IDandNEXT_PUBLIC_AUTH0_AUDIENCE=https://api.policyengine.org/, in Production and in Preview for this branch only. They do nothing while sign-in is off. There is no fixed staging alias (the one custom environment,app-preview, serves another review), so this branch's preview is the staging app.NEXT_PUBLIC_AUTH_ENABLED=trueandNEXT_PUBLIC_API_URL=https://policyengine-api-staging-43j3ckhyaa-uc.a.run.appin Preview for this branch, and redeploy it. Until then the preview stays browser-only, so reviewers can use it.NEXT_PUBLIC_FLAGSHIP_SHELLis not set in Production either.)DATABASE_URL.Known limitations
noneand starts the browser over.Testing
bun install --frozen-lockfilepasses with bun 1.2.21.lint(eslint and stylelint) andtypecheckpass;prettierpasses on every file this PR touches.vitest: 4870 passed, 10 skipped, after the second merge ofmain. One test was updated formain's new adjust-panel labels.typecheckandnext buildpass, including the/[countryId]/accountand/[countryId]/callbackroutes.lint,typecheckandprettierpass,vitesthas 4889 passed, 10 skipped, and calculator-apptypecheckpasses.next buildwas not rerun.main(bun 1.4.2): applintandtypecheckpass,prettierpasses on every file the merge touched, andvitesthas 5160 passed, 10 skipped. calculator-apptypecheckandnext buildpass.sur-link no longer open, while a v1 report link with sign-in off still does; a signed-in run opens at its row's link; "Open" in Your reforms follows the row ID (or the v1 report without accounts); someone else's report shows the owner's validation pin and pins nothing; the session's own row is read by link and another user's is not;sur-not-found message and the browser-ID message;Related
/v2/shared-reports): Stage 12A: Sign-in identity and v1-anchored user associations policyengine-api#3891🤖 Generated with Claude Code