Repository navigation
v0.17.0 — data safety, money correctness, and a clean container
10 Rabi al-Thani 1448 — waxing gibbous, 74% lit
Trust in the numbers and the data: every figure displayed is one the app can justify, and
the upgrade path was tested against a real production database before tagging.
Data safety
- Backups are validated by content (WAL checkpoint, SHA-256,
PRAGMA integrity_check) instead of file size — an empty DB with live WAL sidecars passed the old check. - Startup re-encryption decrypts each rewritten value and compares it to the original before committing, and detects the wrong-key fail-open case that would double-encrypt a row.
- Fixed the migration skipping
payment_records.amountwhile reporting "no migration needed". Verified 10/10 values preserved on a production copy. - Added
scripts/ops/restore-backup.sh, an interactive restore with row-count checks.
Money correctness
- Exchange rates were hardcoded to 2023 values, understating non-USD wealth by 42% (EGP) and 77% (TRY) and misreporting nisab status. Now live, 1-hour cache, non-direct pairs solved through USD.
- Payment amounts under 1,000,000 returned
NaNfrom a base64 length check; now parse raw and defer to authenticated decryption, failing loudly instead. parseFloatwas run on encrypted columns in six places — ~1 ciphertext in 6 starts with a digit, so it returned a small wrong number rather thanNaN. All six now decrypt first.- Saved calculations record the rate used (
fxRateUsed), and/rate-stalenessflags drift ≥1% without rewriting history. - Amounts display in their recorded currency. An IDR asset no longer shows
$in its retirement preview, and 11 sites building their ownen-USformatter now use the canonical one (Rp 50.000.000, notIDR 50,000,000.00). Fixed Arabic-Indic digits for SAR/EGP.
Export/import
- CSV export wrote formatted values (
$1,234.56) thatparseFloatread asNaN; with a|| 0fallback every amount became zero on re-import. Export now writes raw numbers; import accepts every legacy format.
Security
- Containers no longer run as root. The app ran as uid 0; it now starts root only to chown the data volume, then hands off with
setpriv. Verified on a named volume and a bind mount. - Registration was gated in a route file the app never loaded, so disabling signups didn't disable them. Both paths now fail closed.
- Reconnected the commented-out error handler that had collapsed 54
AppErrorstatuses into a flat 500. - Backup/restore/session/audit/privacy endpoints returned hardcoded payloads (
restorereported success, restoring nothing). They now read real data or return501. - Removed invented data paths (
Math.random()"historical" nisab prices, a fabricated all-zero comparison); unreachable today, but a trap for the next developer. - Scrubbed personal emails, operator paths, production hostnames and a LAN IP from tracked docs.
Maintainability
- Removed 113 unreachable files (28,729 lines) found with
knip— 35 server, 78 client. - Split
server/src/routes/auth.tsfrom 1,251 lines to a 62-line facade over 7 modules, routes compared byte-for-byte. - Removed a dead payment subsystem, the legacy root
tests/directory, and an unimported duplicate auth directory. - Added
knip.jsonto both workspaces so this stays checkable.
Tests
- Suites: server 507 → 753, client 605 → 622; server coverage 25.6% → 38.8%, with CI gates added.
- New coverage for the calculation engine (2.9% → 62.4%), encryption round-trips, 11-currency precision, the backup verifier against real SQLite files, and both ciphertext formats.
- Replaced tests that mocked the code under test with ones that run it against real files.
Release cadence
docs/RELEASE-CADENCE.md: one release per Hijri month, aimed at a lunar anchor (crescent or waxing gibbous). Anchors are a preference, not a contract — a verified security or data-safety fix ships when it is ready.
Note: no breaking changes. Non-Latin currencies now group differently — IDR renders 1.500.000 where it previously showed 1,500,000.
Full Changelog: v0.16.8...v0.17.0