# 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 via pty smoke tests; the test suite (`make test`) covers the server/protocol/ledger only. ## 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. 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. `scripts/deploy.sh` builds locally and ships over SSH, or builds on the host when architectures differ. `scripts/deploy.sh --dev` cross-compiles the binaries here and hot-reloads the daemon (SIGHUP re-exec via `docker cp`), skipping the image build and container recreate. 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. 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. ## 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. Still missing before filing: a command/TUI to edit the moms `report_rules` per org (below). - Moms `report_rules` seed is a corrected starter mapping (schema v3), but there is still no command/TUI to edit rules per org; add one before filing if the mapping needs adjustments (SCHEMA.md §10 promises owner editing). ## 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). 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). 7. Pre-migration `VACUUM INTO` snapshot (promised in SCHEMA.md, not built). 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, then PSD2), invoicing/reskontra, 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 - 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 - 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 3; forward migrations are in `db.c`.