fix(salary): one save per surface on the employee form (#1623)
* fix(salary): one save per surface on the employee form The employee edit page stacked two competing saves: the opening-balances Card ended in "Spara ingaende saldon" and the page ended in "Spara andringar" 80px below, with no visible boundary between their scopes. Worse, both self-saving panels lived INSIDE the page <form> and shadcn Button sets no default type, so every panel button (save opening balances, add/remove benefit) implicitly submitted the outer form too, firing the full employee PATCH alongside the panel's own request. Restructure so each surface owns exactly one save: - The employee <form> now closes right after the Bank card, with Avbryt + "Spara andringar" directly under the fields it actually saves. - Formaner and Ingaende saldon move below the form into a "Sparas separat" section (uppercase kicker + one-line scope hint) so the page save structurally cannot include them and their buttons can no longer leak submits into the employee form. - OpeningBalancesPanel becomes its own <form>: Enter saves the panel, and the save button enables only when its fields are actually dirty (fingerprint of loaded values, reset on successful save). - EmployeeBenefitsPanel buttons get explicit type="button". New strings in both messages/sv.json and messages/en.json. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(salary): release the loading skeleton when the balances fetch fails CodeRabbit on #1623: a rejected fetch or JSON parse skipped the setLoading(false) line, holding the skeleton forever. The load now wraps in try/finally; a failed load falls back to the empty form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
Jakob Wennberg
parent
dd4ced1f93
commit
51539b93ed
@@ -993,6 +993,7 @@ One line per decision: `[YYYY-MM-DD] <decision>: <why>`. Appended by agents and
|
||||
[2026-08-14] The onboarding branch question ("Var fanns bokföringen innan?") became its own journey step at the existing KLART station (done -> source, same station grammar as momsyn/moms under MOMSEN) instead of a sixth rail station: a 6-point rail crowds the 680px band's 150px label slots, "BOKFÖRINGEN INNAN" would sit next to the existing "BOKFÖRINGEN" station, and mode='add' (which never sees the branch question) would need an asymmetric rail. The done screen ends in a revealed Fortsätt that dispatches DONE_CONTINUE (mode='first' only, reducer-guarded).
|
||||
[2026-08-14] Migration /preview fetches ALL allowed fiscal years (dropped latestOnly): the connect step's "Hittade X konton och Y verifikationer" renders from /preview's sieStats, not /sie-data's generateImportPreview, so fixing only /sie-data would have left the founder-reported "0 verifikationer" (actual: 4153) on screen. Costs one SIE export per extra year at connect time, the same work /sie-data repeats right after; honest counts won over latency.
|
||||
[2026-08-14] /sie-data validation stays newest-file-only (not per-file, not on the merged parse): preserves exactly which datasets are accepted today, and validateSIEFile assumes single-file invariants (balance yearIndexes relative to ONE current year) that mergeParsedSIEFiles deliberately does not preserve. Older files' problems still surface per-file at import time.
|
||||
[2026-08-15] Employee form save-scope fix: moved the self-saving sections (Förmåner, Ingående saldon) OUTSIDE the page <form> under a "Sparas separat" kicker with a one-line scope hint, kept them as Cards (locked section pattern; the task-suggested hairline no-box region would deviate from every sibling section) and rejected the tab/sub-page shape (page has no tab structure, would be more invasive). This also fixes a real double-fire: shadcn Button has no default type, so the panels' save/add/delete buttons inside the form implicitly submitted the whole employee PATCH on every click; opening-balances now has its own <form> whose save enables only when dirty.
|
||||
[2026-08-14] SKV manual-verifikat deep link payload moved from URL params to single-use sessionStorage (supersedes same-day URL-params decision): compliance swarm flagged financial data in query strings landing in history/access logs/Referer (GDPR Art.5(1)(f), ISO A.8.12); URL now carries only the opaque row id.
|
||||
[2026-08-14] SKV prefill sessionStorage XSS window accepted as residual risk (ISO A.8.12 low, swarm PR #1621): script execution already implies full ledger read via authenticated APIs; a server-issued staging token adds a roundtrip, not protection. Documented in manual-verifikat-prefill.ts header.
|
||||
[2026-08-15] BankID mobile stranded-tab fix: the session id never reaches the browser; it lives in a signed `__Host-` HttpOnly cookie (extensions/general/tic/lib/bankid-flow-cookie.ts) that /poll, /complete, /link and /cancel read. Reported bug: BankID does not reliably return the user to the tab that started the flow (outside plain Safari the iOS https redirect goes to the OS, which opens a NEW tab), the session lived in per-tab sessionStorage, so the new tab rendered the start button and the completed signup was stranded. Login hid it because its completion is self-finishing and the Supabase session is a cookie every tab shares. Cookies being origin-wide is exactly what the handoff needed, so there is no client storage, no cross-tab lock and no heartbeat. TWO REJECTED ATTEMPTS, both killed by adversarial review before merge, both worth not re-deriving. (1) localStorage plus an owner/heartbeat lock: a TIC sessionId is an unauthenticated bearer credential (/bankid/poll is skipAuth and returned user.personalNumber verbatim; /bankid/complete with mode 'login' returns a tokenHash that verifyOtp turns into a session, MFA skipped via bankid_linked), so origin-wide persistent storage turned ANY same-origin XSS into a login-fixation primitive, and the design also deadlocked in the very flow it fixed (the hidden tab stopped polling, the new tab cleared the record, and the storage event never reaches a tab iOS has frozen). (2) The first cookie version: it pinned `mode` but not the USER, so on a shared browser an abandoned link flow bound the first person's personnummer to the second person's account, which is the same class of flaw that killed (1); its cookie also used a narrow Path, which a script can shadow by setting the same name at a LONGER path (RFC 6265 sends longer paths first, and a Max-Age=0 at the shorter path cannot delete it); and its "single-use" was a Set-Cookie, which does not serialise two requests that already carried the cookie. Consequences now, all deliberate: `__Host-` with Path=/ and unconditional Secure, because the prefix forbids Domain and forces Path=/, leaving exactly one possible (name, domain, path) and making both the longer-path shadow and a subdomain toss unsettable (Secure costs nothing since isBankIdEnabled() is false when NEXT_PUBLIC_SELF_HOSTED is set, so the earlier conditional-Secure only risked shipping unprotected behind a proxy that omits x-forwarded-proto); readBankIdFlow fails closed on two cookies of the name rather than picking a winner; SameSite=Lax not Strict, because the BankID return is a top-level navigation from outside the site; a `link` flow requires auth at /start and pins userId, and /link rejects a flow belonging to anyone else; single-use is a unique index (bankid_consumed_sessions, migration 20260815120000) claimed BEFORE generateLink, since serverless has no shared memory and a second magic link invalidates the first; the claim fails closed on any error other than 23505; account_exists deliberately does NOT consume, so a mistyped e-mail is correctable; the window is 300s for the order and re-issued at 900s once /poll observes completion, because the signup e-mail step is a person typing and on the old design it was bounded only by TIC's retention; /poll whitelists its response fields, so the personnummer is no longer returned to anyone, and clears the flow when TIC 404/410s or answers without a status, so a dead session cannot make BankID look unavailable for the rest of the window. The client resumes from ONE probe against /poll, never a readable hint cookie: a hint goes stale, cannot tell a live order from a dead one, and made the component poll on mount in states where nothing was in flight. `link` never auto-resumes at all, since picking one up without a click is how an abandoned flow binds to the next person. Android stays on redirect=null; no Android report motivates changing it and #194 closed that path deliberately.
|
||||
|
||||
Reference in New Issue
Block a user