Skip to content

Releases

v0.10.1

Choose a tag to compare

@dholbach dholbach released this 03 Nov 08:54
ae406fa

Fixes only: sign-in attempt limits that hold under concurrent requests, key rotation that no longer undoes a concurrent edit, inquiries that stay anonymised, and calendar-import and session-log corrections.

Upgrading

Run ./prod.py update. There are no migrations, and prod.py and docker-compose.prod.yml only change their version string.

The hourly retention pass now also anonymises Onboarded inquiries that have no linked client once they are older than 180 days. To see what the first run will change, run ./prod.py manage tidy_inquiries first.

Highlights

  • Security: sign-in attempt limits hold under concurrent requests. Requests sent together could each read a failure count below the limit and each get a guess. Attempts against one account are now counted one at a time.
  • Key rotation could undo an edit (#559). A note saved while encryption rotate was running was overwritten with its older content. It is now left as saved.
  • Anonymised inquiries stay anonymised (#558). Their personal fields are read-only. Deleting a client by any path now anonymises the inquiry they came from. An inquiry can no longer be converted into a client twice, or after it was anonymised.
  • Calendar approval queue. "Create new invoice" makes one invoice per batch instead of one per event. Events that could not be imported stay in the queue, and the queue lists why.
  • Session log input. A non-numeric duration no longer causes a server error. Editing the log of an already-billed session no longer changes its duration.
  • Admin bulk actions Mark as sent and Mark as cancelled now clear a stale payment date (#557).
  • Hardening. Media rewrites are atomic, and one practice's calendar error no longer stops the other practices from syncing. Operator commands guard real installations: seed_sample_data asks before seeding a live installation, and setup_practice --no-input refuses once a practice exists. check_bank_duplicates no longer prints IBANs. Admin badges escape their translated text.

Full changelog: docs/CHANGELOG.md

v0.10.0

Choose a tag to compare

@dholbach dholbach released this 30 Oct 07:46
90dd67e

A security release from four source reviews (views and settings, bank import, practice scoping, admin and operator tooling), plus safer backups, restores and updates.

Upgrading

  • Download the new prod.py and docker-compose.prod.yml first (./prod.py update prints the curl commands), then run ./prod.py update. It now takes a backup before restarting.
  • Three migrations run on start (0009–0011). All are validation or key-widening only; existing rows already satisfy them.
  • The backup directory is mounted read-only into the app container. manage.py prune_old_backups is removed; ./prod.py backup prunes after every verified run.
  • The hourly scheduled jobs now run the inquiry retention pass. Preview what the first run will change with ./prod.py manage tidy_inquiries.
  • With DEBUG off, the app refuses to start if DJANGO_SECRET_KEY is unset. ./prod.py always generates one, so only a hand-built .env is affected.

Highlights

Security

  • The dev stack no longer listens on the local network.
  • Spelling a username with lookalike characters no longer gets around the login lockout.
  • POST-only state changes, ?next= same-host checks, CSP frame-ancestors 'none', Ghostscript -dSAFER, an upload extension allowlist on the model fields.
  • Bank import:
    • A sender-written payment reference can no longer abort a CSV import.
    • Imports are atomic, and a failing row is isolated from the rest.
    • Review actions are re-checked on the server.
    • A second payment from a different payer with the same reference is no longer dropped as a duplicate.
  • Practice isolation: fixes several latent cross-practice leaks in aggregates, revenue helpers and calendar suggestions. A new guardrail test flags unbound queries.
  • Calendar: the hourly sync no longer books sessions from invitations someone else sent.
  • Operator commands:
    • remove_financial_duplicates never deletes bank-linked records or records with receipts.
    • seed_sample_data --clear only touches its own demo data.

Fixes

  • GDPR erasure now also removes the client's converted inquiry and imported calendar events.
  • clearbanktransactions --with-financials deletes only the records the selected transactions link to.

Backups, restore and 2FA

  • ./prod.py update backs up before migrating (--skip-backup to opt out).
  • Restore is all-or-nothing:
    • both archives are verified first;
    • the database is restored in a single transaction;
    • media is staged and swapped only after unpacking succeeds;
    • all sessions are signed out afterwards.
  • The backup directory is kept at 0700. The encrypted-backup self-check no longer writes a plaintext copy.
  • Turning two-factor sign-in on or off, or running two_factor_reset, signs the user out of their other sessions.

Inquiries

  • tidy_inquiries runs hourly. Inquiries with no step for 60 days lapse, and closed non-client inquiries are anonymised 180 days after the last contact.

Full changelog: docs/CHANGELOG.md

v0.9.0

Choose a tag to compare

@dholbach dholbach released this 27 Oct 09:13
a710d37

Upgrading

  • One migration runs on start (0008): adds the Lapsed inquiry status and an anonymised_at field. Additive, nothing rewritten.
  • If you installed the systemd units, re-run sudo bash scripts/install-system-jobs.sh so the corrected unit files take effect.

Highlights

  • Fix — same-day sessions crashed the session log and invoice lookup (#537): with two sessions on one date (e.g. a cancel + rebook at the same slot), logging a session or attaching sessions to an invoice raised MultipleObjectsReturned. Both now pick the day's live session, preferring one not yet logged or billed.
  • Lapsed inquiries and manage.py tidy_inquiries (#534): open inquiries with no step for 60 days close as Lapsed (outcome unknown), and closed non-client inquiries are anonymised 180 days after the last contact, keeping the statistics. Dry run unless --apply; not scheduled yet.
  • Inquiry chart with marketing lanes and open-inquiry ageing (#533): every month of the last twelve, with marketing periods drawn above the bars, plus open inquiries ranked by days since their last step.
  • Fix — scheduled jobs no longer fire at boot before the app is up (#535), and the systemd installer no longer matches unrelated my-practice-* units.
  • Fix — inquiry row actions render as compact buttons; green .btn-success buttons render green (#524).
  • Deps: Pillow 13.0.0, python-dateutil 3.0.0, pypdf 6.21.2, pydyf 0.13.0, markdown 3.11.2.

Full changelog: docs/CHANGELOG.md

v0.8.1

Choose a tag to compare

@dholbach dholbach released this 14 Oct 20:54
9c19042

This release replaces v0.8.0, which could not start in production. If you are coming from v0.7.x, update straight to v0.8.1. Everything v0.8.0 introduced is included, see the v0.8.0 release notes.

Fixes

  • v0.8.0 container exited at startup (#523): the vendored Chart.js ended in a source-map reference to a file that was never vendored, and production collectstatic aborted on it, so the container restarted forever after running its migrations. The reference is gone. A new guardrail runs collectstatic through the production storage, so this class of failure is caught in CI.
  • Healthchecks and the release smoke test now check /accounts/login/. Since v0.8.0, /admin/login/ redirects there.

Already on v0.8.0?

The migrations ran before the container failed. Run ./prod.py update; nothing else is needed.

Coming from v0.7.x? Read before ./prod.py update

  • Back up first (./prod.py backup). Migrations encrypt existing client/inquiry notes, calendar credentials and every uploaded client document in place.
  • FERNET_KEY now also decrypts uploaded documents. Losing it loses those files too. Keep a copy of .env somewhere safe.
  • Download the new prod.py and docker-compose.prod.yml (./prod.py update prints the curl lines). New settings only reach the container through the new compose file.
  • Port 8000 is now published on 127.0.0.1 by default. Run the new ./prod.py update (or start) once: it adds MY_PRACTICE_BIND_ADDRESS=0.0.0.0 to an existing .env, so a LAN installation stays reachable.

Full changelog: docs/CHANGELOG.md

v0.8.0

Choose a tag to compare

@dholbach dholbach released this 14 Oct 20:29
e47bdce

A security and data-safety pass from an outside takeover review.

⚠️ Upgrading — read before ./prod.py update

  • Back up first (./prod.py backup). Three migrations encrypt existing client/inquiry notes, calendar credentials and every uploaded client document in place.
  • FERNET_KEY now also decrypts uploaded documents. Losing it loses those files too. Keep a copy of .env somewhere safe.
  • Download the new prod.py and docker-compose.prod.yml (./prod.py update prints the curl lines). New settings only reach the container through the new compose file.
  • Port 8000 is now published on 127.0.0.1 by default. Run the new ./prod.py update (or start) once: it adds MY_PRACTICE_BIND_ADDRESS=0.0.0.0 to an existing .env, so a LAN installation stays reachable. Starting with plain docker compose up skips that.

Security

  • Uploaded files are served only to signed-in members of the owning practice, never straight from disk (#490)
  • Client/inquiry notes, calendar credentials and uploaded client documents are encrypted at rest, with key rotation (manage.py encryption rotate) (#500, #501)
  • Two-factor sign-in (TOTP, recovery codes, optional REQUIRE_TWO_FACTOR), and the /admin/login/ bypass is closed (#504)
  • Sign-in attempt limit and an 8-hour idle timeout (#496)
  • Flash messages are escaped, and the CSP no longer allows unsafe-inline or unsafe-eval (#492, #505)
  • Practice scoping fails closed: a user without a practice sees nothing (#495)
  • The production port and the dev database no longer listen on every interface (#521, #499)

Features

  • Issued invoices are locked; mistakes are fixed with "Cancel & correct" (#491)
  • ./prod.py backup / restore with verified archives, optional gpg encryption, and a dashboard warning when backups stop (#489, #503)
  • Sign-out and two-factor sign-in in the account menu (#504, #507)
  • ./prod.py setup asks how the app will be reached and writes .env to match (#521)
  • Charts rebuilt on Chart.js: they follow dark mode and fix several axis bugs (#519)
  • New logo and favicon (#514)

Fixes

  • Your own time off no longer counts as client cancellations (#502)
  • Payment reminder amounts are formatted correctly, and money rounds half up as Decimal (#493, #515)
  • WCAG AA contrast fixes; theme and privacy mode apply before the first paint (#497)
  • The admin "add bank transaction" page no longer returns a 500 (#494)
  • Bank-import notes appear in the user's language (#514)
  • Pages no longer wait on the GitHub update check, and UPDATE_CHECK_DISABLED now works in production (#517, #520)

Under the hood

  • htmx replaced by a 60-line partial_post.js (#516)
  • ADR-0007 records why invoices are kept ten years (#510)
  • Dependency updates, including Django 6.1.2 and an npm fix for GHSA-vfj7-8cjw-p6xm (#487, #498)

Full changelog: docs/CHANGELOG.md

v0.7.2

Choose a tag to compare

@dholbach dholbach released this 07 Oct 05:28
444ee3e

Highlights

Security

  • Open redirects closed: the practice switch and todo toggle trusted the path of the Referer header, but https://evil//evil/x has the path //evil/x, a protocol-relative off-site redirect (#485).
  • Calendar import endpoints no longer return raw exception text to the browser (#485).
  • Path traversal hardened: client codes are slugified in document upload paths, and questionnaire codes from the URL must be lowercase slugs (#485).
  • CI supply chain: GitHub Actions pinned to commit SHAs, 3-day Dependabot cooldown, script injection in image.yml fixed (#484).

Fixes

  • 20-minute intro calls were billed, and calendar events were booked onto unrelated clients whose two-letter code happened to appear inside a word ("Nat", "Blocked"). Codes must now stand alone, and intro calls map to the correct service type again (#477).
  • The UI holds together down to a 768px window: button rows wrap and wide tables scroll inside their own container (#474).
  • The German treatment contract keeps its signature block together on one page (#475).

Chores & deps

  • Abandoned async spike and the uvicorn pin removed (#476).
  • cryptography 50.0.2, markdown 3.11, python-dotenv 1.2.4, google-auth family, source-map-js 1.2.2 (GHSA-68fv-2mgg-jv7q).

No migrations. Update with ./prod.py update.

Full changelog: docs/CHANGELOG.md

v0.7.1

Choose a tag to compare

@dholbach dholbach released this 23 Sep 10:56
b8e345b

Patch release: four bug fixes, one UI merge and housekeeping.

Highlights

  • check_media --delete-orphans reported every client document as deletable (#471) — ClientDocument was missing from a hand-maintained field list, so all stored client documents (signed contracts, intake forms, doctors' letters) were listed as orphans under a line offering to delete them. The field list is discovered from the app registry now: forgetting a model there produced deletion, not a gap in a report.
  • Scanned PDFs were rotated a second time on compression (#470) — a 90° page came out on its side, a 180° page upside down, on compress_media and on any upload large enough for Ghostscript to shrink. gs applies /Rotate rather than stripping it, so restoring the value afterwards turned every page again. The restore pass is gone, verified by rasterising; the test that passed throughout was replaced with a pixel-comparing one.
  • A session that only moved to a new time was cancelled and duplicated (#468) — editing a recurring series with "this and following events" makes Google mint a new event id, and the reschedule check compared only date and duration, so a dragged occurrence kept the wrong stored time indefinitely. Both fixed.
  • The Dokumente tab now lives on the Overview tab (#469) — beside the intake, contract and questionnaire sections it belongs with; a client's first upload no longer forces a page reload.
  • Assessments and Contract rows stay usable without an email address (#467) — instruments are named in the row rather than inside a send button, and the Contract step offers the PDF download the Intake step already did.

Also: dev containers no longer lose node_modules to the bind mount (#461), long files flagged by codebase_metrics --largest condensed (#462), legacy pre-i18n seed strings dropped from seed_sample_data --clear (#463), git permission allow rules tightened (#464), CODE_STRUCTURE.md corrected (#465), psycopg 3.3.6 (#466), metrics refreshed for 2026-09 (#472).

Full changelog: docs/CHANGELOG.md

v0.7.0

Choose a tag to compare

@dholbach dholbach released this 18 Sep 10:49
bc780e2

Highlights

  • Unmatched bank transactions are a Focus Queue task (#447, closes #422). One aggregate row per practice instead of a conditional fifth nav link — deliberately not one per transaction, since a per-row title would have to name the counterparty. The nav is honestly four links again.
  • Supervision feedback in the session timeline (#449). Supervision topics and their feedback sit in the Protokoll timeline by date next to the session logs they refer to; the separate hidden list is gone.
  • Update banner no longer offers an older release as an upgrade (#437). Release tags are compared as parsed (major, minor, patch) tuples rather than !=.
  • seed_sample_data --clear fixed (#438). It walks the PROTECT graph leaf-first, atomically, instead of dying half-way on todos the focus-queue sync had materialized.
  • Migrations reset: 35 files → 3 (#444). Self-hosters have nothing to do — the replacement 0001_initial shares its name, so a plain migrate reads it as applied and runs only two idempotent data migrations. Schema equivalence verified on 678 facts. A database that has not reached migration 0034 should upgrade via v0.6.1 first.
  • Bank review: ignoring an auto-expense now deletes the record it created (#453), matching what the withdrawal view already did. Expense and withdrawal review share one base class; the importer's parse and ingest halves are separated (#446) so a bank-API fetcher can reuse the matching pipeline.
  • Codebase metrics page in docs/development/ (#439, #442, #451), refreshed monthly with SVG charts and a long-file tracker.
  • CI/tooling (#454, #456, #458, #459): one hook mechanism, gitleaks over full history, Dependabot actually finding the Python manifests with patch updates auto-merging, requirements-file sync check, mypy and check --deploy as gates, a release guardrail for version drift, and an ADR log in docs/decisions/.

Deps: pypdf 6.19.0, urllib3 2.8.0, uvicorn 0.53.0. pip-audit reports 0 known vulnerabilities.

Full changelog: docs/CHANGELOG.md

v0.6.1

Choose a tag to compare

@dholbach dholbach released this 13 Sep 00:57
917a14b

Highlights

  • Mood tags showed their storage key instead of a translated label. SessionLog.mood_tags stores raw keys in a JSON list, so get_FOO_display() was unavailable and four render sites printed the key directly — Hohe_Aktivierung in an English UI, Niedrig_Affektiv in a German one. Both locales now read correctly, with no translation-catalogue changes needed.
  • Demo data (seed_sample_data) is English throughout. Session notes, session logs, intake and case notes, todo and time-off titles, tag names and expense descriptions were German and leaked into English screenshots. ICD-10 labels, the practice's regulated professional title and the GebüH fee schedule stay German on purpose — catalogue and statutory text, not UI strings.
  • Demo data now covers GebüH billing. The seeder previously had no GebüH data at all, so the invoice-detail breakdown and the client-detail diagnosis callout could not be exercised on a fresh install. Three clients now span the diagnosed, early-probationary and escalated states.

Upgrading

./prod.py update. If you have an older demo dataset, --clear still recognises the pre-translation German titles, so ./prod.py manage seed_sample_data --clear followed by a reseed picks up the new content cleanly.

Full changelog: docs/CHANGELOG.md

v0.6.0

Choose a tag to compare

@dholbach dholbach released this 12 Sep 22:51
b5811f2

Minor release — the command palette replaces the nav bar as the primary way around the app.

Highlights

Command palette (⌘K) — ⌘K / Ctrl+K / /, or the header 🔍 button, opens a filterable palette combining search (clients, open inquiries, invoices, with c:/i: prefixes), 21 jump-to destinations and 8 quick-create actions. The nav bar is now four links — Übersicht, Klienten, Rechnungen, Focus Queue — with the Invoices/Finances/Admin dropdowns folded into the palette. global-search.js is retired. (P-047, #420)

Search matched every row for any multi-word or hyphenated query. SearchRank(...) > 0 was used as the match predicate in three places and isn't one: ts_rank returns exactly 0.0 only for a single-lexeme miss, while any multi-lexeme miss scores 1e-20. Two words trigger it — and so does anything Postgres splits on a hyphen, which means every invoice number. (#423)

Revenue report opened on an empty page unless you picked a year from the dropdown every time. It now defaults to the current year, falling back to the most recent year with paid invoices. (#421, #425)

Privacy mode is now enforced in both directions. Five templates were still rendering client data in the clear — including every client's full name in each <option> of the calendar-import select, the one place a CSS blur could never have worked. A new guardrail test ratchets both under-blur and over-blur, the latter having previously hidden form labels and client codes that are themselves the privacy-safe representation. (#424, #426, #428)

Quick-add forms return to the page they were started from, year and filter intact — adding a tax prepayment from the quarterly overview, a tag from a client's tag picker, or a task from the Focus Queue. (#431)

Also in this release

  • Dead bank_transaction_detail view removed — it rendered a template that never existed, so the routed URL had been a 500 since v0.1.0; nothing linked to it (#429)
  • Focus-queue tests failed for two hours every night, comparing a Berlin date against a UTC one (#430)
  • Header controls moved up to the title row, freeing the nav row's full width (#427)
  • Practice logo/signature decode cached across PDF renders (#419)
  • CI discovers JS test suites by glob instead of a hand-maintained list (#420)
  • P-XXX project numbering rule written down after an audit found 63 skipped numbers (P-047)
  • Deps: pypdf 6.18.1, ruff 0.16.7 (dev). pip-audit: 0 known vulnerabilities

Full detail: docs/CHANGELOG.md