aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAnders Betts <anders.betts@gmail.com>2026-09-21 08:33:36 +0200
committerAnders Betts <anders.betts@gmail.com>2026-09-21 08:33:36 +0200
commitfcbfd1c23ed91082fcbb77a7337567a1b9cedd91 (patch)
tree8f0dbda537024c17aca1a6acaa7ebaaf0978551c
parentf290736c33a6df85cd83eaed18e80530681955b9 (diff)
docs: prune STATE into a decision archive; record the branch rule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
-rw-r--r--AGENTS.md20
-rw-r--r--docs/DECISIONS.md268
-rw-r--r--docs/STATE.md299
3 files changed, 317 insertions, 270 deletions
diff --git a/AGENTS.md b/AGENTS.md
index 0008428..5e206ee 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -69,8 +69,9 @@ User-visible server messages are English; the TUI is Swedish.
## TUI work
-Read `docs/STATE.md` first for status, locked decisions, pending decisions
-and the prioritized backlog. Then read `docs/TUI-GUIDELINES.md` and follow it. Summary: one shared line editor,
+Read `docs/STATE.md` first for status, open decisions and the prioritized
+backlog (settled decisions are archived in `docs/DECISIONS.md`; consult it
+only when you need the history behind a rule). Then read `docs/TUI-GUIDELINES.md` and follow it. Summary: one shared line editor,
universal keys (Ctrl+N new, F5 refresh, Esc/q back, digits jump, `g` goto),
forms behave the same everywhere, hints always visible, errors shown as
`CODE: message`. Screens call only the widget layer (`clients/tui.[ch]`:
@@ -95,8 +96,10 @@ helpers and `struct app` are in `clients/ui.[ch]`; the scene registry
`test_core` in `build-asan`/`build-ubsan`. Run them after touching
allocation or arithmetic paths.
- Before you stop: `make gate` — a clean rebuild with `WERROR=1`, the full
- suite and `test-asan`. `sh scripts/install-hooks.sh` points
- `core.hooksPath` at `.githooks/` so the pre-push hook runs it automatically.
+ suite and `test-asan`, in its own `build-gate/` (it never touches
+ `build/`). Run `sh scripts/install-hooks.sh` once per checkout: it points
+ `core.hooksPath` at `.githooks/` so the pre-push hook runs `make gate`
+ automatically.
- Never run `bokftui` for tests without `scripts/tui-sandbox.sh`: it
isolates `XDG_CONFIG_HOME`/`XDG_CACHE_HOME` so a run cannot overwrite the
human's `~/.config/bokf/tui.conf`, `~/.cache/bokf/tui.log` or bw session.
@@ -104,3 +107,12 @@ helpers and `struct app` are in `clients/ui.[ch]`; the scene registry
`~/bokf-demo` or the live NAS volume.
- Never commit unless the human asks. When asked, keep the message short and
in the repo's style.
+
+## Parallel work
+
+Anything that touches more than one domain file goes on an `eff/*` (or
+`feat/*`) branch in a git worktree under `/tmp/opencode/wt-*`
+(`git worktree add /tmp/opencode/wt-<name> -b eff/<name> main`), never in
+the main checkout. Verify the branch with `make gate`; the lead merges it
+after review. Protocol and docs updates (`PROTOCOL.md`, `SCHEMA.md`,
+`STATE.md`) stay in the same branch as the code they describe.
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
new file mode 100644
index 0000000..b84e7fa
--- /dev/null
+++ b/docs/DECISIONS.md
@@ -0,0 +1,268 @@
+# 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<old>-<stamp>.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 <nr>`, 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_<domain>[]` tables live in
+ `src/cmd_<domain>.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_<group>.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.
+
+## 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<old>-<stamp>.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.
+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.
diff --git a/docs/STATE.md b/docs/STATE.md
index b6c5d4f..b7ed645 100644
--- a/docs/STATE.md
+++ b/docs/STATE.md
@@ -1,7 +1,7 @@
# 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.
+Settled decisions live in `docs/DECISIONS.md`; this file is the live snapshot.
+Read with `AGENTS.md` (rules) and `docs/TUI-GUIDELINES.md` (UI conventions).
## Status
@@ -11,270 +11,34 @@ 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
+## Open 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<old>-<stamp>.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 <nr>`, 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_<domain>[]` tables live in
- `src/cmd_<domain>.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_<group>.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).
+None open. Completed items that used to be listed here are archived in
+`docs/DECISIONS.md`.
## 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<old>-<stamp>.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).
+1. ~~eSKD file generation~~ done: `report.vat_eskd`.
+2. ~~Bokslut automation~~ done: `bokslut.post` + TUI Bokslut screen.
+3. ~~K2 årsredovisning draft and SRU files~~ done: TUI draft + `sru.export`.
+4. ~~`audit.verify` voucher chain~~ done (also attachments with `full:true`).
+5. ~~`report.general_ledger` / `report.voucher_list`~~ done.
+6. ~~`describe` argument schemas~~ done (`CMD_ARGS`).
+7. ~~Pre-migration `VACUUM INTO` snapshot~~ done.
+8. ~~Docker image + compose~~ done (no registry, no systemd 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.
+10. ~~Bank import/reconciliation (CSV first)~~ phase 1 done (SEB CSV +
+ matching, schema v8). Next: phase 2 prefill-from-transaction and
+ `bank_rule.*`, then PSD2; invoicing/reskontra and AGI/payroll if
+ employees.
+11a. Imported history years with source P&L closings: decided, no importer
+ change (see DECISIONS.md).
+11. ~~SIE import only into an empty fiscal year~~ chronological multi-year
+ import works; each year must still target an empty fiscal year.
12. TUI polish: horizontal scrolling in long text fields, bracketed paste.
+Original entries for the struck items are in `docs/DECISIONS.md`.
+
## Environment / how to run
- **Deployed**: `scripts/deploy.sh` (latest `v0.1.48`, healthy on nas).
@@ -314,13 +78,16 @@ check.
## 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.
+- Developer tooling: the `g_cmd_<domain>[]` tables in `src/cmd_*.c` carry
+ 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`) or when the generated
+ command catalogue is stale (`make gen-protocol`). `make test-asan`/
+ `test-ubsan` build `test_core` with sanitizers; `make test-pty` runs
+ `scripts/tui-golden.py` against a throwaway `/tmp` daemon; `make gate`
+ is the pre-push check (clean `-Werror` build in `build-gate/` + tests +
+ ASan).
- 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.