Skip to content

v0.17.0 — data safety, money correctness, and a clean container

Choose a tag to compare

@slimatic slimatic released this 21 Sep 09:43
· 273 commits to main since this release
8c8e9aa

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.amount while 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 NaN from a base64 length check; now parse raw and defer to authenticated decryption, failing loudly instead.
  • parseFloat was run on encrypted columns in six places — ~1 ciphertext in 6 starts with a digit, so it returned a small wrong number rather than NaN. All six now decrypt first.
  • Saved calculations record the rate used (fxRateUsed), and /rate-staleness flags 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 own en-US formatter now use the canonical one (Rp 50.000.000, not IDR 50,000,000.00). Fixed Arabic-Indic digits for SAR/EGP.

Export/import

  • CSV export wrote formatted values ($1,234.56) that parseFloat read as NaN; with a || 0 fallback 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 AppError statuses into a flat 500.
  • Backup/restore/session/audit/privacy endpoints returned hardcoded payloads (restore reported success, restoring nothing). They now read real data or return 501.
  • 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.ts from 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.json to 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