c0ecf2fa3bebd46bdfd0169efd73b89653d1dfed
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
f266c386f3 |
chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers (#2150)
* chore: repo-wide bloat sweep, remove dead code and fold duplicate helpers Remove 33 dead files, ~270 unreferenced exports/types, 13 dead i18n namespaces and 4 unused dependencies; fold byte-identical helper copies into one canonical home each (lib/utils chunk/sleep/utcDateStamp, lib/dates/iso, lib/invariants/uuid, lib/xml/escape, lib/reports/sru/format, lib/pdf/number-text, lib/browser/panel-request, lib/api/v1/body + v1ValidationError rolled out to ~55 v1 routes, booking-template schemas). No behaviour change: v1 bodies and status codes, MCP tool schemas, DB writes and money math are untouched. Naive ore rounding was deliberately not swapped for roundOre; see DECISIONS.md 2026-09-02 for the full list of things left alone on purpose. tsc, lint, 19588 unit tests and check:guards green; antipattern baseline ratcheted (naive-ore-round 622 -> 620, hand-rolled-invariant 115 -> 113). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> * test(transactions): import RawTransaction from @/types after the ingest re-export removal CI's type ratchet (check:types, full tsconfig) caught the one test file that still imported the type through lib/transactions/ingest. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> --------- Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> |
||
|
|
1a41119682 |
perf(bundle): drop the BAS chart and the Node crypto polyfill from the shared client baseline (#1942)
* perf(bundle): drop the BAS chart and the Node crypto polyfill from the shared client baseline
Two chunks rode along in the first-load JS of almost every dashboard route:
the full BAS 2026 chart (315 KB uncompressed, in 81 route manifests) and
the browser polyfill for Node's crypto/vm/Buffer (327 KB, in 26 routes
incl. login and register). Neither was needed on first paint; both got
there through static imports of helpers that happen to live next to code
that needs the data or the builtin.
Node polyfill (4 pure splits, behaviour unchanged, re-exported from the
original modules for server callers):
- lib/auth/bankid-flags.ts: isBankIdEnabled (login, register, security
settings imported it from bankid.ts, which imports crypto).
- lib/import/bank-file/formats.ts: the format registry + detection (the
import history imported getFormat from parser.ts, which hashes).
- lib/salary/personnummer-format.ts: parsing/validation/formatting (the
employee forms reached the encrypting personnummer.ts via tax-column).
- lib/auth/api-key-scopes.ts: scope catalogue, groups, tool map, helpers
(the API key panel imported STAGING_SCOPES from the key generator).
BAS chart:
- lib/bookkeeping/bas-lazy.ts + use-bas-reference.ts: the chart becomes a
dynamic import, fetched once per session after first paint; components
that show BAS names/descriptions call useBasReference() and re-render
when it lands. Until then (and on the server) only the hardcoded
account-descriptions answer, so SSR and hydration agree.
- lib/bookkeeping/bas-labels.ts: class/group labels out of bas-reference.ts
(account-descriptions needed a label and paid for the whole chart).
- lib/bookkeeping/bas-account-numbers.ts (generated, ~11 KB) +
scripts/generate-bas-account-numbers.ts (--check) + parity test:
isStandardBASAccountNumber for AddAccountDialog/ChartOfAccountsManager.
- lib/bookkeeping/account-classifier-{heuristic,client}.ts: the BAS-aligned
heuristic shared by the server classifier and a client variant that uses
the lazy chart.
- lib/bookkeeping/invoice-accounts.ts: INVOICE_FX_RATE_MISSING,
InvoiceFxRateMissingError, getRevenueAccount, getOutputVatAccount out of
invoice-entries.ts, whose engine import pulled account-backfill and the
chart into SendInvoiceDialog/PaymentBookingDialog.
- CorrectOpeningBalanceDialog re-seeds names when the chart lands;
OpeningBalanceRowEditor builds its Fuse indexes lazily; the
ChartOfAccountsManager BAS-katalog tab awaits the chunk.
Tooling:
- scripts/perf/client-import-closure.mjs: static import closure of every
'use client' module with the shortest chain to a target (file or bare
specifier); found every path above without a build.
- scripts/checks/client-node-builtin.mjs wired into check:guards: a client
module reaching a Node builtin is a hard failure (0 today).
Left as is: invoices/[id], its credit page and SendInvoiceDialog still
reach the chart through lib/invoices/issue-credit-note -> invoice-entries
-> engine -> account-backfill; splitting the engine is out of scope here.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(perf): unambiguous import-edge regex in the closure walker (CodeQL js/redos)
One quantifier per span: a greedy [^'"]* up to the specifier quote, which it
cannot cross, so a run of whitespace has a single parse. Same edges as
before (multi-line named imports, re-exports, side-effect imports; type-only
imports still skipped).
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>
|
||
|
|
9686b54b41 |
refactor(design): lock the border-radius ladder, one radius per role (#1607)
Seven radii were in circulation (4/5/6/8/12/16px + pill) with no rule for which went where; one toolbar row on /transactions mixed four shape languages. This locks a 4-tier ladder (design.md convention 16): - pill: interactive toolbar controls (buttons, chips, pickers, segmented controls, toolbar search, count nubs) - rounded-xl (12px): overlay tier: page panel, dialogs, slide-overs - rounded-lg (8px): cards, form fields, popover/menu content, boxes - rounded-sm (4px): nested leaves (menu items, checkboxes, kbd/code nubs) Changes: - New SegmentedControl primitive (pill-in-pill tablist, h-8) replaces the hand-rolled bg-muted/70 tablist copied across 11 files - New ToolbarSearch primitive (pill, h-8) adopted on 9 page toolbars; dialog/picker searches keep the rounded-lg Input - dialog.tsx 8px -> 12px, matching SettingsModal/slide-over/CommandPalette - ContextPicker chips at the shared h-8 toolbar height - ~300 rounded-md / bare rounded call sites remapped by role; auth icon tiles and the mobile nav sheet come down from 16px to 12px - rounded-md, bare rounded, rounded-2xl and rounded-[Npx] are dead vocabulary, enforced by a new off-ladder-radius check in check:guards Verified: lint 0 errors, 14422 unit tests pass, check:guards green, tsc clean on all changed files, sandbox screenshots of transactions/ bookkeeping/granskning toolbars and the Ny verifikation dialog. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
02a5d10538 |
refactor(ui): migrate remaining inline pages to the concept design language (#1470)
* refactor(ui): migrate remaining inline pages to the concept design language Catch-up pass for surfaces the 2026-07 UI migration missed: - Bankavstämning: de-boxed toolbar, dry-table sections (preview, omatchade verifikationer, ignorerade, matchade), instructional copy moved behind the page "?" (HelpPopover via FocusedReport, sv+en), AttnLine for the dirty- dates hint, EmptyState for the blank page, space-y-8 rhythm. - Report detail views (trial balance, income statement, balance sheet, resultat-/balansrapport, reskontror, huvudbok, grundbok, dimension-P&L): shared Skeleton/Error/EmptyState shells, border-2 totals bands flattened to hairline cards with font-display tabular-nums headline numbers, ReportSectionTable rebuilt on the group-band idiom, font-mono money -> tabular-nums, house tablist for Förenklad/Detaljerad, GL filter de-boxed onto Input primitives, verdicts follow chips-mark-exceptions. - Extensions browse: PageHeader, locked section headers, rounded-lg secondary icon tiles, flat hover shift on cards, p-6 content. - Återkommande fakturor: page-level list moved off ui/table onto dry-table with hover-revealed quiet row actions; Skeleton loading. - Help: EmptyState for no search hits, flat hover shift on resource links. - Chart of accounts: spinner loading blocks -> Skeleton rows. - Kunskap graph + salary calendar popovers: rounded-lg, Input/Textarea primitives instead of hand-rolled shadow-sm controls. No logic, endpoint, or data changes. Verified via sandbox screenshots; lint 0 errors, 13214 tests green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): review triage: skip empty industry sectors, keyboard path to schedule edit 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> |
||
|
|
f24b26a139 |
fix: similar-sweep currency remediation, security hardening and v1 API fixes (#1215)
* fix(security): gate replace_sie_import behind owner/admin membership The RPC was SECURITY DEFINER with EXECUTE granted to PUBLIC and anon, no company_members lookup, no auth.uid() reference and no unauthorized raise, while setting gnubok.allow_delete to disarm the BFL immutability and retention triggers. Any caller holding a company_id and an import id could hard delete another tenant's verifikationer. Confirmed live in production. Applies the same fail closed owner/admin guard that undo_sie_import already carries (migration 20260624120000), resolving the actor from COALESCE(p_user_id, auth.uid()) so it denies when the role is NULL, then revokes EXECUTE from PUBLIC and anon. search_path and the raised statement_timeout are restated, since CREATE OR REPLACE drops settings that are not repeated. userId is a required parameter on replaceSIEImport: the service client has a NULL auth.uid(), so a caller without an explicit actor now fails to compile rather than hitting the closed gate at runtime. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): validate arcim OAuth callback state server side The callback route is skipAuth and decoded the state parameter as plain base64url JSON, trusting consentId and provider from it. A one time code was minted at flow start and never read. An unauthenticated attacker who learned a consent id could run an OAuth flow on their own provider account and post the callback with a forged state, landing their tokens on another tenant's consent, so the victim's next migration imported the attacker's ledger. State is now an opaque randomBytes(32) pointer to a provider_otc row, consumed by a single atomic UPDATE guarded on used_at IS NULL and expires_at, so a replay loses the row lock race and updates nothing. provider is read from provider_consents rather than trusted from the client. provider_otc already existed for exactly this purpose and was never wired up. Also scopes getConsent to an owning company, closing a cross tenant status oracle where the preview and migrate paths echoed a consent's status before the scoped check ran. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): scope documents storage to company_id (phase A) The documents bucket policies matched on auth.uid(), and upload keys were documents/{userId}/..., so company membership was never consulted. Removing a member revoked nothing: their session still authenticated and they kept direct Storage read access to every receipt, supplier invoice and bank statement they had uploaded. The same bug was fixed for sie-files in 20260416120000; this bucket was left behind. Phase A is additive. Company scoped policies are added alongside the uploader scoped ones, uploads move to documents/{companyId}/{userId}/..., and reads accept either layout so nothing breaks mid migration. Phase C, which drops the old policies, is gated on the backfill reporting zero remaining legacy prefix objects. The policy compares the company segment as text rather than casting to uuid the way sie-files does: this bucket holds keys whose second segment is not a uuid (MCP audit packages), and Postgres does not guarantee the bucket prefix qual runs before the cast, so a planner reordering would raise 22P02 and fail the whole query instead of filtering the row out. deleteDocument now removes both candidate keys. Removing only the stored pointer would leave a readable orphan copy of a document the user asked to erase. The backfill script is included but has never been run. It defaults to dry run, refuses .env.local by name, and verifies each copy is readable and SHA-256 identical before repointing the row. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(security): enforce events:read scope and membership on /api/events This was the only one of the three validateApiKey call sites with no downstream guard: v1 and the MCP server both check scope and re-verify company membership, this route did neither. An events:read scope existed and was documented as gating the endpoint but was never called, so a legacy key falling back to DEFAULT_SCOPES read the full log. The bound company id went straight from the api_keys row into a service role query, so a key whose user had been removed from the company kept reading. Adds the scope check before any database access, re-verifies company_members with archived_at IS NULL, honours test mode by stamping X-Gnubok-Mode instead of ignoring it, applies minimisePayload so the pull surface can never return a wider payload than the push surface, and replaces the three flat error strings with the canonical envelope. Test key reads are served rather than blocked: TEST_KEY_WRITE_BLOCKED is gated on mutations in with-api-v1, so a read gets the same treatment as every other v1 read endpoint. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * perf(bookkeeping): sweep remaining journal_entries!inner embeds A previous refactor removed this pattern from lib/reports and introduced fetchEntryLines, but the class was never swept. Seventeen sites remained and had become the top application consumer of production database time: measured across the resulting query shapes, 32,694 calls and 25,848 seconds of execution, mean 790ms, with shapes averaging 2.6s and 3.0s and maxing at 7,962ms against the 8s statement_timeout, which surfaced to users as 500s on the booking path. PostgREST compiles an embed with filters on the embedded side into a correlated INNER JOIN LATERAL with a parameterized LIMIT, which stops Postgres reordering the join, so each query walked the whole journal_entry_lines table across all tenants. Driving from the entries side instead turns that into two indexed round trips. Converted sites keep their existing shape: the helper reattaches the parent entry under the same key the embed produced. Several conversions also remove a latent silent truncation where an unpaginated query was capped at PostgREST's 1000 row ceiling. Two deliberate exceptions. The free text ilike legs of the MCP display query stay on the embed, because each is capped at legLimit and that cap drives the truncation contract the tool reports, while the helper is unbounded. The accounts route moves to the existing get_account_usage_counts RPC instead, since its embed was a head count and the helper returns rows. commitEntry's write path is untouched: the change there is confined to the read query of the pre-commit dimension rule check. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): anchor v1 list cursors on created_at Page two returned page one, forever, while still advertising a fresh next_cursor. The three routes sorted by and encoded a Postgres date column, which serializes as YYYY-MM-DD, but decodeDefaultCursor validates the cursor timestamp as full ISO-8601 and returned null, so the keyset filter was never applied and has_more never went false. An integrator syncing verifikat looped on the newest rows indefinitely. The transactions route already solved this and its comment names the trap; the fix was never ported. All three now order and encode on created_at with an id tie break, matching the transactions keyset predicate exactly. ISO_TIMESTAMP is deliberately left alone: relaxing it would silently change sort semantics on the route that currently works. Default ordering therefore moves from business date to insert order. Every business date is still on the row, and the invoices list gains date_from and date_to filters so a date range is still reachable; the other two already had them. The tests use an in-memory PostgREST that actually evaluates the filters, because the repo's pass-through mock cannot catch this class of bug: the bug is that the filter is never sent. They walk to exhaustion with a hard iteration cap, so an unterminated walk fails instead of hanging. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(api): separate dry run from commit in the idempotency hash The request hash was built from url.pathname, which excludes the query string, so a dry run and its commit hashed identically. Following the flow documented in dry-run.ts, re-issuing the request with the same Idempotency-Key returned the cached preview with Idempotent-Replayed set and wrote nothing, while reporting 200. An agent or integrator saw success for a write that never happened. dry_run is folded into the hash only when true, not as an unconditional boolean. Including it as false would change the hash of every ordinary write, and with a 24h idempotency TTL any key in flight across the deploy would fail the request_hash comparison and 409 on a legitimate retry. Both hash call sites now go through one shared helper so they cannot drift into a permanent cache miss, and dry run responses are no longer stored at all. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * ci: install the Bedrock SDK out of tree in the compliance review The Swedish accounting compliance gate had failed ten consecutive runs and so was posting nothing. With --no-package-lock npm discarded the lockfile and re-resolved the whole tree from package.json, floating @hookform/resolvers to 5.4.3, whose valibot ^1 peer conflicts with the pinned valibot 0.39.0. Installing into the parent of the checkout resolves only that one package, so an unrelated peer conflict can never take the gate down again. Node still finds it because ESM bare specifiers walk up parent node_modules; NODE_PATH would not have worked, as it is CommonJS only. --legacy-peer-deps was rejected because it masks future genuine peer conflicts and still reifies the full tree. The same step's SDK version is aligned from 0.31.0 back to the 0.29.1 that package.json and check:guards enforce after the streaming outage. That drift went unnoticed because the pin guard only inspects package.json and the lockfile, never workflow files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * build(docker): generate crontabs from vercel.json vercel.json defines 16 cron jobs; both Docker crontabs carried 9, and were byte identical to each other. Self hosted deployments therefore never sent recurring invoices, never dispatched webhooks and never cleaned up idempotency keys. tax-deadlines also ran once a year on 2 January instead of daily, and documents/verify weekly instead of daily. Extension crons are included rather than excluded. The Dockerfile copies the whole tree before building, so every extension cron route is compiled into the image regardless of the enabled preset, and each returns 200 when its extension is unconfigured, so curl -sf logs no failure. Two such entries were already present in the crontab for extensions absent from the preset, which settles the intent. documents/verify is treated as drift rather than a self hosted concession: the weekly cadence was present in the hosted crontab too, and the run is capped at 200 documents walking a nulls-first queue, so weekly drains the integrity queue seven times slower on a check that exists for BFL retention. webhooks/dispatch keeps its per minute cadence, adding 1,440 requests a day on self hosted. A gentler tick would silently stretch the first retry, since the retry ladder opens at 60 seconds. SCHEDULE_OVERRIDES is the one line place to change that. A parity test asserts the path sets match minus a documented exclusion list, and ratchets three cron routes that are currently scheduled nowhere so they are named rather than silently rotting. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * chore(observability): add a provider agnostic error sink There is no error tracking in this codebase: logs go to console and Vercel retention and nowhere else, nothing alerts on the 16 cron jobs, and seven code comments across lib, app, components and extensions asserted that Sentry captures errors when Sentry is not a dependency. The two most recent bug fixes on this repo were both discovered by customer email. This adds the sink, not a vendor. No dependency is taken: the interface has a no-op default and a registration point, so behaviour is unchanged until an adapter is registered. Releases are tagged from the build id already inlined by next.config.ts. Redaction moved out of lib/logger.ts into a leaf module that both the logger and the sink import, so there is one denylist and no path from application data to a third party can skip the personnummer regex, including direct sink calls that bypass the logger. That matters here because these logs carry personnummer and financial data. verifyCronSecret now reports its own 401s, which covers all 16 jobs without touching a route file and catches the case where CRON_SECRET is rotated without updating the scheduler and every job silently 401s forever. The threshold is one failure rather than the backup alert's three: suppressing the first occurrence is precisely how an outage stays invisible. The seven misleading comments are corrected to describe what the code actually does, including the two cases that still are not covered: the client side one, since the sink is server side, and a warn level call that is not forwarded. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix: remediate the 2026-07-26 similar-sweep findings across all surfaces Resolves the ~150-finding sweep (dev_docs/similar-sweep-2026-07-26.md) with one agent per finding; every behavioural fix carries a regression test proven to fail at HEAD. Full status, corrections to the sweep, refusals and open decisions in dev_docs/similar-sweep-2026-07-26-remediation-status.md. Structural roots closed: - resolveSekAmountOrNull(): honest SEK resolution refuses instead of booking 1:1; four duplicated toSek closures now refuse via INVOICE_FX_RATE_MISSING - ledger-line-amount.ts: journal_entry_lines.currency labels the document, not the amount; SQL pre-filter decoy proven and fixed - sparse-patch.ts: .partial() does not strip .default() in Zod 4.4.3; the exploitable salary payslip-line PATCH and KPI preferences sinks fixed - tests/schema: migration-replay phantom-column guard (13k+ refs, closed CHECK sets, onConflict targets); found 28 real defects, all fixed, all four baselines now empty - three new ratchet guards: sek-labelled-amount, cross-extension-import, ungated-extension-route Highlights: lawful VAT-rate set on all seven invoice surfaces (ML 6 kap), RC input VAT mismatch wired on web + both MCP callers, missing-underlag resource delegates to the shared RPC predicate, push-notifications consent polarity fail-closed, deadlines undo honours requested state, silent-failure and read-side-fabrication classes fixed across settings/KPI/inbox/Stripe/ Arcim/kassaflodesanalys, error-envelope stringification fixed at 10+ sites with isSwedishUserMessage extended. Also includes the parallel session's MCP invoice tools (update_invoice, recurring schedules, invoice deliveries) which share files with the sweep work and are verified green together. 13 new migrations are NOT applied anywhere; they apply via branch merge. 20260726120000 backfills 1247 supplier-invoice rows. pg tests for new DDL are written but unrun (no local Postgres). Verified: 11088 tests / 881 files green, tsc 0 non-test errors, lint 0 errors, check:guards passing, MCP payload 57475/57500. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(migrations): rename replace_sie_import migration off main's 20260726090000 version origin/main shipped 20260726090000_agent_quota_rpc_caller_guard.sql; keeping our replace_sie_import migration on the same version would abort the Supabase apply with a schema_migrations_pkey duplicate at merge time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): remediate pre-publish deep-review findings across all slices A 13-agent review of the full branch diff surfaced 1 critical, 5 high and ~45 further findings; this commit resolves them in one pass: - replace_sie_import / undo_sie_import: p_user_id honored only for service_role callers; any other caller is pinned to auth.uid() (impersonation gate bypass), authz raise errcode 42501 mapped to a Swedish 403 in the route, new caller-guard migration for undo - bulk_book_transactions refuses homogeneous non-SEK batches instead of writing foreign magnitudes into SEK ledger columns - credit-note cap trigger: company-match on credited_invoice_id, no cross-tenant figures in exception text - link_voucher RPCs resolve NULL invoice currency as SEK end to end - personal-number ciphertext CHECK split into NOT VALID + VALIDATE - same-currency foreign settlements clear 1510 at booking rate and book realized diff to 3960/7960; rate-less foreign write paths refuse - receivables revaluation covers partially_paid and outstanding amounts - period lock guard paginates candidates past the PostgREST 1000 cap - documents: service-client storage removals after authz, dual-layout reads in integrity cron and archive export, backfill delete-source sweep actually deletes with hash verification and shared-key grouping - invoice matching normalizes NULL/lowercase currencies (regression), duplicate candidates stop claiming amount matches they never ran - match-invoice aborts on any booking failure (no paid-without-verifikat) - refresh-exchange-rate reverts on concurrent booking (TOCTOU window) - KPI preferences upsert arbiter aligned to the company-scoped constraint - personnummer_last4 stripped from all salary responses incl. MCP tools - worked-hours batch restores destroyed rows on conflict and error paths - MCP: shared duplicate-claim builder (no more 'null kr'), short-circuit on tag_journal_lines overflow, auto_send schedules stage as high risk - observability sink redacts emails/IBANs/API keys and keeps redacted stacks in prod; assorted small guards (safe-return-to /@, dry_run=True, cursor helper off-by-one, OAuth state TTL 10 min, arcim saveMappings call removed) Full dispositions, deferred items and hand-verified accounting numbers are documented in the PR body and DECISIONS.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(personnummer): implement masking and encryption for personal numbers with tests * fix(review): address CI and compliance-bot findings for PR #1215 pg-real: the CI image's auth shim reads the legacy request.jwt.claim.role GUC, so both service-role simulations (runAsServiceRole and the invoice-delivery test's local helper) never satisfied auth.role() = 'service_role' and every legitimate p_user_id path failed closed; the shared helper now sets both GUC shapes plus SET LOCAL ROLE with a fail-loud sanity check, and the delivery test reuses it. The link-voucher migration had recreated both RPCs from pre-rewrite file text, reintroducing the NULL-unsafe membership pattern the null-safe-tenant-guards ratchet bans; both guards now use public.caller_is_company_member() with all currency changes preserved. Compliance bots: the customers export now emits the standard masked form instead of raw AES-256-GCM ciphertext in the Org-/personnummer column, and maskCustomerRow returns a non-round-trippable placeholder on decrypt failure instead of 500ing the list. MCP parity: gnubok_lock_period's staging pre-check now runs the exact countUnbookedInPeriod the commit path enforces (exported from period-service; local mirror deleted), and gnubok_agi_status resolves AGI state run-scoped so a correction run no longer renders as already filed. Declined with evidence: PR-Agent's opening-balances null-zeroing concern (all mergeable columns are NOT NULL with defaults per 20260713101000). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(review): address codex review findings on PR #1215 - restore 20260726140000 to its preview-recorded content and restate the NULL-safe tenant guard under 20260727130000: a recorded migration version never re-runs, so the in-place edit could not reach the preview branch - replace toFixed() with sv-SE two-decimal formatting in the ROT/RUT cap warning texts and update the pinned test expectations - drop the em dash in the fiscal-periods route comment - strip trailing whitespace in import-existing.test.ts Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * test(reports): raise timeout on real PDF render tests renderToBuffer does real @react-pdf layout work and exceeds the 5s default when the full suite saturates the CPU; tests pass in isolation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
6d9846b1e7 |
feat(settings): Fönster redesign - flat rows, ? help, dirty save bar (#1193)
* feat(settings): Fönster redesign - flat rows, help behind ?, dirty save bar Founder-approved concept (2026-07-25) applied to the whole settings surface, modal and full-page variants alike: - New primitives in components/settings/SettingsRows.tsx: section header (serif title + one-line intro), eyebrow groups, hairline label/control rows, flat inputs/selects/textareas, segmented control, animated reveal for gated settings, danger zone. - Every static explanation paragraph moved behind a "?" popover (HelpPopover) at row or group level; dynamic status stays visible. - Modal chrome: company kicker over serif title, fixed 920x680 window. - SettingsFormWrapper: save is a sticky bar that appears only when the form is dirty; collapses to zero height when clean. - All 11 sections converted (Konto, Abonnemang, Företag, Bokföring, Skatt, Löner, Fakturering, Mallar, Bank incl. Enable Banking-panel, Assistenten, API) with handlers, validation, role/entitlement/sandbox gates and i18n keys preserved; checkboxes became switches, cards dissolved into groups. - Fix: Escape with an open help popover closed the whole settings modal; it now closes the popover first. - New i18n keys: settings_intro.*, group labels, wrapper_unsaved (sv+en). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): founder feedback round 1 on the Fönster redesign - Abonnemang paying state: status and manage split into two rows so the row no longer wraps awkwardly; the included-features list now shows for paying companies too. - Logos where the counterpart has one: BankID mark on the security row and on the Koppla BankID button, Skatteverket mark on the connection rows. - Buttons are unmistakably buttons: 27 text-labeled row actions went from ghost to outline pills; icon-only actions stay quiet. - The agent-knowledge view (Regler & profil: Dina regler, Momsprofil, Konventioner) converted to the flat row language; it was the last old-style surface inside settings. Descriptions moved behind "?", rules render as hairline rows, the per-row "Regel" chip demoted to muted text. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(settings): address review-bot findings on the Fönster redesign - SettingsFormWrapper marks the form dirty on switch clicks too: Radix Switch is a button and fires no input event, so switch-only changes (f-skatt, KU, ROT/RUT, OSS...) never revealed the save bar. - i18n: the migrated hardcoded strings got keys in both locales (fiscal-period start date/range/months, security set-password trio); dates in ApiKeysPanel/OAuthClientsPanel/CalendarFeedSettings now pass the active locale to formatDateLong. - A11y: member remove/revoke buttons and the invite role select got correct accessible names; BankNameCombobox accepts aria-label wired from its row; the pinned-fact icon exposes role img. - BankIdSettings: explicit Avbryt under the QR block so a cancelled BankID flow cannot strand isLinking. - VoucherSeriesManager: clear the skeleton when no company is resolved. Verified end to end in sandbox: switch-only dirty bar, PUT /api/settings 200 for text and switch saves, persistence across hard reload. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
bfd5b42eb1 |
feat(settings): open Assistenten on Kunskap with the konteringskarta first (#1044)
The Assistenten settings hub used to open on Minne, with the konteringskarta buried two clicks away (Kunskap tab, below a second nested tab row). Now /settings/assistant opens on Kunskap and the LedgerGraph hero is the first thing on screen. - Kunskap is the default view and first tab; Minne moves to ?view=memory (old ?view=knowledge links still resolve to the default) - Drop the nested Kompetens/Minne/Regler & profil tab row inside the Kunskap view: Kompetens and Minne duplicated the top-level tabs one row above; Regler & profil now renders inline under the graph with a section header (KnowledgeTabs.tsx deleted) - Restore vertical rhythm (space-y-8) between the hero, detail section and footer, lost when the view moved into the settings tabs - Update redirects and memory deep links (/settings/agent-memory, AgentChat memory chips, FactsCard manage link) to ?view=memory - Match the loading skeleton to the new layout Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
dcd33997b7 |
feat(agent): move 'Vad din agent vet' into settings (Assistenten -> Kunskap) (#1008)
Relocates the ledger-knowledge surface off the top nav and into the assistant settings hub as a third tab (Minne / Kompetens / Kunskap), per the code's own "minne + kunskap under Assistenten" intent and the #935 flag that this was an easy call to change. Because both settings surfaces (the full-page rail and the intercepting settings modal) mount each section as a propless component via SETTINGS_SECTIONS, the knowledge data must be fetched client-side rather than passed as a server prop: - New GET /api/agent/knowledge aggregates buildLedgerContext + buildDeepEntities + buildAgentCompetence + company name (read-only, company-scoped via withRouteContext). - AgentKnowledgeView + AgentCompetenceSections converted from async server components to client components (getTranslations -> useTranslations; no other server-only usage). - New AgentKnowledgePanel client wrapper lazy-fetches the payload when the Kunskap tab opens (Radix unmounts inactive tabs), with Skeleton and error states matching the memory/skills panels. - Removed the Brain/agent-knowledge entry (and its now-unused import) from the Analys nav group. - /agent-knowledge kept as a redirect to /settings/assistant?view=knowledge so old links/bookmarks resolve. Tests: new route test (auth 401, no-company 400, happy-path aggregation). i18n: load_error_* keys added to both locales (parity kept). Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b51af3d80 |
feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) (#935)
* feat(agent): 'Vad din agent vet' page rendering the ledger context (P2) The human-facing surface for the openwiki ledger-context: a read-only page that renders the exact payload the AI agent reads (Accounted://ledger/context + the briefing digest) as a legible profile of how this company books. - Route app/(dashboard)/agent-knowledge (server component) calls the shared buildLedgerContext(supabase, companyId) directly: one payload, two renderers, no new API or data path. - Sections mirror the payload 1:1: coverage/freshness strip, counterparty patterns (monochrome confidence bars + seen/agree evidence), supplier patterns, explicit rules shown as authoritative instructions distinct from observed patterns, account usage, VAT profile, conventions. - Nav entry in the Analys group (icon Brain), ungated so it doubles as an upsell; flip requiredCapability to paywall. - Design per .claude/rules/design.md (PageHeader, Card, Table, Badge, AccountNumber BAS tooltips); sv + en strings (agentKnowledge namespace). VAT/BAS labels stay Swedish in both locales per i18n rules. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): deep entity-resolved analysis + radial graph for the knowledge page Reworks the 'Vad din agent vet' page from tables into a radial-hub graph driven by a new full-history deep analysis, per founder feedback. - fix(rpc): median_booking_lag_days now measures real posting promptness via committed_at, not entry_date (which the bank flow sets to the transaction date, giving a ~0 tautology: 151/152 on prod). migration 20260708120000. - feat(rpc): get_ledger_deep_context (migration 20260708130000): full-history, deterministic, read-side. Merges counterparties by normalize_counterparty_key (e.g. Claude = 14 bookings across 12 name variants, weekly, 9 710 kr, always 5420), mines booked verifikat for SEK spend (coalesce amount_sek), detects recurrence cadence, dominant account + share, plus supplier entities. Storno excluded, corrections kept; 19xx/26xx excluded from the dominant contra. - LedgerGraph: radial SVG (company center, accounts inner ring, payees outer ring), hover/focus reveals variants + spend + cadence + account. Keyboard focusable nodes with per-node accessible names + a screen-reader data table. - Page fetches the deep context alongside the light context; coverage strip gains tracked-payee / recurring / tracked-spend stats. sv + en strings. - 14 pg tests (light + deep) green; both RPCs applied to prod + version-matched. Reviewed by an adversarial multi-lens pass (accounting/SQL, frontend/a11y, prod-fact verification); all four verified findings fixed (SEK currency, storno-lag guard, keyboard a11y, spacing tokens). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): gentle mount fade-in for the radial map (reduced-motion safe) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): render the page for a rules-only company (empty-state edge case) isEmpty ignored explicit_rules, so a company with configured mapping rules but no posted transactions hit the 'hasn't learned anything' empty state and lost its rules section. Rules are independent of bookings. (CodeRabbit) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): show Kompetens (skills) + Fakta (memory) on the knowledge page The 'Vad din agent vet' page now shows the full picture of what the agent knows: alongside the booking map, a compact read-only view of its Kompetens (the Swedish accounting/tax knowledge atoms it ships with, grouped by tier as chips with active/dormant state) and the Fakta it remembers (top learned facts with kind + source), each linking to /settings/assistant for full management. Server-rendered via a new buildAgentCompetence() that mirrors GET /api/agent/skills + /api/agent/memory. Also renders in the no-bookings case so a new company still sees its agent's competence. sv + en strings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * refactor(agent): restructure knowledge page - hero graph + tabbed detail Declutters the page per feedback: the booking map is the always-visible hero, and the supporting detail (Kompetens · Minne · Regler & profil) moves into tabs so only one view shows at a time instead of a long card stack. Split AgentCompetenceSections into standalone CompetenceCard + FactsCard for the tabs; removed the top stat row on request. sv + en. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(agent): Reconciliation Aurora rewrite of the ledger knowledge graph Full rewrite of LedgerGraph: node area = sqrt(spend), colour = cadence, shape = supplier/counterparty, confidence = depth-of-field; on-mount descriptor-collapse animation with xN badge; cadence pulse veins; deterministic seeded layout; framer-motion only (no new deps); keyboard navigation, reduced-motion and sr-only support. Build-verified; 3-lens adversarial review findings fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(agent): sample-size-honest confidence in the ledger knowledge graph dominant_account_share was raw cnt/total, so a counterparty with a single booking rendered as '100% säkerhet': fake certainty by construction (the data_quality_master Item-C / P3 finding). New migration replaces get_ledger_deep_context with a Laplace-smoothed share (cnt+1)/(total+2) (1/1 -> 0.67, 3/3 -> 0.80) and exposes the raw evidence as dominant_account_count / dominant_account_total. The detail card now shows 'Bokförd hit i k av n fall' under the confidence bar; the existing focus buckets, stroke widths and percent labels inherit the honest value unchanged. pg-real test updated to guard the n=1 case. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |