diff options
| author | Anders Betts <anders.betts@gmail.com> | 2026-09-21 08:33:36 +0200 |
|---|---|---|
| committer | Anders Betts <anders.betts@gmail.com> | 2026-09-21 08:33:36 +0200 |
| commit | fcbfd1c23ed91082fcbb77a7337567a1b9cedd91 (patch) | |
| tree | 8f0dbda537024c17aca1a6acaa7ebaaf0978551c /docs | |
| parent | f290736c33a6df85cd83eaed18e80530681955b9 (diff) | |
| download | bokf-fcbfd1c23ed91082fcbb77a7337567a1b9cedd91.tar.gz bokf-fcbfd1c23ed91082fcbb77a7337567a1b9cedd91.zip | |
docs: prune STATE into a decision archive; record the branch rule
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'docs')
| -rw-r--r-- | docs/DECISIONS.md | 268 | ||||
| -rw-r--r-- | docs/STATE.md | 299 |
2 files changed, 301 insertions, 266 deletions
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. |
