Commit Graph
10 Commits
Author SHA1 Message Date
Jakob WennbergandClaude Opus 5 ee8ddb3849 fix(assistant): stop cross-user conversation access, bricked threads and lost sessions (#1209)
* fix(assistant): stop cross-user conversation access, bricked threads and lost sessions

Hotfix batch (PR1 of the assistant UI makeover, dev_docs/assistant_redesign_plan.md
section 7). No visual change; each of these is wrong today regardless of which
design lands, and three are unrecoverable per incident.

/api/agent/invoke never checked who owns a resumed conversation_id. RLS on
agent_conversations/agent_messages is company-scoped, not user-scoped
(20260517204000), so a member could post a colleague's conversation id, have
their history loaded into the prompt and read it back, while their own turns
were appended to that thread. The conversations list route filters on user_id
for exactly this reason. Also pins company and intent: resuming a thread from
another company would mix ledgers, and resuming under a different intent would
swap the tool whitelist under history the model has already seen.

A turn persists the assistant message carrying tool_use blocks before the tools
run, and their results only after the batch finishes. Dying in between (client
disconnect terminating the function, a deploy, a slow tool) left history ending
on an unanswered tool_use, which the Messages API rejects on replay: every later
turn 400s, and agent_messages is append-only for the BFL trail, so nothing could
repair it. History is now patched on read by synthesizing is_error tool_results,
leaving the stored trail untouched.

check_and_increment_agent_quota is SECURITY DEFINER in public with a
caller-chosen p_user_id, so any authenticated user could drain a colleague's
minute/day budget and lock them out of every agent endpoint. A plain REVOKE
would break the limiter (all three callers use the user's RLS client) and, as it
fails open, silently remove the spend cap: the function now refuses to act for
anyone but the caller, while service-role connections keep passing an explicit
id.

The single reject route re-read status and then wrote unguarded, so losing the
race with commit's atomic pending -> committing claim stamped `rejected` over an
operation that had already posted a verifikat, invisible to the committing-state
recovery sweep. Guarded on status like bulk-reject already is; a lost race is
now a 409.

The sheet's Escape handler listened on window with no defaultPrevented or target
check while the sheet is deliberately non-modal, so pressing Esc to dismiss the
reject-reason Select inside an approval card, the command palette or any dialog
unmounted the sheet and discarded the conversation, the streaming turn and the
un-actioned proposal. It now yields to open overlays and to focus outside the
sheet.

Verified: 9526 unit tests pass, lint clean on touched files, guards pass, and
the new pg-real test proves the quota guard against real Postgres (attacker
raises 42501, victim counters stay at 0). The four unrelated pg-real failures on
this machine reproduce identically with these changes stashed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(assistant): close anon path on the quota RPC, order the ownership check ahead of writes

Review follow-ups on the hotfix batch.

The caller guard used auth.uid() alone, which is NULL for the `anon` role just
as it is for backend roles, so an unauthenticated caller holding the public anon
key (it ships in the browser bundle) could still pick any p_user_id and drain
that user's quota. The guard now keys on the request role: anon and
authenticated may only ever spend their own quota, backend roles keep passing an
explicit id. The default PUBLIC execute grant is revoked as a second layer, with
execute granted only to authenticated and service_role. Covered by a new pg test
for the anon path.

The ownership check ran after the onboarding.intake stamp, so a request that was
about to be rejected could still write intake_completed_at. It now sits directly
after the capability gate, ahead of every side effect and ahead of the company
and profile reads, which also makes a rejected request cheaper.

The tool-result repair matched ids anywhere in the history, but the API needs
results in the message IMMEDIATELY after the tool_use. A result persisted after
an intervening turn (two turns racing on one conversation) left a shape that
still 400s. The repair is now positional, and orphaned or late-duplicate
tool_results are dropped, since an unmatched tool_result is rejected just as an
unanswered tool_use is.

The Escape guard matched the Radix popper wrapper, which stays mounted when a
popper is force-mounted; it now requires data-state="open" so a closed popper
cannot block Escape for the rest of the session.

Both new route errors are Swedish, per the user-facing error rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:48:18 +02:00
Jakob WennbergandClaude Fable 5 3e1ea29d02 fix(pending-ops): record posted ids and land failed_partial instead of clean rejected after partial commits (#842) (#1110)
Multi-step executors (match_transaction_invoice, credit_invoice) post an
irreversible voucher or persist a credit note and then run later fallible
steps. A failure there previously marked the whole op status=rejected,
hiding the posted entity and its id from operators.

- new migration 20260722134114: add failed_partial to the
  pending_operations status CHECK and treat it as terminal in both
  immutability triggers (immutable, undeletable, never re-claimable)
- PartialCommitError + ExecutorResult.partialPostedIds carry the posted
  ids; the dispatcher writes status=failed_partial with
  result_data.posted_ids and returns code=partial_commit
- instrument only the two named executors; hoist the read-only
  settlement-account resolution above the storno in the match executor
- consumer sweep: status union + query schema widened, failed_partial
  folds into the Avvisade tab with a badge and posted-ids detail line,
  bulk/reject routes and MCP tools message it explicitly, worklist and
  expiry sweep intentionally untouched (not pending work)
- tests: pg-real coverage for the new terminal semantics, dispatcher unit
  tests for both partial paths plus byte-for-byte regression guards

Fixes #842

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:33:49 +02:00
Mattsson e11f70b347 Bug/gh issues fiz (#1103)
* refactor: optimize page loading and data fetching

* fix: resolve recurring production runtime errors

* feat: add MCP company and customer updates

* fix: handle year-end tax adjustments

* feat: harden annual report compliance

* fix: expand invoice logo and font support

* fix: sanitize API route error responses

* fix: sanitize user-facing error messages

* feat: persist onboarding and tax assessment notices

* fix: reduce cloud backup audit churn

* feat: refine invoice editor layout

* fix: show saved tax adjustments in INK2

* fix: complete annual report API mappings

* docs: record operational safeguards and decisions

* fix: harden annual report review findings

* fix: adjust column span for description based on VAT registration

* New css class name
2026-07-21 23:00:15 +02:00
MattssonandClaude Opus 4.8 abe9ac9d8c Fix/attributes config (#926)
* fix(git): pin LF on generated extension registry and vitest snapshots

setup:extensions and vitest write these files with LF; with
core.autocrlf=true git expects CRLF and flags them as phantom
modifications on every dev/build run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): enforce MFA on mcp-oauth consent and gate viewer storno route

mcp-oauth/authorize renders an HTML consent page and issues 303 redirects that withRouteContext cannot express, so it kept raw getUser() and thereby skipped the AAL2 gate: a password-only (AAL1) session could approve consent that mints a long-lived, MFA-bypassing API key. Add a route-local requireAal2() step-up on GET and POST; AAL1 sessions redirect to /mfa/verify, BankID users are exempt.

Separately, POST /api/reports/vat-declaration/rc-basis-gaps/fix calls correctEntry() (storno of a posted entry) but lacked requireWrite, so viewer-role members could trigger it. Add { requireWrite: true }.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route transactions endpoints through withRouteContext

Migrate the transactions routes off hand-rolled supabase.auth.getUser() onto the MFA-enforcing withRouteContext wrapper; add requireWrite on mutating handlers (book, uncategorize, attach-document, ignore, batch-match, create-from-document). Behavior and response shapes preserved; tests updated to the wrapper mock pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route SIE import and bank reconciliation through withRouteContext

Migrate import/sie and reconciliation/bank routes onto the MFA-enforcing wrapper; requireWrite on mutations (import execute, create-accounts, mappings write verbs, link/unlink/run/mark-opening-balance). Reads (status, unmatched-entries) stay ungated. Response shapes preserved; tests added/updated to the wrapper mock pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route salary endpoints through withRouteContext

Migrate salary employees and runs routes (plus ku, payroll-config, tax-tables) onto the MFA-enforcing wrapper; requireWrite on mutations. Personnummer masking/encryption untouched; file downloads (AGI XML, payslip PDF, payment files) keep their headers. Two payment-file GETs retain requireWrite because they stamp *_file_generated_at and previously gated viewers. Tests added/updated to the wrapper mock pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route report endpoints through withRouteContext

Migrate the read-only report routes (trial balance, balansrapport, resultatrapport, income statement, ledgers, KPI, VAT declaration, salary journal, monthly breakdown, journal register, continuity check, full archive, etc.) onto the MFA-enforcing wrapper. All read-only, no requireWrite. JSON/XLSX/PDF/ZIP response bodies and headers preserved byte-for-byte; tests updated to the wrapper mock pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route invoices, skatteverket, agent and extension endpoints through withRouteContext

Migrate invoices, supplier-invoices, skatteverket tax-payments, and dynamic extension routes onto the MFA-enforcing wrapper with requireWrite on mutations. The two NDJSON streaming agent routes (invoke, onboarding/stream) use requireAuth() directly (the wrapper can't wrap a streaming response) so MFA is still enforced. skatteverket payment-file GET keeps requireWrite (stamps a generated-at field). Response shapes and file headers preserved; tests added/updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route documents, events, team and account endpoints through withRouteContext

Migrate documents, events, kpi/preferences, vat/validate, support/contact onto the MFA-enforcing wrapper with requireWrite on mutations. account/password, team/accept and team/members use requireAuth() directly (user-level or pre-membership flows with no active company context) so MFA is still enforced. events keeps its dual API-key-or-session auth. Document retention guard untouched; tests added/updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(api): route settings and pending-operations endpoints through withRouteContext

Migrate settings (api-keys, oauth-clients, booking-templates, counterparty-templates, logo, company settings) and pending-operations (commit, bulk-commit, reject, edit-before-approve) onto the MFA-enforcing wrapper with requireWrite on mutations. Credential-guarding routes keep their per-user ownership filters. Response shapes preserved; tests added/updated to the wrapper mock pattern.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(guards): ratchet raw-route-auth baseline 119->1 after A1 migration

Lock in the withRouteContext migration so the count cannot regress. The single remaining entry, mcp-oauth/authorize, is a documented exception (HTML consent + redirects, MFA enforced via route-local step-up). Record the campaign and requireWrite decisions in DECISIONS.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(vat): add eSKD momsdeklaration file export for "Deklarera via fil"

Generate the Skatteverket eSKDUpload v6.0 XML file so users can file VAT by
upload instead of typing every ruta into the form. Extract buildFiledAmounts()
as the shared whole-krona source of truth (öre truncated per SFL 22 kap 1 §) so
the XML file and the manual-filing PDF can never disagree. Adds the /eskd API
route, an XML option in the report export menu, and the upload button on the
manual-filing card. Strings in sv + en.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(vat): add 'vat_settlement' source type and update related components

* fix(booking): adjust search input layout and enable autofocus

* fix(vat): support 12-digit org numbers and adjust emission order for eSKD file

* fix(migration): add 'vat_settlement' to journal_entries.source_type CHECK

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 09:54:46 +02:00
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00
Jakob WennbergandClaude Opus 4.8 e5f1d7a916 fix(reports): count reversed entries in bank/supplier/AR reconciliation (#734)
A reversed (storno-corrected) journal entry must be summed together with
its storno + correction, exactly as the trial balance and balance sheet
already do (.in('status', ['posted','reversed'])). The reconciliation
paths used posted-only and manufactured phantom differences.

- bank-reconciliation getReconciliationStatus: gl_1930_balance now counts
  posted+reversed (so it equals the balansräkning for the account). Dropped
  the correction_adjustment subtraction and the reversed-linked-tx drop — a
  corrected/amount-corrected bank receipt now reconciles. The prior model
  broke once correctEntry began re-pointing the bank transaction to the live
  correction (the two changes were mutually inconsistent and produced a
  difference equal to the corrected amount).
- supplier-reconciliation (2440) and ar-reconciliation (1510/1513): same
  posted -> posted+reversed fix. Removes the false "Ej avstämd" gap a fully
  paid, fully corrected company shows — the books net to 0 over posted+reversed
  while a posted-only query double-counted the payment legs.

Also in this change:
- MCP gnubok_get_reconciliation_status: add account_number param (was hardcoded
  to 1930; the lib already supported per-account reconciliation).
- counterparty-templates normalizeCounterpartyName: strip trailing month /
  personal-initials tokens so "ngrok JW" / "Ngrok Mars" learn as one merchant.
- pending-operation reject 409 (route + MCP tool): clarify a resolved op was
  approved explicitly (no auto-commit path exists) instead of a bare
  "already committed".
- create_voucher: normalize description with String().trim() for consistency
  with line_description.
- migration 20260625120000: backfill stripped diacritics on ~830 companies'
  seeded chart-of-accounts names (Foretagskonto -> Företagskonto, etc.); the
  seed function was fixed for new companies in 20260516130000 but never
  backfilled. UPDATE-only and idempotent. Already applied to production.

Tests: 899 passing across the touched suites; lint + typecheck clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 20:46:41 +02:00
f53725b20a Agent v1 bundle: TIC v2 onboarding, in-app assistant gating, sidebar nav, MCP fixes (#584)
* fix(sie-import): accept tab as field separator (Bollbok exports)

The SIE 4 spec allows either space or tab between fields, but
splitSIELine() only treated space (0x20) as a separator. Bollbok
exports tab-separated lines for every record except #RAR, which
silently swallowed all #IB / #UB / #KONTO / #KTYP / #VER / #TRANS
records — imports appeared empty even though the file was well-formed.

Also adds a parser-side diagnostic that emits a warning when raw #IB
or #VER lines are present in the input but parsing produced none. The
previous silent failure is how this bug stayed hidden; the warning
gives the import preview something visible to surface next time.

Verified against two real reproducer files (Sean / Erik Hellqvist):
  erik h 2025.SE (UTF-8): 166 accounts, 66 IB, 4 UB, 11 RES, 95 vouchers, 198 TRANS.
  erik h 2026.SE (CP437): 166 accounts, 66 IB, 4 UB, 0 vouchers.
Both now parse with zero warnings/errors.

Tests:
  + 8 Bollbok-shape tab-separated fixtures (2025 + 2026 quoting variants).
  + 4 silent-failure diagnostic-warning tests.
  All 74 sie-parser tests pass; 155/155 in lib/import; 64/64 downstream callers.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(sie-import): address PR #513 review — strip #KTYP quotes, suppress redundant aggregate warning

Two non-blocking P2 findings from Greptile review on PR #513:

1. #KTYP handler stored fields[2] directly, so Bollbok 2026 exports
   (#KTYP\t1510\t"T") stored '"T"' with literal quotes instead of 'T'.
   Latent defect — accountType is unused downstream today, but my tab-
   separator fix made the quoted-value path reachable. Now routes through
   parseStringField so both Bollbok 2025 (unquoted T) and 2026 (quoted "T")
   land as 'T'.

2. The aggregate "kontrollera fältavskiljare och teckenkodning" warning
   fired alongside per-record 'error'-severity issues for malformed #IB /
   #VER records, producing a misleading hint when the parser had already
   pinpointed the structural problem. Now suppressed when an error-severity
   issue with the same tag already exists.

Test coverage:
  + accountType asserted to be 'T' (not '"T"') in both 2025 + 2026 shapes.
  + VER aggregate-warning test now uses #VER lines without { } blocks
    (silent loss, no per-record error) — the canonical case the diagnostic
    is designed for.
  + New suppression test: bare #VER produces per-record errors AND the
    aggregate warning is absent.

75/75 sie-parser tests pass; 156/156 in lib/import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wip: agent chat + composer + memory + document extraction

In-progress work on this branch beyond the SIE-import fixes:
- Specialized accountant agent (composer + intents + chat loop)
- Persistent agent_conversations/messages, agent_profiles, agent_memory
- /chat surface + /onboarding/agent + /settings/agent-memory
- document-extraction extension with status hooks
- MCP server staging refactor + new skills (atoms, bank reconciliation,
  customer onboarding, kreditfaktura)
- pending_operations rejection feedback (category + reason) + realtime
- TIC company profile cached snapshot on companies
- 17 migrations (all additive — see prior conversation analysis)

Parked while branch waits for review/merge. Migrations are already
applied to prod.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(tic): migrate company-data client from api-core v1 to Lens v2

Swaps the seven TIC company-data endpoints we call from the api-core
paths (`/datasets/companies/{companyId}/...`, `/search/companies`) to
the Lens equivalents (`/companies/{id}/...`, `/search-public/companies`).
Hard cutover; proxy pattern preserved.

Schema shifts handled inside the extension so consumers (TicWorkspace,
Step2CompanyDetails) don't need changes:

- `/companies/{id}/bank-accounts` now returns Bankgirot only — map to
  the existing `{ type, accountNumber, bic }` shape, drop terminated.
- `/companies/{id}/industries` returns a discriminated array — filter
  to `companyIndustryCodeType === 'sni2007'` to preserve v1 behavior.
- `/companies/{id}/phone-numbers` renamed the field to
  `phoneNumberFormatted` (fall back to `e164PhoneNumber`).
- `/companies/{id}/documents` replaces `/financial-report-summaries`;
  filter `type === 'annualReport'` and read nested
  `financialReportMetadata` to rebuild the legacy summary shape.
- `isCeased` is now a top-level boolean; `activityStatus` is an enum.
  Translate enum -> 'ceased' for the workspace's existing check.

BankID identity flow (id.tic.io) is untouched — separate TIC product.

Note: deploy gated on the TIC proxy being flipped to lens-api.tic.io
with an `x-api-key` Lens key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tic): expose v2 onboarding & workspace data

Adds six new Lens (v2) fetchers on top of the migration that already
landed in this branch, surfacing the data through /lookup and /profile.

New fetchers in lib/tic-client.ts:
- getFiscalYears          /companies/{id}/fiscal-years
- getAccountingPeriods    /companies/{id}/accounting-periods
- getPayrolls             /companies/{id}/payrolls
- getSignatory            /companies/{id}/signatory
- getRepresentatives      /companies/{id}/representatives
- getCompanyStatus        /companies/{id}/status

/lookup gains a fiscalYear field (current fiscal-year configuration)
so onboarding Step 2 can skip manual MM-DD entry. CompanyLookupResult
extended with optional fiscalYear; consumers without it keep working.

/profile gains five new sections on TICCompanyProfile:
- fiscalYear + fiscalYearHistory   current + deduped period list
- signatory                        firmateckning descriptions
- board + representatives          board-composition summary + active
                                   officers (positionEnd in future)
- payrolls                         payroll2 array newest-first, with
                                   deviation vs annual-report
- statuses                         current+historical status entries
                                   with red/yellow/green/neutral color

TicWorkspace renders the new data as four cards (Status, Fiscal year +
Signatory, Board + Representatives, Payroll history) plus a Badge
mapping for the traffic-light status color.

Tests: 52 -> 60 passing. Added unit tests for the new fetchers' v2
paths, fiscal-year auto-fill in /lookup, and full v2 profile coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(onboarding,agent): lean on TIC v2 to skip Steps 1 & 3 and sharpen Opus

Three small wins that unlock more of the v2 cutover. No new endpoints — the
data was already in the snapshot, just not flowing where it should.

Step 1 (entity_type) — deep-link path only:
- /lookup now returns `legalEntityType` and `registrationDate` (added to
  CompanyLookupResult).
- /onboarding/page.tsx does a server-side /lookup prefetch when
  ?org_number= is present (BankID picker path), maps "AB"/"EF" to the
  EntityType enum, and seeds Step 1's radio. Falls through silently for
  unsupported codes (HB, KB, …) and on TIC errors.
- WelcomeOnboarding hydrates ticLookup state from the server prefetch so
  Step 2's debounced client fetch and Step 3's first-year inference both
  have data on first render — no flash.

Step 3 (is_first_fiscal_year) — every path:
- deriveFirstYearDefaults() parses ticLookup.registrationDate and returns
  { isFirstFiscalYear, firstYearStart } when registered <12 months ago.
  Step 3's initialData picks it up; the user only confirms the end date.
- Settings value wins when present so existing users with a saved choice
  don't get overridden.

Composer prompt:
- redactTic allowlist was the bottleneck — it stripped beneficialOwners,
  signatory, board, representatives, payrolls, statuses, fiscalYear
  before Opus ever saw the JSON. Existing filterRedundantQuestions
  ownership logic was effectively dead because the data path was severed.
  Expanded allowlist to include those v2 sections; kept bankAccounts/
  email/phone/fiscalYearHistory/financialReports out (token cost > signal).
- SYSTEM_PROMPT now documents each v2 section and the rules Opus should
  apply: payroll signal switches from "registration.payroll" to "actual
  payrolls[] filings" (kills the false-positive swedish-payroll selection
  for newly registered employers); beneficialOwners[] becomes the
  authoritative ownership source (single owner → FMB modifier; multiple →
  multi-owner); statuses[] isCeased/red triggers an uncertainty_note.

Tests: 4112 unchanged. Build: green. No schema or migration changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): onboarding polish + composer signal fixes from first-run feedback

UX:
- AgentOnboarding: drop the 10s "Hoppa över — fortsätt med standardval"
  escape hatch. The fallback path runs automatically on timeout; the
  manual skip just teased users into a degraded build.
- ReviewCard step 2 title: "Stämma av detaljerna" → "Stäm av detaljerna"
  (imperative form matches the rest of the steps).
- Drop em-dashes from user-visible Swedish strings in AgentOnboarding +
  ReviewCard (fallback labels, subtitles, placeholder, error message,
  final CTA). Em-dashes survive in code comments only.
- "Fråga min revisor" → "Fråga min assistent" everywhere it surfaced:
  AgentTrigger, AgentSparkleButton, ReviewCard preview, ReviewCard
  fallback comment, general.help intent buttonLabel + prompt text.
- AgentTrigger / AgentSparkleButton / EmptyState.AgentHelpLink /
  TransactionInboxCard ask-button all gated on identity.isVerified.
  Pre-onboarding users no longer see the floating FAB or per-page
  Sparkle buttons. AgentSheetProvider.identity gained an isVerified
  field; (dashboard)/layout.tsx selects agent_profiles.verified_at and
  passes it through.

TIC verksamhetsbeskrivning:
- tic/index.ts /profile: /companies/{id}/purposes returns every
  historical verksamhetsföremål filing. Picking [0] was returning the
  oldest "äga och förvalta" holding-company boilerplate for companies
  whose later filings narrowed the purpose ("tillhandahålla
  företagskrediter och finansiella teknologilösningar"). Sort the
  array by lastUpdatedAtUtc desc and take the most recent non-empty
  purpose.

Composer banking signal:
- loadBankingSummary now reads journal_entry_id alongside
  description/amount/date and returns per-counterparty `direction`
  ('in' | 'out' | 'mixed') and `has_unbooked` (any row not yet booked).
  Aggregate `unbooked_count` accompanies the rollup.
- buildUserPrompt emits each counterparty as
  `Name: 12 345 kr (ut, OBOKFÖRD)` so Opus can tell income from cost
  on sight and tell which counterparties are still open questions.
- SYSTEM_PROMPT now explicitly forbids verification questions about
  counterparties whose direction is unambiguous AND status is 'bokförd'.
  Should kill the regressions from the first agent build:
  * "Konsult, J 98 565 kr — intäkt eller kostnad?" when the amount is
    clearly negative.
  * "ALMI AB 493 000 kr — lån eller bidrag?" when the transaction is
    already categorized.

Tests: 4112 unchanged. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent,ui): representation needs deltagare+syfte, drop duplicate doc icon

Representation booking:
- transaction-categorization prompt now requires the agent to capture
  participants (name + company) AND purpose before staging a
  representation categorization. SKV's representationsregler + ML 8 kap
  require the verifikation to document who attended and what the
  meeting was about; without that the avdrag is denied and the post
  should be booked as non-deductible / personalkostnad.
- The agent confirms back in plain text (audit trail in the chat),
  writes the deltagare + syfte to gnubok_remember_fact (long-term),
  THEN stages. Saknas deltagare/syfte: explicitly tell the user the
  avdrag won't go through and offer the non-deductible alternative.
- Known gap (followup, not this commit): the staged op's journal entry
  description doesn't yet carry the deltagare text. Until we add a
  `notes` field to gnubok_categorize_transaction, the audit trail
  lives in chat + agent_memory only.

TransactionInboxCard duplicate attachment indicator:
- Drop the FileCheck2 "open document" button from the trailing slot.
  TransactionAttachmentIndicator (Paperclip) next to the description
  already opens the underlag on click. Two icons doing the same thing
  was noise. Cleaned up the unused state (isOpeningDoc, hasAttachment,
  handleOpenAttachment) and dropped now-unused imports (FileCheck2,
  useToast).

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent,nav): notes on verifikation + redesigned sidebar

Audit-trail notes for representation:
- gnubok_categorize_transaction gains an optional `notes` string.
  Threaded through stagePendingOperation → commitCategorizeTransaction →
  createTransactionJournalEntry, which now appends notes to the entry's
  description (capped at 500 chars). The verifikation an external auditor
  reads now carries deltagare + syfte directly — not just chat history /
  agent_memory.
- transaction-categorization prompt updated: representation flow now
  REQUIRES the agent to pass deltagare+syfte via the notes parameter.
  Without it the booking is non-deductible / personalkostnad per SKV.

DashboardNav redesign:
- Top section: flat, no header — Hem (/chat), Underlag (was
  Dokumentinkorg), Transaktioner, Granskning. Always visible; the inline
  badge on /pending shows the count when there are pending ops.
- Mid section: four collapsible dropdowns (Försäljning, Inköp,
  Redovisning, Personal). Each auto-expands when the active route lives
  inside it. KPI moved from main to Redovisning. Extension nav items
  (TIC workspace, etc.) fold into Redovisning.
- Bottom-left: new account popover (DropdownMenu, opens upward) holding
  CompanySwitcher, Inställningar, Hjälp, Support, Logga ut. Replaces
  the old top company-switcher card + the bottom Support/Logout block.
- Mobile drawer mirrors the new structure: top items as flat list,
  same four dropdown groups, separate "Tillägg" section when
  extensions exist, "Mitt konto" section at the bottom.
- i18n: invoice_inbox label renamed "Dokumentinkorg" → "Underlag"
  ("Documents" in en). New keys: mitt_konto, group_extensions.

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nav): unhide Leverantörer under Inköp

The /suppliers entry existed in navItems but was marked hidden — leftover
from when the supplier list lived elsewhere in the IA. Removing the
hidden flag puts Leverantörer in the Inköp dropdown alongside
Leverantörsfakturor.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nav): CompanySwitcher back to top-left, user account moves bottom-left

The previous pass collapsed both concepts into the bottom popover. They
mean different things: the company is the org context everything below
operates against (top-of-sidebar, scannable); the user is the
account-holder (bottom-of-sidebar, where settings/logout live).

- (dashboard)/layout.tsx: fetch profiles.full_name alongside the
  existing identity queries; pass userName + userEmail into
  DashboardNav.
- DashboardNav: restore CompanySwitcher at the top of the sidebar
  (pre-redesign placement). Bottom-left popover trigger now shows the
  signed-in user's name + single-letter initial (accountInitial helper
  falls back to email's first char, then "?"). Popover header carries
  full name + email; items unchanged (Inställningar, Hjälp, Support,
  Logga ut). CompanySwitcher removed from inside the popover — nested
  dropdowns were awkward and the top placement is where it belongs.

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pending): trim the agent context strip

The row-level AgentContextStrip on /pending was rendering the model
name (eu.anthropic.claude-sonnet-4-6) and the full atoms array
(horizontal/swedish-vat, vertical/konsult-it, …) inline, which made
each row 60–80 chars of mostly-the-same metadata. Reviewers never
scan that text; they scan amounts and decide approve/reject.

Now the strip shows only the conversation deep-link
(Konversation #<short id>) — the one piece that's actually useful for
diving into context. Model + atoms remain available in agent_metadata
for debugging surfaces; they're just not in the list view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): shared ground rules + paragraph breaks after tool calls

Two regressions surfaced in real usage. Both are systemic.

Shared agent ground rules:
- /chat surface (general.help) was happily inventing four-digit BAS
  account numbers ("Debet 6212 - Molntjänster…", "Kredit 2614 - Ingående
  moms…") and proposing booking decisions on invoices it had never
  seen, with no follow-up questions about currency/scope/etc.
- transaction-categorization had those rules baked into its prompt;
  general-help / bokslut-step / invoice-draft / supplier-invoice-review
  / verifikation-draft / vat-review never inherited them.
- Extracted lib/agent/intents/shared-rules.ts with five cross-cutting
  rules: underlag first (check inbox + ask user to upload to
  Dokumentinkorgen when missing), ask follow-ups when ambiguous, never
  write four-digit BAS account numbers in chat (category names only),
  cite atoms / load skills (don't guess), check counterparty history
  before proposing.
- Injected renderAgentGroundRules() into all six intents above.
  transaction-categorization left alone — it has more detailed inline
  rules tied to its specific underlag-flow.

Paragraph break after tool calls:
- text_delta from the model often resumes after a tool call without a
  leading newline ("kategoriseras." → gnubok_query_journal runs → "Inget
  historik hittades…" appended directly). Markdown rendered the
  concatenation as one paragraph.
- AgentChat text_delta handler now inserts \n\n when (a) the buffer
  ends with text content, (b) the incoming delta starts with text
  content, (c) at least one tool call has run, and (d) the buffer
  doesn't already end with a blank line.

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(nav): default-open dropdown groups; closing is per-user

Dropdowns started collapsed which meant first-time users had to open
each group to discover what's inside. Inverted the state: default open,
user can collapse, active route still forces a group open.

- manualExpanded → manualCollapsed (semantics flip)
- toggleGroup unchanged externally; flips the bit
- isGroupExpanded returns !manualCollapsed[g] || hasActiveChild

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent): rate-safe v1→v2 TIC upgrade, counterparty defaults, profile settings

Three pre-ship quality wins.

Rate-limit-safe TIC v2 upgrade:
- The /profile endpoint fans out to ~13 Lens calls; the account has a
  ~3000/mo ceiling. Force-refreshing every pre-v2 (v1) snapshot across
  the customer base would blow the budget.
- ensureTicSnapshot gains an `upgradeV1` flag. A cached snapshot still
  inside the 7-day window is re-fetched only when (a) the caller passes
  upgradeV1 AND (b) the snapshot is v1-shaped (missing the v2-only
  `statuses` key). Gated to the two agent-onboarding call sites — a
  deliberate, once-per-company action and the only consumer of the v2
  sections. Workspace + signup keep the natural 7-day staleness, so the
  v1→v2 migration is lazy and bounded to companies actually building an
  agent.

Known-counterparty defaults (shared-rules):
- Agent now proposes a sensible default for well-known counterparties
  instead of asking the same question monthly: Almi → lån, Tillväxtverket/
  Vinnova/EU-stöd → bidrag, Skatteverket → skatt/avgift or återbäring,
  Bolagsverket → avgift, Försäkringskassan → ersättning, EF private
  withdrawal → eget uttag. Stated as an assumption the user can correct,
  not a hard rule — underlag/history still wins.

Företagsprofil settings page:
- New /settings/agent-profile (Företagsprofil / "Company profile"):
  view + edit the agent's company profile after onboarding — assistant
  name + avatar, the profile summary the agent reasons from, and a
  read-only chip view of loaded specialities (atoms). Backed by the
  existing GET/PATCH /api/agent/profile.
- New GET /api/agent/atom-titles?ids= resolves atom slugs → human titles
  for the chips (registry is globally-readable reference data).
- Added to SettingsSidebar; i18n keys agent_profile (sv "Företagsprofil"
  / en "Company profile").

Note: /chat already redirects unverified users to / (chat layout guard),
and / renders WelcomeGate → /onboarding/agent. No redirect work needed.
AgentSetupBanner.tsx is orphaned dead code (WelcomeGate superseded it).

Tests: 4112. Build: green. Both new routes compile.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(nav,agent): Hem=Översikt + separate Assistent button; memory dedup

Nav restructure:
- "Hem" now points to / (Översikt dashboard) again, not /chat. The agent
  chat gets its own top-level nav entry "Assistent" (Sparkles icon) → /chat.
  Mobile bottom nav mirrors this (Hem / Assistent / Transaktioner).
- / restored to render DashboardContent (the Översikt) for built-agent
  users instead of redirecting to /chat. Users who haven't built their
  assistant yet still get WelcomeGate (the build-agent checklist); once
  verified, / shows the dashboard. Chat is reachable anytime via its nav
  entry. Restored main's dashboard data-fetch; added an agent_profiles
  verified_at probe to drive the WelcomeGate branch.
- i18n: nav.assistant ("Assistent" / "Assistant").

agent_memory dedup (gnubok_remember_fact):
- The agent re-remembers the same fact constantly (e.g. "Vercel = omvänd
  skattskyldighet" on every Vercel categorization), which would bloat
  agent_memory with paraphrases over months.
- Before insert, compare the incoming fact against the 300 most-recent
  active memories by word-set Jaccard similarity (lowercased, punctuation-
  stripped, stopwords dropped). A near-duplicate (≥0.82) is treated as
  already-known: bump its relevance toward the new score + refresh
  updated_at instead of writing a new row. Embedding-free, zero added
  latency beyond one bounded SELECT.

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent,nav): företagsprofil=Bolagsuppgifter, avatar nav icon, dedupe greeting

Företagsprofil settings page (the right content this time):
- Replaced the agent atoms/summary panel with CompanyProfileView — a
  read-only "Bolagsuppgifter" view of the cached TIC company snapshot
  (name, org-nr, form, address, F-skatt/Moms/Arbetsgivare, SNI, bank,
  verksamhet, employees, latest financials, status traffic-lights,
  fiscal year, firmateckning, företrädare). Server component reads the
  companies.tic_snapshot column directly — no extension import, stays
  inside the core-build boundary.
- Route renamed /settings/agent-profile → /settings/company-profile.
  Removed the old AgentProfilePanel + the now-unused /api/agent/atom-titles
  endpoint.

"Assistent" nav icon = the agent's chosen avatar:
- DashboardNav reads agent identity from AgentSheetProvider and renders
  the onboarding-chosen avatar for the /chat ("Assistent") entry across
  desktop sidebar, mobile drawer, and mobile bottom nav. Falls back to
  the Sparkles glyph pre-onboarding (no avatar yet).

Nav cleanup:
- Dropped the beta badge from Underlag.
- Filtered the TIC workspace (/e/general/tic, "Företagsprofil") out of
  the nav — the same Bolagsuppgifter now lives under Inställningar →
  Företagsprofil, so it shouldn't appear in two places.

Doubled intake greeting fix:
- /chat/intake fires an invoke with no conversation_id, then swaps the
  URL to /chat/[id] the instant the `conversation` event lands — which
  can beat the greeting being persisted. /chat/[id] then hydrated with 0
  messages and, because the auto-fire guard keyed on (id && messages>0),
  fired a SECOND invoke on the same conversation → two greetings.
  Guard now keys on conversation-id presence alone: a set id means
  resume, never bootstrap. Closes the race.

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): paragraph-break-after-tool split words mid-stream

The earlier "insert \n\n when text resumes after a tool call" heuristic
re-evaluated on EVERY text_delta (any delta not starting/ending with
whitespace, once a tool had run). Streaming deltas arrive in sub-word
chunks, so it injected breaks between fragments of the same word:
"minnes\n\nno\n\nterna", "kund\n\nrep\n\nresentation".

Replace the per-delta heuristic with a consume-once ref:
- tool_use sets breakBeforeNextTextRef = true
- the next text_delta consumes it: prepends \n\n exactly once (only when
  the buffer has content, doesn't already end in whitespace, and the
  delta doesn't start with whitespace), then clears the flag

So the break fires once per tool→text resume, never mid-word.

Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): much shorter replies, representation headcount + VAT cap, dot separator

Brevity (system-prompt Svarsformat — affects every reply):
- Hard "korthet är regel nummer ett": aim for 2-4 sentences, lead with
  the answer/action, no warm-up ("Här är vad som gäller…"), don't derive
  VAT in prose, don't restate what the approval card shows, one question
  at a time. The agent was writing textbook-length essays.

Representation rule now in shared-rules (so verifikation-draft, vat-review,
etc. all get it — previously only transaction-categorization had it, which
is why the verifikation flow guessed 25% VAT and skipped the cap):
- Require ANTAL deltagare (headcount), not just one name — the moms
  deduction is per person (underlag cap 300 kr/person ex moms).
- Use the receipt's ACTUAL VAT rate (usually 12% on food), never assume
  25%.
- Meal representation isn't income-tax deductible (post-2017); whole cost
  booked as non-deductible representation.

Verifikation description separator:
- createTransactionJournalEntry appended notes with an em-dash
  ("Utlägg Eatnam — Deltagare:…"), violating house style. Switched to a
  middle dot " · ". journal_entries has no separate notes column — the
  description IS the BFL verifikationstext / audit field, so deltagare +
  syfte correctly live there.

Tests: 4112. Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(settings): tidy Bolagsuppgifter — no status colours, clean firmateckning

From first-look feedback on the Företagsprofil page:

- Status: dropped the coloured traffic-light badges (red/yellow/green).
  Per the design system semantic colour is data-only, never chrome, so
  status now renders as plain label + date. Also filtered to dated
  entries only — Bolagsverket emits flags like "Har aldrig varit verksam"
  with no date that read as noise next to the real status. Ceased status
  gets muted destructive text (the one chrome colour the system keeps).

- Firmateckning: the source text carries ">" list markers and crams
  several rules onto one line, and repeats "Firman tecknas av styrelsen"
  across rows. cleanSignatory() strips the markers, normalises whitespace,
  splits run-on "Firman tecknas …" clauses onto separate lines, and the
  render dedupes — so each rule reads as its own sentence.

Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): inbox items expose all terminal links + processed flag

The Eatnam receipt was booked against its bank transaction (so the inbox
row had matched_transaction_id + created_journal_entry_id set), yet the
agent reported it as loose/unmatched and a duplicate risk. Root cause:
gnubok_list_inbox_items only selected and returned matched_supplier_id +
created_supplier_invoice_id — the supplier-invoice path. The
transaction-match and direct-journal-entry paths were invisible, so any
receipt cleared via /transactions looked unprocessed.

- list_inbox_items now selects + returns matched_transaction_id and
  created_journal_entry_id alongside the supplier fields, plus a derived
  `processed` boolean (true when ANY of the three terminal links is set).
- New unprocessed_only=true input filters to items with no terminal link
  — the "what still needs handling" view that prevents the agent from
  flagging already-booked docs as duplicates. (Fetches a wider window
  then filters client-side so limit applies post-filter.)
- Description updated to document the processed semantics, within the
  280-char tool-description budget.

The DB linkage itself already worked: /transactions attach-document sets
matched_transaction_id, and commitCategorizeTransaction stamps
created_journal_entry_id. This was purely a read/surface gap.

Tests: 4112 (+ MCP description guard). Build: green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(mcp): repair stage-but-never-commit tools + consolidate tool surface

- post_annual_depreciation AND reverse_entry were never in the pending_operations operation_type CHECK, so both staged then died with check_violation at INSERT. Add the CHECK migration, a commitPostAnnualDepreciation executor (reusing commitAnnualPostings), risk tier, and the PendingOperationType union member.
- Salary tools de-risked: calculate_salary_run calls runSalaryCalculation() directly (no self-fetch/forged cookie); create_salary_run uses a transactional create-run helper with compensating delete; generate_agi actually generates + persists the declaration.
- import_sie parses + validates at stage time with a content-rich preview (company, fiscal year, voucher/account counts, balance) instead of a blind byte count.
- batch-match-invoices passed user.id where companyId was expected (silently matched zero).
- VAT report+widget merged behind render_ui; gnubok_search_tools ranks by relevance; gnubok_feedback readOnlyHint corrected; tools/list instruction text fixed; income decision-tree + GL/query_journal cross-refs added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent): load skill atom bodies from the DB so they survive the build

Skill bodies were read from disk at runtime (.claude/skills/**/SKILL.md); on Vercel the dynamic readFile path isn't traced into the lambda and on Docker .claude/ is excluded, so atoms loaded EMPTY in production — a despecialized agent. Inline the bodies into agent_atom_registry instead:
- Migration adds body + mcp_exposed columns; a build-time generator (scripts/generate-skill-bodies.ts) emits a deterministic dollar-quoted seed migration with a content-hash manifest + --check CI guard.
- Read sites (mcp-server atoms.ts, chat system-prompt.ts, composer prewarm) read body from the DB, with a dev-only disk fallback. mcp_exposed curates which atoms the MCP exposes (swarm-* never become atoms).
- The seed script + generator share scripts/lib/atom-discovery.ts; estimated_tokens now reflects SKILL.md only.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(agent): safe the in-app assistant — gating, FAB de-confliction, rate limit, friendly errors

- Hide all agent entry points until verified_at: the Assistent nav tab (sidebar + mobile) and the agent-memory settings tab now match the floating FAB's gate.
- FAB de-confliction: /kpi -> kpi.explain and /bookkeeping/year-end -> bokslut.step so the floating button opens the SAME assistant as the page button (no two-agents-on-one-page).
- Generous per-user rate limit (30/min, 1000/day) on /api/agent/invoke, /onboarding/stream, /composer via a new agent_rate_counters table + check_and_increment_agent_quota RPC; fails open. Bounds runaway Bedrock spend without touching normal users.
- Friendly errors: Bedrock 429/timeout/5xx normalized to Swedish (friendlyModelError) in run-turn + the invoke route; the chat client surfaces the server's friendly message instead of a raw HTTP status.
- /chat/new validates ?intent= against the registry so bad deep-links fall back to general.help instead of rendering a broken-looking error.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): keep /chat read-only — redirect categorization + swap the "categorize" suggestion for a VAT-report question

general.help (the /chat assistant) is read-only, but it still gave per-transaction bokföringsförslag in prose and asked "godkänner du dessa?" — an analysis the user can't act on (no write tool, no per-tx underlag). Strengthen the prompt to redirect categorization/bokföring to the per-transaction flow (open the transaction -> "Fråga om denna transaktion", where the agent sees the underlag and stages a real ApprovalCard); a short overview is still allowed. Add a guard test locking in no-write-tools + the redirect language. Swap the /chat empty-state "Hjälp mig kategorisera" chip (which lured users into exactly this dead-end) for a VAT-report question the read-only assistant can actually answer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(pending): declutter the review queue rows + header

Fold the conversation deep-link onto the actor label (drop the separate
"Konversation #xxxx" strip and its icon), hide the quick-pick when there's
only one operation type (it duplicated "Markera alla"), and drop the "(0)"
from the disabled bulk-approve button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(vat): enhance VAT handling by integrating document validation and improving error messaging

* feat(settings): add assistant knowledge surface + consolidate settings tabs

Expose the agent's skill atoms (agent_atom_registry) in a read-only surface
beside the existing memory view, and tighten the settings tab bar from 14 to
10 tabs.

- New GET /api/agent/skills + AgentSkillsPanel: lists active, mcp_exposed
  atoms grouped by tier (Kärnkompetens / bransch / bolagssituation), flags
  which are active for the company from agent_profiles, and lazy-loads each
  SKILL.md body on expand.
- New /settings/assistant tab with a Minne/Kompetens toggle (?view=skills);
  /settings/agent-memory and /settings/agent-skills redirect into it.
- Merge Företagsprofil (TIC snapshot) into the Företag tab via
  CompanyProfileSection; /settings/company-profile redirects.
- Merge Skatteverket-anslutningen into the Skatt tab — OAuth returnTo and the
  callback toast now target /settings/tax; /settings/skatteverket redirects.
- Drop the Säkerhetsbackup tab (already under Importera/Exportera).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(inbox): keep booked underlag out of the unmatched queue + widen match window

- categorize: after booking an inbox underlag onto a verifikat, backfill the
  inbox row's matched_transaction_id + created_journal_entry_id so it stops
  showing as unmatched (mirrors the /attach-document paperclip path).
- TransactionMatchPicker: bias the candidate window forward (60d before →
  180d after the invoice date) so late payments aren't dropped before scoring,
  and widen the ranking date tolerance to 120d so the true match floats to the
  top instead of collapsing to "Svag match". Fix "okatigoriserade" typo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wip: bundle in-progress branch work + agent onboarding chat optimizations

Captures the uncommitted work-in-progress on this branch so it lives on the
remote. Heterogeneous changeset — bundled as one commit since the work was
already entangled across files.

Headline change in this commit (from this session):
- Remove the double interview in agent onboarding. Phase B's verification-
  question form stepper is gone — the Phase C chat (onboarding.intake) now
  owns the entire interview and reads the composer's verification_questions
  server-side as its question bank.
- ReviewCard collapses from 3 steps to 2 (meet → review-and-confirm) with
  value-first ordering: profile + "vad jag kan hjälpa dig med" + facts +
  optional seed note. CTA reads "Möt {namn}" to signal the chat follows.
- ChatIntakeStarter handoff subcopy updated to match reality (assistant
  greets first; user can leave anytime).
- Stamp agent_profiles.intake_completed_at server-side in
  app/api/agent/invoke/route.ts on the first user-typed reply in any
  onboarding.intake conversation (idempotent IS NULL guard, best-effort).
  Closes the previously dead-write column and unlocks the opportunistic-
  follow-up hook the migration anticipated.

Plus in-progress branch work being carried forward (not introduced here):
agent runtime + intent prompts, composer + atom-discovery scripts, MCP
server skills surface, onboarding flow components, dashboard/inbox tweaks,
two new agent_atom_registry migrations, additional agent-chat tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(agent): drop inline "Fråga assistenten" affordances — rely on the FAB

The bottom-right "Fråga {namn}" FAB (AgentTrigger) is already route-aware
and picks the right intent per page, so duplicating it as inline page-
header buttons and empty-state links is noise. Removed:

- EmptyState `agentHelp` link ("Eller fråga {namn} hur du kommer igång")
  + the AgentHelpLink component + agent_default_name/agent_ask_link i18n
  keys + the agentHelp props on EmptyInvoices/EmptyCustomers/EmptyTransactions.
- AgentSparkleButton on /bookkeeping (verifikation.draft) and /kpi
  (kpi.explain) page headers.

The FAB stays — when verified, it appears on those routes and routes to
the right intent automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): gate the last two ungated "Fråga assistenten" affordances

Both surfaces previously called useAgentSheet directly without checking
identity.isVerified, so they appeared pre-onboarding (everywhere else the
FAB / sparkle buttons / /chat / Assistent nav are all gated on verified_at).

- Settings page header: remove the "Fråga {namn}" pill entirely. The FAB
  covers /settings routes route-aware (settings.help) — no need for a
  duplicate inline trigger.
- Invoice inbox transaction picker: hide the "Fråga assistenten" button
  when the agent isn't built. Done at the parent (InvoiceInboxWorkspace)
  by passing onAskAssistant only when identity.isVerified is true; the
  child renders the button only when the callback is present.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(tic,onboarding,agent): single-call TIC lookup + director-aware narrative voice

- TIC: collapse the company lookup from 6 endpoint calls to 1
  (search-public already exposes sniCodes, bank accounts, emails, phones,
  and registration flags). Derive fiscal-year MM-DD from
  mostRecentFinancialSummary; newly-registered companies fall through to
  the client's first-year defaults.
- Onboarding: BankID picker no longer auto-provisions companies. Every
  pick routes through the wizard with orgnr (and entity_type via the
  CompanyRoles match) prefilled; F-skatt/VAT/address get confirmed in
  steps 2-4 instead of being auto-fetched. createCompanyFromOnboarding
  reuses CompanyLookupResult and adds a defensive top-level catch so
  server-action errors surface to the UI instead of being redacted.
- Agent composer: loadUserDirectorship() checks BankID CompanyRoles for
  a director-like position (ceo/boardMember/chairman/externalSignatory,
  active) before the narrative uses second-person ownership voice
  ("Du driver…"); unknown users get neutral third-person voice so we
  never put ownership words in the user's mouth.

Tests cover loadUserDirectorship, narrative voice, tic-fetch path,
onboarding page, and updated TIC client + lookup/profile suites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(tic): extend agent-onboarding TIC budget to 10s + backfill stranded org_numbers

The 5s TIC fetch timeout aborted client-side before the upstream Lens
fan-out (~13 calls) could complete, but the in-flight upstream calls
still counted against quota — actions.ts already documents ~530 wasted
calls from this in May. Same bug still applied to the agent-onboarding
stream path. Adds an optional `timeoutMs` to `ensureTicSnapshot` so
deliberate wait-screen callers (agent onboarding stream) can run with
10s while background/dev callers stay on the conservative 5s default.

Page-level server fetch (page.tsx) intentionally stays at 5s to avoid
blocking TTFB without a visible progress affordance.

Backfill migration mirrors `company_settings.org_number` to
`companies.org_number` for the 105 cases where it's safe (after dedup
+ conflict filtering). 56 of those are on active companies — unblocks
duplicate guards, SIE/SRU exports, and TIC fallback chain. Zero TIC
API calls — pure data move. Idempotent.

Also sweeps a pre-existing SSRF guard on the stream route's origin
derivation that was sitting unstaged in the working tree — it lives in
the same diff hunks as the TIC budget change and couldn't be split cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* wip: bundle in-progress branch work

Sweep up uncommitted agent/MCP/RLS work-in-progress so the branch is fully
backed up to origin. Not reviewed in detail — committed as-is to preserve
working state alongside the TIC fixes in the previous commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(agent): tag the "Bygg din bokföringsassistent" CTA as Beta

Adds a Beta badge next to the assistant-setup heading on the dashboard
banner, dashboard inline card, and onboarding checklist row. Also drops
the stale "Gratis i 30 dagar" subline from the dashboard card.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(build,migrations): PendingOperationType salary ops + resolve migration version collisions

PR #584 went red on three things:

1. core-only build / Vercel: `lib/pending-operations/commit.ts:2666` switched on
   'create_salary_run' and 'generate_agi' but `PendingOperationType` was missing
   both literals. Add them to the union.

2. Supabase preview: migration version 20260526120000 collided with main's
   newly-merged 20260526120000_fix_replace_sie_import_hard_delete.sql.
   Bump the branch's pair to 20260526120050 / 20260526120051 — still ahead of
   20260526120100_restvardeavskrivning so ordering is preserved.

3. 20260527170000 was used twice on this branch
   (_agent_rls_with_check + _journal_entry_no_doc_required). Bump the second
   to 20260527170100 so the pair stays orderable and Supabase doesn't choke
   on the duplicate schema_migrations PK.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ci): reword comment so core-only guard stops flagging it

The "Check no core imports from extensions" step greps for the literal
\`from '@/extensions/\` across lib/, app/api/, components/. A comment in
lib/agent/composer/tic-fetch.ts quoted the exact pattern verbatim to
explain *why* the file does a self-fetch instead of importing the TIC
extension directly — which the grep matched even though no actual
import exists.

Rewrite the line to keep the same meaning without the literal pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Emil <emilmattsson14@gmail.com>
2026-05-28 11:27:01 +02:00
Mattsson a1a816b4a5 Delete features (#218)
* Implement company and account deletion features

- Add event types for company and account deletion to CoreEvent.
- Enhance Supabase middleware to handle company context resolution and cookie management for archived companies.
- Create API routes for deleting accounts and companies, including necessary validations and event emissions.
- Implement tests for account and company deletion endpoints to ensure proper functionality and error handling.
- Add retention notice component to inform users about bookkeeping data retention during destructive actions.
- Create database migrations to support soft deletion of companies and anonymization of user accounts, ensuring compliance with retention laws.

* feat: enhance account deletion process and update user notifications

* Add service client for onboarding completion check and update escape hatch visibility

* Enhance invite flow and email handling for company members

* Refactor company context and RLS policies for active company isolation

- Update `switchCompany` to remove unnecessary revalidation as client handles navigation.
- Revise `getActiveCompanyId` to prioritize `user_preferences` and validate against non-archived memberships.
- Modify `setActiveCompany` to ensure `user_preferences` is the authoritative source while maintaining cookie compatibility.
- Enhance middleware to resolve active company using `user_preferences` and fallback to first non-archived membership.
- Introduce new API route `/api/company/current` to fetch the active company ID for cross-tab synchronization.
- Implement `CompanyTabSync` component for real-time active company enforcement across tabs.
- Create migration for RLS policies to enforce single-active-company isolation using `current_active_company_id()`.

* feat: implement viewer role enforcement for write permissions

- Added `useCanWrite` hook to determine if the current user has write permissions based on their role in the active company.
- Updated various components (JournalEntryForm, CustomerForm, DeadlineForm, etc.) to disable write actions and show a lock icon with a tooltip for users without write permissions.
- Introduced `requireWritePermission` function to enforce write permissions at the API level, returning a 403 response for viewers.
- Created tests to verify the behavior of the viewer role and write permissions.
- Added database migration to enforce read-only access for viewers at the database level.
2026-04-11 17:06:32 +02:00
MattssonandClaude Opus 4.6 0dd1f5ebc1 feat: multi-tenant company refactor (GNU-19) (#153)
* feat: multi-tenant company refactor (GNU-19)

Introduce companies table, company_members, and user_preferences to
support multiple companies per user. All data scoping changes from
user_id to company_id across the entire codebase.

Key changes:
- Database migration: new tables, company_id on 40+ tables, backfill,
  RLS rewrite from user_id to company-member-based, updated RPCs
- Types: Company, CompanyMember, CompanyRole, UserPreferences types;
  company_id added to all entity interfaces; companyId on all events
- Engine: all 7 core functions take companyId; storno, period, year-end
  services updated; 16 report generators updated
- Middleware: company context resolution (cookie → prefs → first company)
- API routes: ~120 routes updated with requireCompanyId()
- Frontend: CompanyProvider context, layout/dashboard/onboarding updated
- Extensions: context factory, 9 extensions, all lib files updated
- Tests: 1880 tests passing, all helpers updated with company_id defaults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add database migrations for multi-tenant company and team system (GNU-19)

Adds company_invitations, company creation RPC, team_members, account
deletion RPC, and teams table refactor migrations. Updates base
multi-tenant migration with cascading FKs and onboarding_step column.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add team types and update core infrastructure for multi-tenancy (GNU-19)

Adds TeamRole, MemberSource, and Team types. Refactors Supabase service
client to be stateless, updates middleware for team-aware routing, extends
CompanyContext with team/role fields, and updates extension service types
to accept companyId.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: thread company_id through business logic functions (GNU-19)

Replaces user_id scoping with company_id across all lib modules:
bookkeeping, documents, transactions, invoices, reconciliation, tax,
deadlines, and import. Updates corresponding tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* refactor: thread company_id through API routes and extensions (GNU-19)

Updates all existing API routes to extract and pass companyId. Updates
enable-banking and arcim-migration extensions for company-scoped
transaction ingestion and sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add company and team management API routes (GNU-19)

Adds CRUD endpoints for company members, company invitations, team
members, and team invitations. Includes invite token utilities, email
templates, and company switch server action.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add team/company UI components, pages, and dashboard updates (GNU-19)

Adds CompanySwitcher, ConsultantEmptyState, Step0RoleChoice, company
members and team management panels. Updates dashboard layout for
team-aware routing, onboarding for multi-step role choice, and auth
callback for team invite acceptance. Ignores supabase/.branches/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add null guards for company in import page (GNU-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: move appUrl declaration to outer scope in invite route (GNU-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add optional chaining for company.name in members section (GNU-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add optional chaining for second company.name in members section (GNU-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: add null guards for company in extension components (GNU-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: pass companyId to executeSIEImport in arcim-migration extension (GNU-19)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: update tests to use companyId instead of userId and improve type handling

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 16:41:52 +02:00
Jakob WennbergandClaude Opus 4.6 f3ae3cd361 feat: event log, pending operations, and MCP staging (#135)
* feat: event log, pending operations, and MCP staging

- Event log system: persist bus events to event_log table for external
  automation platforms. Batch insert for transaction.synced. Daily
  cleanup cron at 02:00 UTC.
- Pending operations: MCP write tools (categorize, create customer,
  create invoice) now stage to pending_operations instead of executing
  directly. Users review and commit/reject from /pending in the web UI.
- Granskning page: card-based review UI with expandable previews,
  commit/reject dialogs. Only shown in nav when pending ops exist.
- Commit route re-executes using core lib functions (no extension
  imports). Guards against stale state (double-commit, deleted entities).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: stage new MCP write tools after main merge

Add staging for 4 new write tools from #133:
- mark_invoice_paid, send_invoice, mark_invoice_sent,
  match_transaction_invoice
- Expand pending_operations CHECK constraint
- Add commit executors with full execution logic
- Add UI labels and generic preview component
- Remove confirm parameter from categorize (single-call staging)
- Fix UUID in pending op title (fetch transaction description)
- Hide Granskning nav when no pending ops

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR review feedback

- Fix TS build error: use `select('*, customer:customers(*)')` for
  match_transaction_invoice to avoid array type inference
- Add status guard to commitSendInvoice (prevents duplicate sends)
- Replace auth.admin.getUserById with user email from session auth
- Restore optimistic lock check in commitMatchTransactionInvoice
- Fix tool description typo: expense_software → expense_office

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 10:07:36 +01:00