1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
|
# 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.
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.
## 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`.
|