# bokf — settled decisions Settled decisions, newest last; the log exists for archaeology, not for daily reading. The live snapshot is `docs/STATE.md`; the rules are in `AGENTS.md`; UI conventions are in `docs/TUI-GUIDELINES.md`. Entries are kept verbatim from the STATE.md they were pruned from (2026-09-21). ## Locked decisions 0. **TUI widget layer**: `clients/tui.[ch]` is the only module that touches ncurses; screens compose its widgets and never keep local input/drawing patterns. Every widget has a spec in `TUI-GUIDELINES.md` and unit tests in `tests/test_tui.c` (run by `make test`; pure logic separated from drawing). Extend the layer, not the screen. 1. **Name/license**: `bokf`, daemon `bokfd`, clients `bokfctl` (scriptable) and `bokftui` (ncurses); GPL-3.0-or-later; repo `~/work/bokf`. 2. **Stack**: C11, Makefile, vendored SQLite 3.53.4 / yyjson 0.13.0 / Argon2 20190702 / SHA-256 (public domain). Only system dep: libncursesw. 3. **Storage**: SQLite WAL, `synchronous=FULL`, STRICT tables, composite-key tenant isolation, append-only triggers, `VACUUM INTO` snapshots; forward migrations snapshot to `pre-migration-v-.db` first and refuse to migrate without it. Postgres deliberately rejected for now; keep DB access behind one layer for a later port. 4. **Protocol**: NDJSON over Unix socket (+ optional plain TCP and a native TLS listener), protocol v1. `dry_run` on every mutation, `client_ref` idempotency, stable error codes, `describe` + `agent.instructions`, money in integer öre. 5. **Auth**: multi-org; memberships owner/bookkeeper/viewer; API tokens bound to user+org with scopes, shown once, revocable; sessions in memory; Argon2id. Server messages English, UI Swedish. 6. **Compliance design**: SHA-256 audit chain, SHA-256 voucher chain (canonical encoding in `SCHEMA.md` §7.1/§9.1), period locks, fiscal year close, corrections only as ändringsverifikat, SIE 4 (CP437) in/out. 7. **Verifikat ids**: series is free text (`A`, `V-`, `A ` …); unbroken numbering per fiscal year+series; id displayed as `series+number` (`V-8`). Org setting `default_series` (Inställningar) for new vouchers and new templates. 8. **Templates** (`konteringsmallar`): server-side; formula language over `x` with `+ - * /` and parentheses, positive=debit, negative=credit, zero rows dropped; rounding remainder assigned to the largest row; `{x}` in the description; `template.*` commands + `voucher.post {template,x}`; archive instead of delete. 9. **Ingående balans**: one series `IB` voucher per fiscal year (dated at year start). Reports treat series IB as IB, not period movement; SIE export/import round-trips without double counting; TUI editor shows the year's *effective* opening balances (carry-forward plus IB vouchers) and posts deltas, so nothing is ever edited and a target balance can be entered directly. 10. **Attachments**: content stored in the DB (BLOB), immutable, linked via append-only `voucher_attachments`. Default limit 10 MiB (`max_attachment_bytes`); the socket line limit is derived from it (base64). TUI `Ctrl+F` attaches via file browser; the client checks the size from `meta` first. Setting `attachment_dir` (tilde expanded). 11. **UI keys**: Ctrl+N = add, F5 = refresh, Esc/q = back (never exits), Ctrl+C = quit, 1–9/g = jump, F7 = clear row in editors, F9 = save. Shared line editor and date field; see TUI-GUIDELINES.md. The dashboard is sectioned (Bokföring / Rapporter & bokslut / Register / Räkenskapsår / Övrigt) with non-selectable headers; theme/styles and pager markup live in the widget layer (`tui_style`, `tui_markup`). Year-end **Bokslut** and the former "Information om året" are one screen (`yearinfo` is an alias). 12. **Login**: two steps — credentials, then org picker ("Välj organisation att representera"). No org switch in the dashboard; fiscal year switch is on the dashboard. `--org ID` bypasses the picker. 13. **Settings**: `settings.get`/`settings.set`; keys `default_series`, `attachment_dir`. 14. **Transport**: Unix socket for host clients; plain TCP loopback-only; native TLS listener (`BOKFD_TLS`, `BOKFD_TLS_CERT/KEY`, OpenSSL, TLS 1.2+, cert reload on file change) with client targets `tls:host:port` and system-trust verification (`BOKFD_TLS_CA` for private CAs). Certificates come from a lego sidecar using INWX DNS-01 (`compose.yaml`). Externals get accounts/roles/tokens, never VPN access. The runtime image is **Alpine + backend only** (`bokfd`, `bokfctl`; the ncurses TUI is a frontend built on the client machine). `scripts/deploy.sh` builds locally and ships over SSH, or — when the architectures differ — cross-compiles the backend on the dev machine as **static aarch64 (glibc + OpenSSL archives, runs directly on Alpine, DNS verified) and assembles the image in the host's Docker** (`deploy/Dockerfile.cross`, ~20 s; the image carries no `libssl3`). `scripts/deploy.sh --dev` hot-reloads the binaries in the running container (SIGHUP re-exec via `docker cp`), cross-compiling first when the architecture differs. 15. **Reports in the TUI**: rendered as fixed-width Swedish tables that mirror the Kapitas PDF exports (Saldobalans, Resultatrapport with previous-year column and 89xx bokfört/ej bokfört, Balansrapport with Ing balans/Ing saldo/Period/Utg balans and Beräknat resultat, Momsrapport ruta för ruta). The TUI never shows report JSON. Amounts are Swedish formatted (`1 234,56`); moms rutas are whole kronor truncated like Kapitas. Report tables mark their column-header row with `\x04` (`TUI_MARK_STICKY`) so the pager pins it above the scrolling body; the verifikat detail shows column headers and separates the underlag section with a rule. PgUp/PgDn page without wrapping and stop at the first/last row. 16. **Moms rules (schema v3)**: default seed covers 05 over 3000-3019, 3100-3199, 3300-3399, reverse-charge sales (32xx) in 41, EU purchases in 20/21, reverse-charge output VAT 2614/2624/2634 in 30/31/32, and box 48 signed negative. Rules may share a box and are summed; box 49 is the sum of the moms boxes only. v3 migrates existing databases. 17. **Year-end hub and menu IA (2026-09-19)**: the forms widget layer gained an action list (`tui_form_run_actions`; fields and actions in one focus ring, `TUI_FORM_ACTION + i` for a chosen action, dimmed-but-selectable rows with a `disabled_reason`, `-1` heading/status rows). The **Bokslut** screen is the year-end hub: the eight year fields, the derived status row `Bokslut bokfört: ja/nej` (`voucher.list` with `text:"Skatt på årets resultat"`; a closed year counts as posted) and the actions Årsredovisning (K2), Inkomstdeklaration (INK2/SRU), Bokslutsplan (torrkörning) and Bokför bokslut (dimmed with a reason on a closed or already posted year). Årsredovisning and INK2/SRU left the Rapporter menu, and Ingående balans moved from Bokföring to the dashboard's Räkenskapsår section next to Räkenskapsår. The form widgets remember focus across runs (`int *focus` as the last argument to `tui_form_run*`: clamped to a selectable row on entry and written back on every return; screens own a `static int sel`), so a sub-screen round-trip or a field save returns the highlight to the same row. 18. **Momsregler (2026-09-20)**: the per-org vat `report_rules` are edited with `report.rule_list/create/update/delete` (mutations owner-only, audited, `dry_run`; rules are config, not ledger) and in the TUI **Momsregler** screen under Register. `report.vat` honors `match_type` `account`, `range` and `type`; several rules may share a box and are summed. Only the `vat` report is consumed today, so the commands accept only that report. 19. **Bank reconciliation (2026-09-20, schema v8)**: phase 1 is mechanical only — `bank.import` (SEB CSV, idempotent via a per-row source hash), `bank.list` with exact-amount suggestions within ±5 days, `bank.match`/`bank.unmatch`. Imported rows are statement evidence, matches are mutable, and **nothing is booked automatically**. Setting `bank_account` (default `1930`). Phase 2 (decided shape): from an unmatched row, open Nytt verifikat with date/description/amount prefilled (F4 template still there); after posting, auto-match the new voucher and stay in the list. Later: `bank_rule.*` pattern suggestions and "skapa verifikat från transaktion". TUI screen **Bankavstämning** under Bokföring. **Phase 2 implemented 2026-09-20**: Ctrl+N (or `Skapa nytt verifikat…` in the match list) prefills the form from the transaction and auto-matches the posted voucher, returning to the list with the cursor kept. 20. **Menu IA (2026-09-20)**: one top-level section per workflow domain (Bokföring, Fakturering, Lön, …), Register owns master data, Rapporter owns read-only output, Räkenskapsår its own, and a feature adds at most one section. Sections stay stable; screens come and go inside them. 21. **Invoicing (2026-09-20, design)**: `docs/INVOICING.md` is the chapter. bokf owns invoices end to end: customer register, a global always-increasing number series in `invoice_sequence` (start 17761), `OCR = number + MOD10`, a one-page PDF reproducing the Google Sheets original (Helvetica + Comfortaa outlines, no new deps), issue = number + PDF attachment + voucher + link in one transaction, F5 preview of the real PDF without consuming the number, and SMTP sending with the password encrypted in the DB (AES-256-GCM, `BOKFD_SECRET_KEY`). Implementation waves: schema v9 + PDF, SMTP, TUI (Fakturering). **Wave 1 done 2026-09-20**: schema v9 (`customers`, `invoice_sequence`, `invoices`, `invoice_rows`, widened `vouchers.source` with a table rebuild), the PDF renderer (`src/invoice.c`, 2.2 % raw pixel diff vs the Google Sheets original — all structure exact; only Arial-vs-Helvetica glyphs differ), and `customer.*`, `invoice.sequence_get/set`, `invoice.preview/issue/list/get/pdf`. Issue is atomic: number + PDF attachment + voucher (D 1510/K 3xxx+26xx, `source:"invoice"`) + links. Settings `invoice_receivable_account`, `invoice_revenue_account`, `invoice_bankgiro`. **Swish QR: decided 2026-09-20 — not supported** (invoice 1's QR is dropped; the generator has no image support). **Wave 2 done 2026-09-20**: settings secrets are AES-256-GCM encrypted with `BOKFD_SECRET_KEY` (`smtp_password`; `settings.get` never returns it), SMTP over TLS/STARTTLS/plain (`src/smtp.c`) and `invoice.send` (subject `Faktura `, PDF attached, `last_sent_*`, audited, `SMTP_NOT_CONFIGURED`/`SMTP_FAILED`). **Wave 3 done 2026-09-20**: TUI **Fakturering** with `Fakturor` (list, detail `p` = PDF via `xdg-open`, `s` = send/resend), the invoice form (customer picker, auto due date, row table, F5 = preview of the real PDF, Ctrl+Enter = issue + send confirm), **Kunder** in Register, and the SMTP/invoice fields in Inställningar. pty golden scenarios `invoices`, `customers`, `invoice-form`. 22. **Command table split (2026-09-20)**: `src/commands.c` keeps only discovery (`describe`, `agent.instructions`), `command_find` and the registry; handlers, argument schemas and `g_cmd_[]` tables live in `src/cmd_.c`, with shared helpers in `src/cmd_util.[ch]`. `g_command_tables[]` registers the files in the original catalogue order so `describe` output is byte-identical, and `scripts/check-consistency.sh` / `make check` scans `src/commands.c` and every `src/cmd_*.c`. 23. **TUI screen split (2026-09-20)**: the same idea for the TUI: `clients/bokftui.c` keeps `main` (arg parsing, login, startup), `config_path`/`config_mkdirs`, the Ctrl+R reload and the scene registry (`struct scene SCENES[]` + `open_scene`); shared helpers and `struct app` live in `clients/ui.[ch]`; each view group lives in `clients/screens_.c` (dashboard, vouchers, accounts, reports, rules, attachments, bank, audit, templates, ib, bokslut, settings, invoices). Pure move: no key, string or scene-name changes; the registry keeps every name including the `yearinfo` alias. 24. **Payroll (2026-09-21)**: `docs/PAYROLL.md` is the chapter. Schema v10 (v11 adds `employees.email`): `employees` with the personnummer encrypted at rest (`secret_encrypt`, masked in every response; only `payroll.agi` returns it in clear), `payroll_runs`/`payroll_run_lines`, and national `tax_tables`/`tax_table_meta` without `org_id` (the one exception to the tenant rule). Monthly runs post D salary (7210 default; 7010 for kollektivanställda) / D 7510 / K 2710 / K 1930 / K 2731 with `source:"payroll"`; `payroll.pay_tax` does D 2710 + D 2731 / K 1630. Tax tables are fetched from Skatteverket's public fixed-width `allmanna-tabeller-manad.txt` (one button, stale-year warning) and over-80k % records are stored but refused until the rule is settled. Lönebesked PDF + mail (wave 2). **User decisions**: only the owner is employed now but everything is multi-employee; no semester accrual; tax tables from day one; manual step-buttons (Bokför, Lönebesked, AGI-underlag, Betala skatt & avgifter); bank phase 3 (`bank_rule.*`) is dropped. 25. **Per-feature voucher series (2026-09-21)**: the single standardserie setting is replaced by one setting per feature that posts vouchers: `series_voucher` (manual vouchers and new templates, default `A`, falling back to the legacy `default_series`), `series_invoice` (`F`), `series_payroll` (`L`), `series_bokslut` (`Å`) and `series_ib` (`IB`). All are editable (Bolaget → Verifikationsserier), 1–8 characters with no control characters. The manual voucher form remembers the last series it posted as `series_voucher`. Reports and SIE treat both the configured `series_ib` and the historical `IB` series as ingående balans, so old books keep working. "Nästa fakturanummer" is editable in Bolaget → Fakturauppgifter (owner only). 26. **Invoicing follow-ups (2026-09-21)**: schema v12 adds `invoice_rows.is_text` and `invoices.paid_date`/`payment_voucher_id`. Invoices accept **text rows** (`"text": true`): description only, no amount, excluded from totals and the posting voucher; at least one priced row is required. The invoice detail gets `u = duplicera` (same customer, rows and references, dates reset to today, due = today + payment days) and `b = kvittera betalning`, which prefills the ordinary voucher form (D `bank_account`, K `invoice_receivable_account`, both editable, underlag attachable) and, after posting, calls `invoice.pay`, which requires the voucher to credit the receivable with exactly the invoice total. Partial payments are out of scope. Lists show `betald `. SMTP: `smtp_from`/`smtp_reply_to` are validated as e-mail addresses (settings.set and mail config), with the sender's display name taken from the org name. 27. **Document header (2026-09-22)**: the invoice header bar no longer draws the `MAKANDRA AB` Comfortaa outline; it prints the organization name (`orgs.name`) in Helvetica-Bold, scaled down and truncated with `...` if it would reach the `FAKTURA` wordmark, which stays. The shared setting `document_header_color` (`#rrggbb`, default `#314c59`) colours the header bar of **every** generated document — currently the invoice and the lönebesked; `settings.set` refuses other values and the renderers fall back to the default if a stored value is invalid. The lönebesked prints the employer name in its header too, so the `MAKANDRA AB` outlines are unused at runtime (kept in the generated header). Images remain out of scope. 28. **TUI interaction model (2026-09-22, design)**: settled in the UX session; the spec is `TUI-GUIDELINES.md` "Interaction model" and is not implemented yet. Two focus modes: the navigation keys (`Tab`, arrows, `Home`/`End`, `PgUp`/`PgDn`) only move focus/selection/scroll and never mutate data, while an active field keeps caret semantics. `Enter` activates the focused item only when it owns an action (menu item, action row, opening list row); a plain report has none, so `Enter` does nothing there — it never saves a whole form, deletes or posts. Entities have one of three lifecycles: **register** (explicit `Spara` after validation; drafts live in memory and in `$XDG_CACHE_HOME/bokf/drafts.json`, marked ``, deletable from the list and from the editor), **document** (the form is the draft, explicit post, immutable) and **settings** (explicit `Spara`; the per-field autosave is dropped). Every savable form ends with a `Spara` (or `Posta`) action row; commit is blocked while invalid and a server error keeps the draft. Screens declare actions in one ordered `struct tui_action` list that drives the `F2` menu, the accelerator keys and the footer hints; `F2` only, no `§`; `Ctrl+Enter` is dropped as a commit key (`F9` and the save action row remain). **Kunder pilot implemented 2026-09-22** (`clients/drafts.[ch]`, ``, the `Spara` row, the `F2` menu, delete from list and editor); the other screens follow. 29. **Context menu and web-safe keys (2026-09-23)**: `→` opens a context menu ("Åtgärder") where no caret uses the arrow, `^O` everywhere (also in table cells); it is a box at the right edge listing every action with its key, so it doubles as the hotkey list. Choosing an action feeds its key to the widget, so the menu and the accelerator share one path. All function keys are removed (F2 menu, F4 mall, F5 refresh/validate/preview, F7, F9 save/post) and so are `Ctrl+N` (now `n`), `Ctrl+Enter` and `Ctrl+F`; the extended keyboard protocols are no longer enabled. Reason: the TUI is to be served in a browser terminal for users outside the home network (the owner and Petter first, others later), and browsers own the F-keys and `Ctrl+N`/`T`/`W`. Supersedes the key parts of #11 and #28; `make check` rejects F-keys, `^N` and `^Enter` in `clients/`. 30. **Web frontend (2026-09-23)**: bokftui runs in the browser through ttyd in its own container (`bokf-web`), behind a login gate in C (`bokfweb`) that authenticates with bokfd's `session.open` — no second password store — and one login: the gate hands the bokfd session to the TUI through a terminal handle that only works with the login's cookie. Caddy in the container does the routing and `forward_auth`; TLS stays with the host's existing Caddy (port 443 was taken), which proxies `bokf.makandra.eu` to `127.0.0.1:8790`. Every terminal is an isolated process (private HOME, limits, `BOKF_WEB=1`: no local files or programs). bokfd's login limiter became per user name (it was one global counter, so any 5 wrong guesses locked out everybody) and the gate limits per client address. Audience: the owner and Petter first, prepared for more users. ## Completed work formerly listed under "Pending decisions" - Attachments are complete: download (voucher detail `f`, Underlag `Enter`, SHA-256 verified, text inline), attach to an existing voucher (`^F`), remove a link (`d` in the `f` picker) and link an inbox item to a voucher (`k`). - K2/SRU is complete: `sru.export` with the INK2 view, `bokslut.post` with the Bokslut screen, and a K2 årsredovisning text draft with a save action in Rapporter. The draft follows the filed reports (whole kronor, no account numbers, säte, förvaltningsberättelse with the org's static verksamhetsbeskrivning, flerårsöversikt and changes-in-equity from the books, resultatdisposition, styrelsens yttrande, fastställelseintyg, board signatures) and needs no prompting: everything comes from the books, the org record (Företagsuppgifter) and the per-year year booklet now edited on the **Bokslut** screen (`fiscal_year.update`, schema v6: events, AGM and payment dates, proposed dividend, employees, notes — inherited from the previous year when a year is opened; fields 0–5 save per field, the periodiseringsfond/tax rate fields are local). Board members are a list per org (`board.*`, edited under Företagsuppgifter) and the stämma decision is posted with a `Utdelning` template (D 2099/K 2898). Imported history years are read with their "Stäng" closings skipped, so comparisons and the flerårsöversikt show real figures. The moms `report_rules` are now owner-editable in the **Momsregler** screen (decision 18). ## Completed backlog items (original entries) 1. ~~eSKD file generation for momsdeklaration.~~ `report.vat_eskd` (eSKDUpload 6.0, ISO-8859-1, whole kronor) with a save action in the TUI momsrapport. 2. ~~Bokslut automation~~ done: `bokslut.post` (entries/avskrivningar, periodiseringsfond, skatt at a given rate, resultatdisposition, dry-run plan, audited), the TUI Bokslut screen (fond/rate, F5 plan, ^Enter posts after confirmation) and the K2 resultatrapport order. 3. ~~K2 årsredovisning document~~ done as a text draft built in the TUI from `report.income_statement`/`report.balance_sheet` (Förvaltningsberättelse, K2 RR/BR with previous-year column and the result inside equity, noter, underskrifter); `s` saves `Årsredovisning <år>.txt`. Placeholders mark qualitative facts, and incomplete jämförelsetal for imported history years are flagged in the document. ~~SRU files (INK2/INK2R/INK2S)~~ done as `sru.export` (official 2025P4 field tables, BAS mapping, TUI save in Rapporter -> Inkomstdeklaration). 4. ~~`audit.verify` must also verify the **voucher** hash chain (today only the audit chain is verified).~~ Done: recomputes every org's voucher chain in posting order (SCHEMA.md §7.1), flags unbalanced vouchers as a backstop, and `full:true` re-hashes attachment content; result carries `vouchers_checked`, `unbalanced_vouchers`, `attachments_checked` and the first bad voucher/audit/attachment id. TUI Revision shows both counts and the bad ids. Fixed in schema v7: `attachments` now has `no_update`/`no_delete` triggers; `audit.verify full:true` still detects on-disk tampering. 5. ~~`report.general_ledger` and `report.voucher_list`~~ implemented (Huvudbok, Verifikationslista) with Kapitas-style TUI tables; the ledger API supports `accounts`/`from`/`to`, the list an optional `series`. 6. ~~`describe` argument schemas (currently name/summary/permission only).~~ Done: every command carries a `CMD_ARGS` type/required/default schema, the dispatcher validates before the handler and `describe` emits `args[]`. 7. ~~Pre-migration `VACUUM INTO` snapshot (promised in SCHEMA.md, not built).~~ Done: forward migrations snapshot to `backup/pre-migration-v-.db` first and abort if that fails. 8. ~~Docker image + compose (multi-arch amd64/arm64, GHCR) and systemd unit.~~ Done as a Dockerfile + `compose.yaml` (amd64/arm64 build stage) and `scripts/deploy.sh` over SSH; no registry and no systemd unit (the container is the unit). 10. ~~Bank import/reconciliation (CSV first).~~ Phase 1 done: SEB CSV import + matching against vouchers (schema v8, decision 19). Next: phase 2 prefill-from-transaction and `bank_rule.*`, then PSD2; invoicing/reskontra and AGI/payroll if employees. 11a. Imported history years whose SIE contains the source's P&L closings ("Stäng intäktskonton/kostnadskonton") net to zero in the income statement (Kapitas 2022-2026). **Decided 2026-09-19: no importer change.** Locked years stay locked and the source's closings stay in the books; the årsredovisning export flags incomplete jämförelsetal for those years and points to the previous year's annual report. **Amended 2026-09-22**: the income statement (and therefore INK2/SRU and the TUI resultatrapport) now skips the same "Stäng ..." vouchers when they are SIE-imported, so a year being declared shows its real figures; the balance sheet keeps them (the result sits in 2099 and must not be counted twice). The source's own #IB/#UB corrections that the vouchers do not reproduce (e.g. Makandra's FY 2022/2023 result transfer is 17 857.63 kr short of that year's P&L) remain a data divergence: the derived balance sheet is off by that amount until a correction is booked in the current year. 11. ~~SIE import only into an empty fiscal year; consider broader import.~~ Chronological multi-year import works (CRLF, `#RAR 0`, zero rows, `#IB` rule handled); each year must still target an empty fiscal year. Note: years whose source system kept corrected `#IB`/`#UB` that the vouchers do not reproduce (like the Kapitas 2022-2026 books) diverge from the source when imported as history; the latest year imports exactly.