* refactor(ui): normalize dense pages to the locked design system Sweep of the info-dense surfaces against .claude/rules/design.md; no behavior changes, classNames and primitive adoption only. - Replace hand-rolled h1s with PageHeader (import, suppliers, kpi, salary/employees, skattekonto, settings layout) and drop the one double title (SalarySettingsContent under the settings h1) - Replace hand-rolled empty states with EmptyState (skattekonto, banking/api-keys/oauth/counterparty settings) and hand-rolled pulse divs with Skeleton (deadlines, report view loaders) - Remove semantic colors used as chrome: amber/emerald banners in AGIPanel and SkatteverketPanel, success/warning tints in kassaflodesanalys, arsredovisning and import become neutral surfaces with the tint kept on the icon only - Full-opacity borders everywhere (border-border/30-60, border-destructive/20-40, border-foreground/30, text-destructive/80) - Snap off-scale spacing (p-5 to p-6, p-2.5 to p-3, gap/mt-x.5 to scale values); KPI metric tiles p-6 to p-4 per the tile rule - Remove the mobile Select that duplicated the invoices status Tabs (TabsList already scrolls horizontally); single Tabs now serves both breakpoints - supplier-invoices: shared formatCurrency instead of a local formatAmount helper; skattekonto: formatDate/formatDateLong/ formatDateTime instead of raw dates and toLocaleString - arsredovisning flerarsoversikt converted to the Table primitive with right-aligned tabular-nums cells - Settings: CardTitle text-base on section cards, one heading idiom in AccountSettingsContent, h3 to h2 in CompanyProfileView Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(i18n): trim text bloat and fix an untranslated sv string - Fix invoice_credit.create_failed_fallback: sv catalog carried the English "Failed to create credit note"; now "Kunde inte skapa kreditfaktura". Translate new_user_checklist.step3_title in en - Drop descriptions that paraphrase their own title (design.md forbidden pattern): invoice_detail.credited_description, invoice_credit.original_card_description, invoice_editor customer/notes card descriptions (keys deleted from both catalogs, zero remaining usages); the transaction booking DialogDescription becomes sr-only so screen readers keep it - Trim redundant sentences from settings_salary.info_payroll_scope, settings_backup.intro, ext_cloud_backup_long_description, settings.name_description, salary_payments.open_payments_note and shorten invoice_credit.reason_card_description; statutory BFL/tax prose untouched - Normalize toast punctuation (dimensions/self_billing created_description lose the trailing period like their siblings) - common.delete "Radera" to "Ta bort" (zero live call sites; Radera stays reserved for irreversible account/company deletion) Catalogs verified key-identical (4795 keys each) and JSON-parseable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
34 KiB
Decision Log
One line per decision: [YYYY-MM-DD] <decision>: <why>. Appended by agents and humans when a non-obvious choice is made (approach picked over an alternative, dependency declined, action stopped by a CLAUDE.md rule). Read before re-litigating a past decision.
[2026-07-02] Adopted this decision log: CLAUDE.md rewritten per config-over-prompt principles; decisions persist here instead of being re-derived each session.
[2026-07-03] Prod constraint clobber (self-inflicted, repaired in ~10 min): applied pending_operations link_document_to_voucher migration from a checkout predating 20260702171000 (retag_line_dimensions): hand-copied CHECK lists clobber concurrent adds. Zero impact (no retag ops in window). Rule: before applying any expand-types migration to prod, diff the list against the LIVE prod constraint, not the local file history. Long-term fix queued in mcp_optimization_plan P0-1 follow-up (audit test now guards CI).
[2026-07-03] Archived 4 completed/superseded plans to dev_docs/archive/ (dimensions_implementation_plan, specialized-agent-plan, api_ai_architecture/PLAN, mcp-apps-architecture-reference): moved, not deleted, because dev_docs is gitignored (no git history to recover from). Live remnants relocated first: PR10 backlog → dimensions_architecture.md; eval-harness spec → claude_surface_plan.md §2.1. agent_first_vision.md §8 marked superseded by claude_surface_plan.md (Skatteverket filing is BUILT, contra its P0 item 6).
[2026-07-03] Moved this log from dev_docs/DECISIONS.md to repo root: dev_docs/ is gitignored, so the log was invisible to other developers; root matches the existing convention (CONTRIBUTING.md, SECURITY.md).
[2026-07-03] Converted the last three full-page create flows (salary run, employee, recurring schedule) to ?new=1 URL-driven modals matching the verifikat/invoice pattern (#861); old /new routes survive as redirects for bookmarks/agent intents. Moved forms keep their existing hardcoded-Swedish strings: translating them is out of scope for the modal conversion.
[2026-07-03] Momsdeklaration hard-gates on vat_registered === false (EmptyState + settings CTA), not a soft banner: onboarding Step 4 asks the question explicitly, so false is a deliberate answer rather than "unconfigured" (DB default only matters for pre-onboarding companies, which the gate copy points to settings anyway).
[2026-07-03] VAT view auto-fetches on period change and drops the "Hämta" button; fetch state is derived from a key-tagged result object instead of setLoading/setError in the effect: keeps react-hooks/set-state-in-effect ratchet at baseline (repo gate is per-rule count).
[2026-07-03] Added ReportDescriptor.standalone (only vat-declaration) to hide the report-shell back link + fiscal-year selector, instead of changing behavior for all params:'calendar' reports: periodisk-sammanstallning keeps its current shell; scoped diff.
[2026-07-03] New user-facing strings on skattekonto follow that file's existing hardcoded-Swedish convention; the deadlines callout uses next-intl (page already translated). Year-end stays Swedish per .claude/rules/i18n.md.
[2026-07-05] Salary run "Ångra godkännande" transitions approved → review (not straight to draft) and hard-deletes generated-but-unfiled AGI declarations — symmetric with the approve step for a clean audit trail, and stale AGI XML must not stay exportable. Blocked with 409 once the AGI is pending_signature/submitted/accepted: the lawful path is then a correction AGI with the same specifikationsnummer. Payment-file tracking is cleared; whether the file reached the bank is outside app knowledge, so the UI confirm makes the user own that check.
[2026-07-05] PR #894 bot triage: accepted the delete-after-update reorder (destructive op last) and the manual-filing warning in confirm_unapprove_agi; declined soft-cancel status for unfiled AGI drafts and preserving approved_by on recall — a never-filed generated AGI is regenerable working data derived entirely from retained run data (not räkenskapsinformation; unapprove 409s once anything is filed), and the approval with legal weight is the one in force at booking, which unapprove can never touch (paid/booked runs are locked out).
[2026-07-05] Fixed supplier-invoice VAT silently dropped via MCP inbox conversion: gnubok_create_supplier_invoice_from_inbox now derives vat_amount from summed lineItems instead of the unreconciled OCR totals.vat field, and createSupplierInvoiceRegistrationEntry/CashEntry/PrivatelyPaidEntry gate the 2641 posting on itemsHaveVat(items) instead of invoice.vat_amount > 0. Chose to fix both the immediate source (server.ts) and the downstream gate (supplier-invoice-entries.ts) rather than just one: the header field is inherently a redundant, independently-sourced aggregate that can drift again from a different call site in the future, so the engine itself should never trust it as a gate.
[2026-07-06] Migration 20260706100000 adds profiles.deleted_at/anonymized_at (ADD COLUMN IF NOT EXISTS) alongside committing anonymize_user_account verbatim: the prod function writes those columns but no repo migration ever created them, so without the columns the drift capture would ship a function that fails on every from-scratch database (CI replay, self-hosted). No-op on prod.
[2026-07-06] v1 reconciliation run: confidence_threshold has NO server-side default when omitted (existing API consumers keep current behavior; only the unattended enable-banking sync callers pass DEFAULT_UNATTENDED_CONFIDENCE_THRESHOLD=0.9); registry pitfalls recommend 0.9 to integrators. Revisit if telemetry shows API callers auto-applying fuzzy matches.
[2026-07-06] Salary surfaces (payslip PDF x2, payslip email, AGI, KU10, BG/LB + SEPA payment files) now resolve the employer name via getCompanyDisplayName (company_settings.company_name, fallback companies.name), matching invoices. Chose read-side coalesce (Option A) over mirroring the name onto companies.name on write (Option B): companies.name is write-once at onboarding and never authoritative for these surfaces, so A needs no migration/backfill. Included the Skatteverket forms (AGI/KU10) despite the sarskilt-foretagsnamn caveat because the frozen onboarding name (e.g. a lagerbolag's Grundstenen name) is unambiguously wrong and there is no dedicated registered-name field yet; a Bolagsverket-synced legal-name field is the real long-term fix.
[2026-07-06] Sidebar company switcher: extended the same company_settings.company_name coalesce to the NON-active companies in the switcher list (the active one was already overridden with displayName in app/(dashboard)/layout.tsx). Fetched all the user's current names via a bare company_settings.select('company_id, company_name') added to the existing dashboard-layout Promise.all, relying on the company_settings SELECT RLS (company_id IN user_company_ids()) to scope it, rather than an explicit .in(companyIds) that would depend on allMemberships and add a serial round-trip on the every-dashboard-render critical path. Do not "harden" it with an explicit company_id filter: there is no single id here (it is deliberately the set of the caller's companies), and adding one reintroduces the round-trip.
[2026-07-06] Employees API 500 (ERR_CRYPTO_INVALID_AUTH_TAG "length: 6"): root cause was the v1 REST create route storing personnummer plaintext (skipped encryptPersonnummer), which then threw in every decrypt-on-read path. Fix = encrypt-on-write in v1 create + decrypt-on-read in v1 list/detail/patch (the whole v1 employee module assumed plaintext storage) + a tolerant guard in decryptPersonnummer that passes a raw 12-digit value through with a warn (chosen over per-call-site guards: one change un-breaks roster/runs/payslips/KU/AGI/MCP at once, and stays a safety net + observability against any future non-encrypting writer). Prod backfill re-encrypts the 5 existing plaintext rows (2 companies) via scripts/backfill-encrypt-personnummer.ts. Deferred: duplicate-personnummer detection is already broken for encrypted rows (unique index sits on the random-IV column so it never collides); proper fix is a deterministic HMAC hash column + migration, tracked as a follow-up, not folded into the outage fix.
[2026-07-06] Employee "Anstallda" clearing/kontonummer validation: added a shared structural validator (lib/salary/payment/bank-account.ts) wired into the create form, edit form, CreateEmployeeSchema, and the PATCH route, so a typo is caught at entry instead of at Bankgirot LB generation. Scoped to structure (4-digit clearing or 5-digit Swedbank 8xxxx; 5-11 digit account; both-or-neither) to mirror encodeReceiverAccount and avoid false rejections; per-bank mod10/mod11 checksum deliberately deferred to a vetted soft-warning follow-up (needs the official clearing-range table; getting it wrong rejects valid accounts). Update validated only when a bank field actually changes so legacy free-text bank data stays editable. Bank-name lookup is a conservative major-ranges-only table returning null (never a guessed name) for unknown clearings.
[2026-07-06] Kontoplan (chart-of-accounts) load optimization: fixed a double-fetch (the load effect depended on hideK2Excluded, which the effect itself set, re-running every fetch on each visit), deferred the BAS catalog + K2 setting to first "BAS-katalog" tab open, and moved usage counts off the first-paint critical path. Slimmed /api/bookkeeping/accounts/reference to return only the company's activation rows and merge against the client-bundled BAS_REFERENCE, instead of re-sending the full ~1,300-account catalog (~400KB) every load. For the slow get_account_usage_counts aggregate (prod worst case ~440ms, ~58k heap-buffer hits) added a covering index on journal_entry_lines (journal_entry_id, account_number) so the inner join becomes an index-only scan (verified on staging: node flips to Index Only Scan, 111 heap fetches). Chose the covering index over denormalizing company_id onto journal_entry_lines + (company_id, account_number) index: the latter would need the commit_journal_entry RPC write path changed (Hard Rule #2) plus a 599k-row backfill, disproportionate for a usage-count column. Migration 20260706120000 applied to staging only; prod deploy pending Emil's go.
[2026-07-06] Momsdeklaration "file without Skatteverket connection": the VAT report was never gated on the connection (it renders from bookkeeping via /api/reports/vat-declaration); users just read the not-connected "Anslut med BankID" card as a wall. Fix is communication only: added an always-visible VatManualFilingCard under the report (copy-the-rutor + skatteverket.se link) and reframed SkatteverketPanel's not-connected state to "Skicka direkt till Skatteverket (valfritt)". Put the manual card in VatDeclarationView (always rendered) rather than inside SkatteverketPanel, which returns null when the skatteverket extension is disabled, so core/self-hosted users also get manual-filing guidance. New strings kept hardcoded Swedish to match the surrounding momsdeklaration surface (VatDeclarationView + SkatteverketPanel use zero next-intl; VAT ruta labels are a "stays Swedish" surface per i18n rules) rather than adding lone en.json keys to an otherwise all-Swedish card. Rutor copy logic extracted to lib/reports/vat-manual-filing.ts (pure, unit-tested) since components aren't tested. SKATTEVERKET_MOMS_URL is a named constant flagged for Emil to confirm the exact stable e-service URL before shipping.
[2026-07-06] Recurring invoices resend: the daily cron was accidentally dropped from vercel.json in #559 (2026-05-22), so no recurring schedule has sent since. Re-registering it as an hourly cron plus a per-schedule send_hour (0-23, Europe/Stockholm, DST-aware via Intl, no dep). Cron never sends for a past date: a schedule with next_run_date < today is rolled forward without generating (protects outages + the reactivation path). Crucially, the enabling migration (20260706140000) does a ONE-TIME pause of every schedule that exists at deploy so nothing resumes emailing customers behind their back after weeks of silence: users must consciously reactivate (Resume now confirms for auto_send schedules) or click "Skapa faktura nu" (new POST /[id]/run, leaves next_run_date untouched). Chose pause-all-existing over roll-forward-and-resume on Emil's explicit call (prior incident: customers got invoices they shouldn't). No backfill of the ~6 dark weeks. Also fixed the /invoices/recurring/[id] row-click 404 by removing the dead navigation (no detail page exists; edit page deferred). Reminders cron (also dropped in #559) deliberately left for a separate task. Migration not applied to prod by me.
[2026-07-06] Recurring schedule reactivation (PATCH status -> active) rolls a stale next_run_date forward immediately and STRICTLY into the future (never today, even when today is the schedule's day_of_month), and clears last_run_warning: relying on the cron's stale-roll-forward left a past "Naesta korning" visible for up to an hour, and rolling to today would let the cron send within the hour of reactivation, colliding with the no-surprise-sends rule. Today's invoice is the explicit "Skapa faktura nu" action instead. Cron stale-roll-forward kept as the outage safety net. Verified prod has 2 active schedules (both auto_send, next_run 2026-06-01 and 2026-07-05, both stale), so the deploy race between the Vercel cron and the pause migration is harmless with current data: stale rows roll forward without sending in any ordering.
[2026-07-06] Momsdeklaration manual-filing affordance, format decision: replaced the copy-the-rutor clipboard button with a downloadable momsdeklaration PDF (new route app/api/reports/vat-declaration/pdf via withRouteContext + lib/reports/vat-declaration-pdf-template.tsx), and added PDF alongside xlsx in the report's Exportera menu. Rationale: for manual moms filing you submit NO file (you type the rutor into skatteverket.se); moms has no SRU (that's income tax) and its only machine channel is the Skatteverket API. So the export is a read/record document, PDF reads like the actual SKV 4700, and the PDF disclaimer says explicitly it is not an inlamnad deklaration. Amounts are rendered in hela kronor (Skatteverket files whole kronor, no ore): buildManualFilingRows rounds each ruta and recomputes ruta 49 from the rounded output/input rutor per the Section G formula so the document ties out; this whole-krona rounding is deliberate and NOT the ore-precision money rule (nothing here is posted). On-screen report keeps ore (regulated rendering, out of scope). New route uses withRouteContext (not the older createClient+getUser pattern of the sibling report routes) so check:guards' antipattern ratchet stays green.
[2026-07-06] "Spara som mall" on the manual bookkeeping form (JournalEntryForm): wired a save-as-template action next to the existing "Anvand mall" picker so users can capture a booking pattern at the moment they figure it out (user request). Reused the exact building blocks the invoice-inbox BookDirectlyDialog already uses (deriveTemplateLinesFromBooking + shared TemplateForm mode=create + POST /api/settings/booking-templates), so no new lib/API/DB. Two deviations from the extension dialog, both deliberate: (1) built the TemplateForm entityLabels from the settings_booking_templates i18n keys (entity_all/entity_enskild_firma/entity_aktiebolag) instead of reusing BookDirectlyDialog's hardcoded Swedish TEMPLATE_ENTITY_LABELS const, because JournalEntryForm is bilingual (journal_form namespace) whereas the inbox dialog is a Swedish-only extension surface; the four new button/dialog strings were added to both sv.json and en.json. (2) Button placed in BOTH the mobile and desktop layout rows AND in both create + edit (editEntryId) modes, mirroring where "Anvand mall" already renders, rather than gating it to fresh entries. accountNameMap derived from the form's existing catalog state (CatalogAccount) so template line labels get BAS names. No component test (repo has none); relied on the already-tested deriveTemplateLinesFromBooking. Not extracting TEMPLATE_ENTITY_LABELS to a shared const to keep the diff off BookDirectlyDialog.
[2026-07-06] Recurring schedule editing: reused NewRecurringScheduleDialog for both create and edit (POST vs PATCH, driven by ?edit=, prefilled from the already-loaded list row which carries items + send_hour, no extra fetch) rather than building a separate detail page. This is the "edit surface" deferred earlier ("Create dialog only"); clicking a row now opens the prefilled editor instead of doing nothing. PATCH additionally recomputes next_run_date to the next STRICTLY-future occurrence when day_of_month actually changes (compared against the stored value, not merely present in the payload, so editing name/items/time never moves an imminent send), mirroring the reactivation roll-forward. Existing email-gate + reactivation logic in PATCH cover the edit path unchanged. No schema change.
[2026-07-06] v1 invoice POST (#895) refactored onto buildInvoiceWriteData instead of extending the hand-rolled compute: the v1 route was silently dropping ROT/RUT, article_id, revenue_account, accrual, and line_type fields that CreateInvoiceSchema already accepted; one shared builder eliminates that drift class permanently. Wire-shape kept: VAT_RULE_VIOLATION details stay snake_case via a mapping shim.
[2026-07-06] v1 dimension value DELETE mirrors internal semantics (hard-delete unreferenced, 409 DIMENSION_VALUE_REFERENCED with archive hint otherwise) rather than DELETE=archive: identical behavior across dashboard and API beats a simpler mental model that would surprise users comparing the two surfaces. Value dates (end_date for projects) ride the existing PATCH; whole-dimension DELETE stays unsupported.
[2026-07-06] Fastigheter-on-customers (item 3 of #895) deferred to a follow-up issue instead of shipping a quick column: single-default-property vs multi-property registry changes the data model and the ROT prefill UX; needs its own design pass.
[2026-07-06] v1 articles endpoint is read-only list (GET) under invoices:read: the #895 ask is "pick articles when composing invoices via API", not article CRUD; linking article_id does not auto-fill line fields (caller copies price/VAT), matching how invoice_items freeze article data at write time.
[2026-07-06] Kept two-step potential-match fetch on /transactions instead of single PostgREST embed: prod schema cache has no FK relationship for transactions.potential_supplier_invoice_id (PGRST200; migration 20260225100248 ADD COLUMN IF NOT EXISTS likely skipped the REFERENCES clause because the column pre-existed). Revisit after adding the FK via a new migration.
[2026-07-06] Bolagsverket testbank E2E as skipped-by-default vitest (BOLAGSVERKET_TESTBANK_E2E=1): needs the IP-bound firewall opening, so it can never run in CI; GUIDE's documented test pnr 190001010106 fails Luhn, 190001010107 is the accepted one.
[2026-07-06] Paywall leak sweep gating choices: SKV unlock (DELETE /declaration/lock) left ungated so a lapsed company can recover a draft it locked while entitled; agi/kontrollera HU/IU gated (direct SKV API interaction = paid, file download stays free); recurring auto-send blocks only the email, invoice creation stays free (freeze-and-retain).
[2026-07-07] Sjalvfaktura via the public invoice API (support request: "kan inte hitta det i docs"): exposed the RECEIVED self-billing invoice (mottagen sjalvfaktura, ML 17 kap 15: a SALE, Debit 1510 / Credit 30xx+26xx) on the public API as an OPTIONAL is_self_billed flag on the existing POST /api/v1/.../invoices endpoint (+ external_invoice_number, self_billing_agreement_ref, received_date), on Emil's explicit call ("configure sjalvfaktura when creating an invoice ... optional field") over a dedicated /invoices/self-billed v1 endpoint. First built the WRONG interpretation (issue a self-invoice on a SUPPLIER's behalf = a purchase, on the supplier-invoice stack, new SJ- series + PDF + MCP tool + migration 20260706130000); reverted all of it after Emil clarified the user meant the existing seller/received feature, which already existed internally (/api/invoices/self-billed, cookie-session) but was absent from the public API. No migration for B (is_self_billed/external_invoice_number/self_billing_agreement_ref columns already exist from 20260613100000). Extracted lib/invoices/self-billed-sale.ts (resolveSelfBilledSaleDraft + createSelfBilledSaleInvoice) as the single implementation and refactored the internal route to a thin wrapper over it, so the dashboard "Sjalvfaktura" tab and the API can't drift (internal route test still green, 8/8). Fields added as PLAIN optionals (no z.superRefine) so UpdateInvoiceSchema = CreateInvoiceSchema.omit() keeps working (superRefine turns it into ZodEffects, which has no .omit); "external_invoice_number + received_date required when is_self_billed" enforced in the route instead. Documented the flag in the invoices.create registerEndpoint (description + pitfall) since the whole ask was "can't find it in docs". No git touched; nothing deployed.
[2026-07-07] Compliance-review sweep on add/api-and-invoice. FIXED: (1) recurring cron double-send window: replaced the read-only "already ran today" check with an atomic compare-and-set claim on last_run_at (release-on-failure) so two overlapping hourly invocations can't both spawn from the same stale batch row; (2) recurring schedule edit dialog could PATCH auto_send=true for a customer with no email (disabled-but-checked box, async customer load after defaultValues): added a useEffect that forces auto_send=false whenever the effective customer lacks an email, mirroring the manual-select guard; (3) momsdeklaration manual-filing rows: switched Math.round -> Math.trunc (öretal faller bort per SFL 22 kap 1 §; in-repo swedish-sru-filing skill confirms öre are DROPPED not rounded, and this now matches the SRU income-tax path). This narrows the earlier [2026-07-06] "whole-krona rounding is deliberate" decision: whole-krona stays deliberate, but the öre handling is truncation, not round-to-nearest. DECLINED (with rationale): self-billed "Självfakturering" notation + own-voucher-number findings are misdirected: createSelfBilledSaleInvoice books a RECEIVED självfaktura (the counterparty issues the document, we render no PDF), and numberOverride only sets the human-readable verifikat description/tag, not the sequential verifikationsnummer (still assigned atomically by commit_journal_entry). Bank-account mod11 padStart padding is correct: leading zeros contribute 0 and preserve the right-aligned check-digit weighting (already tested vs a real Forex account). AGI/KU10 employer name kept as [2026-07-06] (särskilt-företagsnamn caveat already accepted). Livsmedel 12%->6% (April 2026) reactivation guard declined as a description-keyword food-detector: violates the determinism/never-guess principle, and the one-time pause-all-on-deploy already forces conscious reactivation as the natural checkpoint.
[2026-07-07] bank_file_imports dedup key widened (user_id,file_hash) -> (company_id,file_hash), migration 20260707130000 applied to prod: the old key made a same-user re-import of the same file into a second company resolve the upsert onto the first company's row, which RLS rightly blocked (42501). Mirrors what 20260330130000 did for sie_imports; v1 route's BANK_IMPORT_DUPLICATE_OTHER_COMPANY pre-check removed as obsolete (structured-errors entry kept for API compat).
[2026-07-07] A1 route-auth campaign migrated 118/119 routes off hand-rolled supabase.auth.getUser() onto withRouteContext (enforces MFA/AAL2); ratchet baseline lowered 119->1.
[2026-07-07] mcp-oauth/authorize left on the raw-auth baseline (count 1, not 0): it renders an HTML consent page and issues 303 redirects, which withRouteContext (JSON envelopes + company-context gate) cannot express; MFA is enforced instead via a route-local requireAal2() step-up (AAL1 sessions redirect to /mfa/verify) since consent mints a long-lived API key that bypasses MFA thereafter.
[2026-07-07] Added { requireWrite: true } to POST /api/reports/vat-declaration/rc-basis-gaps/fix: it calls correctEntry() (storno of a posted entry) and was reachable by viewer-role members.
[2026-07-07] Two GET routes kept requireWrite (salary/runs payment bg-lb/pain001, skatteverket payment-file): they persist a file_generated_at stamp and previously gated viewers, so dropping the gate would regress write-protection.
[2026-07-07] Ledger-context as MCP resource, compute-on-read, SECURITY INVOKER RPC: rejected new tool (description budget), cron regen (wasteful), LLM narrative v1 (calculators principle); cache only when measured slow. See dev_docs/ledger_context_resource.md
[2026-07-07] Ledger-context research (openwiki-grounded, verified): digest-in-tool is load-bearing (claude.ai connector supports ONLY tool calls, resources unsupported); confidence must be count-grounded not model-authored (arXiv 2410.09724); prereqs before quality work: merchant-name normalization (splinter bug, #1 unlock), supplier-invoice CTE, storno filter, pending_operations feedback FK, eval harness. Full: dev_docs/ledger_context_resource.md Findings section.
[2026-07-07] Reconciled ledger-context prereqs INTO dev_docs/bank_transaction_ai_normalization.md (§14): plan is the strategic superset; ledger-context RPC gets interim normalizeCounterpartyName() now, re-keys to entity_id at Phase 2/Layer F. Closed 4 gaps: RPC in Layer F substrate list, supplier-side digest patterns, storno/correction exclusion (§13+§14), pending_operations audit+FK for agent-suggestion attribution.
[2026-07-08] Ledger-context prereq trifecta folded into the P1 branch pre-merge (normalize_counterparty_key SQL mirror of normalizeCounterpartyName + supplier_patterns CTE + storno filter + evidence{seen,agree,share,last_booked} format) instead of follow-up PRs: shipping first then fixing would break the payload shape consumers had just learned. Storno filter deliberately asymmetric: account_usage excludes source_type='storno' only; counterparty CTE has NO source_type filter because correctEntry() relinks transactions.journal_entry_id to the correction (the join self-heals) and excluding 'correction' would drop exactly the human-corrected booking. Faithful-mirror discipline: bare "KORT " prefix is NOT stripped (TS doesn't either); hardening the prefix list must change the TS+SQL pair together (pg test pins this). Payload caps trimmed 20/20 -> 15/15 + supplier 10 to hold the 12 KB budget with evidence objects.
[2026-07-08] Ledger-context dominant-contra VAT bug, found by the switch-on check (calling gnubok_get_agent_briefing on real prod data, not synthetic tests): counterparty patterns for foreign SaaS (Google/ngrok/Supabase) showed dominant_account 2614 (reverse-charge output VAT) instead of 5420 (software expense). Cause: the dominant_account CTE excluded only 19xx, so on a reverse-charge booking (expense + 2645 + 2614 + 1930) the three non-bank accounts tie and the account_number ASC tiebreak picks the low VAT number 2614. Fix (migration 20260708110000): also exclude 26xx (always moms in BAS, never characterizes a counterparty); 23xx/24xx/25xx/27xx stay eligible so loan/tax counterparties (e.g. ALMI) still surface their real account. supplier_patterns unaffected (aggregates supplier_invoice_items.account_number = expense only). Regression pg test asserts 5420 over 2614; verified it fails on the old function.
[2026-07-08] Bedrock prod outage + Docker build failure both root-caused to dependabot #884 (a1fad319, 2026-07-06) bumping @anthropic-ai/bedrock-sdk 0.29.1->0.32.0. Runtime: 0.32.0 streaming returns an empty event stream ("request ended without sending any chunks", no HTTP status) - proven NOT a creds/region issue (prod diagnostic logged AKIA key + eu-west-1). Two prior sessions mis-diagnosed it as an AWS env collision and shipped/reverted #937 (BEDROCK_AWS_* rename) with no effect. "Works locally, fails on prod/CI" because local node_modules was stale at 0.29.1 while prod/Docker build fresh from the lockfile (0.32.0). Fix: pin back to ^0.29.1 + regenerate lockfile. FOLLOW-UP: add a dependabot ignore/exact-pin so it does not re-bump to 0.32.x and re-break both.
[2026-07-08] One reconciliation PR adopts 3 prod-orphaned migrations (20260707113729 enrichment + 20260708120000/130000 ledger-stats RPCs) plus their pg-tests/fixtures onto main, instead of waiting on #927+#935 to merge: prod ledger was 3 versions ahead of the repo, leaving the default Supabase branch MIGRATIONS_FAILED and blocking every preview branch from being created. SQL committed byte-identical under the exact apply-time versions -> no-op on prod (idempotent), clean on fresh replays, and a no-op on #927/#935's next rebase. Carries #935's DB layer only (migrations + pg-tests + fixtures), not its UI/lib/i18n. Root anti-pattern: all three applied to prod via MCP apply_migration without committing the file (CLAUDE.md "never leave the remote DB ahead of the repo").
[2026-07-08] Pinned @anthropic-ai/bedrock-sdk to exact 0.29.1 (dependabot #884 auto-bumped it to 0.32.0, which broke Bedrock streaming in prod: empty stream / "request ended without sending any chunks"). Guarded three ways against accidental re-bump: exact pin in package.json, dependabot ignore, and a pinned-dep check in scripts/checks/no-new-antipatterns.mjs (check:guards). Unpin only once 0.32.x streaming is verified against Bedrock.
[2026-07-09] Issue #916 (disconnect orphans ledger accounts): release claims by demoting cash_accounts rows to manual (bank_connection_id = null), never deleting: transactions.cash_account_id and ledger history reference the rows, and upsertFromPsd2 promotes a manual holder in place on reconnect so the bank lands back on its original BAS slot. Orphans predating the fix self-heal via a revoked-status filter in the allocator + collision guard (not data repair). When a promote collides with a duplicate row for the same connection+uid (callback mirrored onto an overflow slot pre-fix), the duplicate is deleted only if it has zero linked transactions, otherwise demoted: preserves FK links while freeing the slot. Picker-save rejections now render inline in the picker instead of routing to the sync-progress modal, whose parent-unmount-on-close made every save outcome invisible.
[2026-07-09] #917 fix scoped to the current-year suggestion: "Sedan räkenskapsårets början" now resolves from the fiscal_periods row containing today, but the "Föregående räkenskapsårets start" custom option still derives from the recurring fiscal_year_start_month: the issue only covers the current-year date and a first-year company has no previous period row to resolve against.
[2026-07-09] Issue #919 (duplicate guard should steer to matching): the match action lives INSIDE DuplicateBookingDialog (fetch to /api/reconciliation/bank/link + account resolution via /api/cash-accounts + resolveAccount, exactly the MatchVoucherDialog path) rather than in each call site or a new endpoint: both call sites (transactions page runCategorize + TransactionBookingDialog/JournalEntryForm) share one implementation and pass only the transaction context + an onMatched callback mirroring onLinked. Match is primary ONLY for ledger-only candidates (transaction_id null, the SIE-import case); sibling-transaction candidates keep "Bokför ändå" primary since N:1 matching is the edge case. No lib change: the candidate already carries the transaction_id discriminator, covered by existing tests.
[2026-07-09] Demo/sandbox users could reach Stripe: an anonymous user on a sandbox company hit POST /api/billing/checkout and created a live Stripe customer (no subscription = no charge; exact tenant/customer IDs kept out of source control, see the incident PR). Root cause: neither billing/checkout nor billing/portal checked is_anonymous or is_sandbox, and withRouteContext lets anonymous users through (they are authenticated, just anonymously). Fix guards BOTH conditions in both routes (is_anonymous is the identity truth; guardSandbox matches the existing lib/sandbox/guard.ts "never charge a token" doctrine), belt-and-suspenders since anon and sandbox happen to co-occur today but are orthogonal. Anon check runs first (in-memory, no DB round trip). Also surfaced isDemo on GET /api/billing/status so the client hides the upgrade CTA instead of showing a button that 403s. Blast radius = exactly one company (no other sandbox/anon tenant had a stripe_customer_id). Left the stray company_subscriptions row + orphan Stripe customer for manual cleanup (prod write / external destructive action, not done unilaterally).
[2026-07-09] UI consistency pass: skipped the suppliers/page.tsx card-grid to Table conversion (did PageHeader + space-y-8 only): the suppliers.* i18n namespace has no column-header keys (nothing equivalent to customers.col_name/col_type), the pass forbade new i18n keys, and a data table whose primary name column has no header is worse than the existing card grid.
[2026-07-09] supplier-invoices/[id]: replaced the local formatAmount with shared formatCurrency at all direct render sites, but the two i18n-templated amounts (amount_registered_description embeds "kr", remaining_to_pay embeds "{currency}") now use the shared bare-number formatAmount from lib/utils instead: passing formatCurrency output there would double-print the currency, and message files were off-limits.
[2026-07-09] common.delete changed "Radera" to "Ta bort": grep proved the key has zero live call sites (every delete dialog uses feature-namespace keys), so this only affects future uses; convention going forward is Ta bort = detach/remove, Radera = irreversible destruction (kept in AccountDangerZone/CompanyDangerZone keys).
[2026-07-09] InvoiceEditor customer-card description kept only for the self-billing branch (issuer_card_description adds real info: who issues the invoice); the plain-invoice branch dropped its description as a title paraphrase per design.md forbidden patterns.
[2026-07-09] SalaryCalendar absence-type rainbow palette (red/amber/emerald/blue/indigo pills) left as-is in the UI consistency pass: those colors encode absence categories (data), not status chrome, and swapping them for the 3 semantic tokens would collapse 5 distinguishable categories; needs a proper categorical-palette decision instead of a mechanical fix.