Repository navigation
Releases
v0.10.1
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 rotatewas 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_dataasks before seeding a live installation, andsetup_practice --no-inputrefuses once a practice exists.check_bank_duplicatesno longer prints IBANs. Admin badges escape their translated text.
Full changelog: docs/CHANGELOG.md
v0.10.0
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.pyanddocker-compose.prod.ymlfirst (./prod.py updateprints thecurlcommands), 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_backupsis removed;./prod.py backupprunes 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_KEYis unset../prod.pyalways generates one, so only a hand-built.envis 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, CSPframe-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_duplicatesnever deletes bank-linked records or records with receipts.seed_sample_data --clearonly touches its own demo data.
Fixes
- GDPR erasure now also removes the client's converted inquiry and imported calendar events.
clearbanktransactions --with-financialsdeletes only the records the selected transactions link to.
Backups, restore and 2FA
./prod.py updatebacks up before migrating (--skip-backupto 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_inquiriesruns 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
Upgrading
- One migration runs on start (
0008): adds the Lapsed inquiry status and ananonymised_atfield. Additive, nothing rewritten. - If you installed the systemd units, re-run
sudo bash scripts/install-system-jobs.shso 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-successbuttons 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
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
collectstaticaborted on it, so the container restarted forever after running its migrations. The reference is gone. A new guardrail runscollectstaticthrough 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_KEYnow also decrypts uploaded documents. Losing it loses those files too. Keep a copy of.envsomewhere safe.- Download the new
prod.pyanddocker-compose.prod.yml(./prod.py updateprints thecurllines). New settings only reach the container through the new compose file. - Port 8000 is now published on
127.0.0.1by default. Run the new./prod.py update(orstart) once: it addsMY_PRACTICE_BIND_ADDRESS=0.0.0.0to an existing.env, so a LAN installation stays reachable.
Full changelog: docs/CHANGELOG.md
v0.8.0
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_KEYnow also decrypts uploaded documents. Losing it loses those files too. Keep a copy of.envsomewhere safe.- Download the new
prod.pyanddocker-compose.prod.yml(./prod.py updateprints thecurllines). New settings only reach the container through the new compose file. - Port 8000 is now published on
127.0.0.1by default. Run the new./prod.py update(orstart) once: it addsMY_PRACTICE_BIND_ADDRESS=0.0.0.0to an existing.env, so a LAN installation stays reachable. Starting with plaindocker compose upskips 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-inlineorunsafe-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/restorewith 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 setupasks how the app will be reached and writes.envto 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_DISABLEDnow 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
Highlights
Security
- Open redirects closed: the practice switch and todo toggle trusted the path of the Referer header, but
https://evil//evil/xhas 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.ymlfixed (#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
uvicornpin 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
Patch release: four bug fixes, one UI merge and housekeeping.
Highlights
check_media --delete-orphansreported every client document as deletable (#471) —ClientDocumentwas 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_mediaand on any upload large enough for Ghostscript to shrink. gs applies/Rotaterather 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
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 --clearfixed (#438). It walks thePROTECTgraph 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_initialshares its name, so a plainmigratereads it as applied and runs only two idempotent data migrations. Schema equivalence verified on 678 facts. A database that has not reached migration0034should 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 --deployas gates, a release guardrail for version drift, and an ADR log indocs/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
Highlights
- Mood tags showed their storage key instead of a translated label.
SessionLog.mood_tagsstores raw keys in a JSON list, soget_FOO_display()was unavailable and four render sites printed the key directly —Hohe_Aktivierungin an English UI,Niedrig_Affektivin 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
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_detailview 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-XXXproject numbering rule written down after an audit found 63 skipped numbers (P-047)- Deps:
pypdf6.18.1,ruff0.16.7 (dev).pip-audit: 0 known vulnerabilities
Full detail: docs/CHANGELOG.md