# bokf — project state Snapshot for resuming work in a new session. Read with `AGENTS.md` (rules) and `docs/TUI-GUIDELINES.md` (UI conventions). Dated 2026-09-19. ## Status Working self-hosted bookkeeping system, not production-proven. Backend ledger core is complete; filing/year-end/payroll are not. TUI is usable and exercised by `make test-pty` (golden screen-text scenarios); `make test` covers the server/protocol/ledger, the TUI widget unit tests and the docs consistency check. ## 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. ## 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). ## Backlog (prioritized, from COMPLIANCE.md §10 and the audit) 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). 9. Password change, user disable, TOTP. 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. 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. 12. TUI polish: horizontal scrolling in long text fields, bracketed paste. ## Environment / how to run - **Deployed**: `scripts/deploy.sh` (latest `v0.1.48`, healthy on nas). Live daemon `tls:bokf.makandra.eu:8788`, token `~/.config/bokf/migration-token` (scopes `read,write`; owner-only actions like closing years must be done by the human in the TUI). Git remote `origin` is `nas:/mnt/data/git-repos/bokf.git` (push `main` and tags). - **Local test rig** (transient, `/tmp`): daemon `./build/bokfd --db /tmp/opencode/bokf-local/t.db --socket /tmp/opencode/bokf-local/sock`, org 1, login `admin`/`testpass123`. Drive the TUI over a pty with `scripts/tui-sandbox.sh -- ./build/bokftui --socket /tmp/opencode/bokf-local/sock --org 1 --fy 1 ...` plus a small driver that feeds keys and an ANSI renderer (recreate if gone; arrows are `ESC O B/A`, Tab `\t`, `^X` `\x18`, `^Enter` `ESC[27;5;13~`, F5 `ESC[15~`). Never test against the live daemon. - Demo: db `~/bokf-demo/bokfd.db`, socket `~/bokf-demo/bokfd.sock`, pid file `~/bokf-demo/bokfd.pid`; login `admin` / `demo1234`. Start TUI: `cd ~/work/bokf && BOKFD_SOCKET=$HOME/bokf-demo/bokfd.sock \ BOKFD_USER=admin BOKFD_PASSWORD=demo1234 ./build/bokftui` - Restart daemon: kill the pid file's process, then `BOKFD_BACKUP_DIR=$HOME/bokf-demo/backup \ BOKFD_EXPORT_DIR=$HOME/bokf-demo/export setsid nohup \ ./build/bokfd --db $HOME/bokf-demo/bokfd.db \ --socket $HOME/bokf-demo/bokfd.sock > $HOME/bokf-demo/daemon.log 2>&1 &` - The user's own early instance was `/tmp/x.db` + `/tmp/bokfd.sock` (schema v1, old binary) — recreate or migrate it with the current build if it is still wanted. - TUI smoke tests: drive over a pty with `script -qec`; function-key escape sequences are timing-sensitive there (not an app bug). Arrows arrive as application-mode sequences (`ESC O B` for Down), not `ESC [ B`, because curses enables the keypad. `Ctrl+N/C/F` are single bytes and reliable. Always wrap the run in `scripts/tui-sandbox.sh -- ./build/bokftui ...`: it isolates `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` so a test can never overwrite the real `~/.config/bokf/tui.conf` or `~/.cache/bokf/tui.log`. ## Known caveats - Developer tooling (2026-09-20): `g_commands[]` carries declarative argument schemas (`CMD_ARGS`); `describe` emits them and the dispatcher validates before the handler runs. `make check` (part of `make test`) fails when a command or error code is missing from `PROTOCOL.md` (`scripts/check-consistency.sh`). `make test-asan`/`test-ubsan` build `test_core` with sanitizers; `make test-pty` runs `scripts/tui-golden.py` (dashboard, audit, vouchers) against a throwaway `/tmp` daemon. - Never commit unless the human asks. - SQLite files must not be backed up live with restic; use `backup.snapshot` (`VACUUM INTO`) and point restic at the snapshots. - Schema version is 9 (v3 moms rules; v4/v6 year info; v5 org description/shares + board members; v7 attachments append-only triggers; v8 bank reconciliation; v9 invoicing + widened `vouchers.source` with a table rebuild); forward migrations are in `db.c`. ## Makandra driftstatus (org 2) - **Org**: Makandra AB, org 2. Räkenskapsår (id): 2022=3, 2023=4, 2024=5, 2025=6, 2026=7, **2027=2 (öppet, aktuellt)**. Bokslut/AR/deklaration görs för det år som är valt i sessionen. - **FK2027**: importerade Kapitas-böcker + 28 bokförda verifikat (V21–V48) för bank/skatt maj–sep 2026, samt V49 som makulerar en dubblett (V20). 1930 stämmer mot banken utom **CDON 2 409 kr** (väntar på kvittots del 2–4; bokförs när det kommer). 1630 = 40 721 (exakt enligt Skatteverket). - **Underlag**: 279 attachment i org 2 (alla historikdokument + insamlade underlag). Bank-/SKV-utdrag ligger i `~/Makandra AB/{bank,skatteverket}` (Syncthing), källkorpus i `~/Downloads/Makandra AB-…/Bokföring/`. - **Stängning**: 2022–2026 ska stängas av ägaren via **Räkenskapsår** i TUI:n; låt FK2027 vara öppen till nästa bokslut. - **Deklaration**: FK2026 är deklarerad av revisorn. FK2027 deklareras våren 2027 (INK2/SRU via Bokslutshubben → Inkomstdeklaration). - **Årshäftet**: fylls i Bokslutshubben (händelser, stämma, utdelning + datum, medelantal, noter). OBS: `dividend_ore` för FK2027 kan vara ett testvärde (10 000) — kontrollera före AR/deklaration. - **Beslut/regler från bokföringsarbetet**: inga bokföringar utan godkännande; låsta år förblir låsta (rättelser görs i aktuellt år); SIE- importören ändras inte och importerad data "manipuleras" inte; historikårens P&L nettar noll pga källsystemets stängningar (AR hoppar över "Stäng"-verifikat i flerårsöversikten); utdelning bokförs vid stämman med mallen **Utdelning** (D 2099/K 2898); pappersoriginal finns i fysisk pärm (får refereras i efterhand, även i stängda år).