0.x releases
Every 0.x release, newest first. Other series: 1.x, 0.x; back to the current releases. Rendered from CHANGELOG.md, the file GitHub and the release tooling read.
0.5.0 — 2026-04-26
Added
- Health-check v2:
/healthendpoint now queries the database (SELECT 1) and Redis (PING) and reports per-component status; returns HTTP 200 with{"status":"ok"}when all healthy, HTTP 503 with{"status":"degraded"}on any failure; suitable for Kubernetes liveness and readiness probes - Structured JSON logging:
LOG_LEVEL(defaultINFO) andLOG_FORMAT(jsonortext, defaultjson) environment variables; JSON output viapython-json-logger; all uvicorn loggers reconfigured uniformly at startup - Slack / Teams webhook formatter:
NOTIFY_WEBHOOK_TYPE(generic,slack,teams) selects the payload format; Slack uses Block Kit (header + fields + action button); Teams uses Adaptive Cards (v1.4, FactSet + OpenUrl action); both new-report and SLA-reminder notifications respect the setting - SLA reminder system: background scheduler (
APScheduler, interval 30 min) firessend_sla_reminders(); checks all non-closed reports for approaching 7-day acknowledgement deadline (REMINDER_ACK_WARN_DAYS, default 2 days before expiry) and 3-month feedback deadline (REMINDER_FEEDBACK_WARN_DAYS, default 30 days before expiry); Redis dedup keys (reminder:ack:{case},reminder:feedback:{case}) with 1-hour TTL prevent duplicate notifications; enabled withREMINDER_ENABLED=true - S3-compatible attachment storage:
STORAGE_BACKEND=s3routes new attachments to an S3-compatible bucket (AWS S3, MinIO, Hetzner Object Storage) via boto3 (sync calls wrapped inasyncio.to_thread);S3_ENDPOINT_URL,S3_BUCKET_NAME,S3_ACCESS_KEY_ID,S3_SECRET_ACCESS_KEY,S3_REGION,S3_PREFIXconfigure the target; existing DB-backed attachments are unaffected (backward-compatible migration makesdatanullable, addsstorage_key VARCHAR(512)) - LDAP / Active Directory login:
LDAP_ENABLED=trueenables corporate directory authentication for admin accounts; two-phase bind (service account → user DN re-bind to verify password);ldap3runs synchronously in a thread pool; first LDAP login auto-provisions anAdminUserrecord; subsequent logins reuse the existing record;TOTPenrollment still required after first login;LDAP_SERVER,LDAP_PORT,LDAP_USE_SSL,LDAP_BIND_DN,LDAP_BIND_PASSWORD,LDAP_BASE_DN,LDAP_USER_FILTER,LDAP_ATTR_USERNAME,LDAP_ATTR_EMAILconfigure the connection - Helm chart:
charts/openwhistle/— production-grade Helm chart for Kubernetes deployments;Chart.yaml,values.yaml, 8 templates (deployment.yaml,service.yaml,ingress.yaml,hpa.yaml,configmap.yaml,secret.yaml,_helpers.tpl,NOTES.txt); all v0.5.0 env vars exposed as chart values; supports existing-secret pattern for credentials; liveness/readiness probes wire to/health - Ansible role:
ansible/roles/openwhistle/— official Ansible role for bare-metal / VM deployments (Debian/Ubuntu); installs Docker CE + Compose plugin; creates system useropenwhistle; renders.env,nginx.conf, anddocker-compose.ymlfrom Jinja2 templates; installs systemd service unit; optionally obtains TLS certificate via Certbot with auto-renewal hook;ansible/deploy.ymlexample playbook;vault.yml.examplesecrets template
Changed
app_versionbumped to0.5.0- Admin login page shows a badge when
LDAP_ENABLED=true admin_users.password_hashis now nullable (migration 011) — LDAP-only accounts have no local passwordattachments.datais now nullable (migration 010) — S3-backed attachments store onlystorage_key
Database migrations
010_s3_attachment_storage.py: makesattachments.datanullable; addsstorage_key VARCHAR(512)column toattachments011_ldap_auth.py: makesadmin_users.password_hashnullable; addsldap_username VARCHAR(255) UNIQUEcolumn toadmin_users
Dependencies added
python-json-logger>=3.2.0— structured JSON log formatterapscheduler>=3.11.0— background job scheduler for SLA remindersboto3>=1.38.0— AWS S3-compatible object storage clientldap3>=2.9.0— LDAP / Active Directory authentication
Tests
- Added
tests/test_v050.py— 68 tests covering all new v0.5.0 features - Fixed
conftest.py: addedadminroleto the enum drop list so re-runs don't fail with "type already exists" on migration 003 - Coverage maintained at ≥90% (90.36% with full test suite)
0.4.0 — 2026-04-26
Added
- Multi-step submission form: single-page
/submitreplaced with a guided 5–6 step wizard (mode → location → category → description → attachments → review); Redis session stores partial state undersubmission-session:{uuid}with a 2-hour TTL; back/next navigation throughout; progress indicator shows current step and total;ow-submission-sessioncookie - Anonymous vs. confidential mode (Step 1): whistleblowers choose anonymous (no personal data) or confidential (optional name, contact info, secure email); confidential data encrypted with Fernet symmetric encryption derived from
SECRET_KEY; decrypted only on the assigned admin's report detail view; newSUBMISSION_MODE_ENABLEDconfig toggle - Multi-location / branch selection (Step 2, conditional):
Locationmodel withid,name,code(unique),description,is_active,sort_order,created_at; location selector shown only when active locations exist; admin management at/admin/locations - Confidential fields on reports:
submission_mode(enum),location_id(FK),confidential_name(encrypted text),confidential_contact(encrypted text),secure_email(encrypted text) added toreportstable via migration 009; all nullable for zero-downtime deploy - Optional secure contact email: whistleblower can provide an anonymous email address in confidential mode; when admin posts a reply, a brief notification (no report content) is sent;
secure_emailnever appears in logs - HinSchG deadline display for whistleblowers: status page shows 7-day acknowledgement deadline with days remaining (or confirmed date) and 3-month feedback deadline with days left / pending acknowledgement indicator
- French language (fr):
app/locales/fr.jsonwith full French translations for all keys;fradded to supported languages inapp/i18n.py; language picker in nav bar shows English / Deutsch / Français dropdown - Location filter on admin dashboard: filter reports by location; location shown in report detail sidebar
- WCAG 2.1 AA accessibility improvements: skip-to-content link in
base.html;aria-labelon all nav elements;aria-current="page"on active nav links;aria-liveregions;role="alert"on errors;aria-requiredon required fields;aria-describedbyon hints;sr-onlyutility; visible focus indicators; language picker keyboard-accessible - New CSS components: submit progress indicator, mode-selection cards with
:has()focus handling, step-action row, review table, skip link, lang picker dropdown
Changed
app_versionbumped to0.4.0- Admin nav in all templates updated to include "Locations" link
- Demo seed creates two demo locations (HQ, Remote) and one confidential demo report
- PDF export includes submission mode, location, and confidential fields (secure email noted as "on file — not printed" for privacy)
add_admin_messageacceptsnotify_whistleblower=Trueto trigger async secure-email notification when a secure email is on fileget_reports_paginatedaccepts optionallocation_idfilter- Health endpoint now returns current
app_version
Fixed
- Language switcher now correctly handles French (
fr) in redirect allowlist
Migration
- Migration
009_locations_confidential.py: createslocationstable,submissionmodeenum, addslocation_id,submission_mode,confidential_name,confidential_contact,secure_emailtoreports
0.3.0 — 2026-04-26
Added
- RBAC — Role-Based Access Control:
AdminRoleenum withadminandcase_managerroles;require_role()FastAPI dependency factory; role shown in dashboard nav and report detail - Case assignment: admins can assign reports to any active staff member; "My Cases" filter tab on dashboard; assignee column in reports table
- Status workflow overhaul:
received → in_review → pending_feedback → closedreplaces the oldreceived → acknowledged → in_progress → closedflow;STATUS_TRANSITIONSdict enforces valid transitions server-side; only valid next-states shown in UI - 4-eyes deletion principle: report deletion now requires two different admins — one requests, a different one confirms; same-admin confirm returns HTTP 409
- Immutable audit log:
AuditLogmodel with 18AuditActionconstants; every admin action is recorded; exportable as CSV from/admin/audit-log; last 20 entries shown per report - Custom DB-driven categories:
ReportCategorymodel replaces hard-coded Python enum; category management page at/admin/categories; existing reports preserve category as string - Case linking:
CaseLinkmodel with normalization constraint (smaller UUID always inreport_id_a); link/unlink cases from report detail page - Internal notes:
AdminNotemodel — admin-only notes never shown to whistleblower; add notes from report detail page - PDF export: full case export via
/admin/reports/{id}/export.pdfusingfpdf2(pure Python, no system packages); includes SLA compliance section per HinSchG §17 - Admin user management: create, deactivate, reactivate, and change roles of admin users at
/admin/users; last-active-admin protection prevents lockout - Dashboard statistics:
/admin/statspage with status distribution bar charts, category breakdown, total count, and 7-day SLA compliance rate - Demo seed improvements: case manager demo user (
case_manager/demo); 4 demo reports covering all statuses; demo internal notes, case links, and audit entries - New admin navigation: persistent links to Stats, Categories, Users, Audit Log from all admin pages
Changed
- Report
categoryfield migrated from PostgreSQL enum toVARCHAR(64)— stored as plain string at submit time for history immutability (migration 006) acknowledged_report()now transitions toin_reviewinstead ofacknowledged- Status labels updated throughout UI and i18n files
Database migrations
003_roles_status_assignment.py— addsadminroleenum,role/is_activeto admin_users, addsin_review/pending_feedbackto reportstatus enum, migrates old values, addsassigned_to_idFK to reports004_audit_log.py— createsaudit_logtable005_admin_notes.py— createsadmin_notestable006_custom_categories.py— createsreport_categoriestable, seeds 7 defaults, migratesreports.categoryfrom enum to VARCHAR007_deletion_requests.py— createsdeletion_requeststable with UNIQUE(report_id)008_case_links.py— createscase_linkstable with normalization CHECK constraint
Tests
- Added
test_v030_services.py— 35 service-layer tests for new features - Added
test_v030_api.py— 25 API-level tests for new admin endpoints - Added
test_pdf_service.py— PDF generation tests - Updated existing tests to use new
ReportStatusvalues (in_review,pending_feedback)
0.2.2 — 2026-04-26
Changed
- Logo redesigned: new "Protected Signal" concept — navy shield with gradient depth, amber glow, and three-arc signal mark; consistent across app favicon, docs favicon, apple-touch-icon, and all inline SVG nav logos
- README trimmed to overview + quick start; full documentation lives exclusively at openwhistle.net/docs.html (single source of truth, no duplication)
- docs.html nav CSS aligned with index.html: SVG circle selector, border-color transition on theme-toggle hover, and light-mode stroke overrides for the logo
Fixed
- Quay.io image reference corrected to
quay.io/jp1337/openwhistleeverywhere
Tests
- Added 128 new test cases across auth, admin, reports, misc, and demo seed modules
- Coverage increased from ~75 % to 91 %
- Resolved all CI test failures caused by DEMO\_MODE=true and function-scoped event loop conflicts
- Extracted
_seed(db)helper fromdemo_seed.pyto enable direct session injection in tests
CI / CD
- Codecov integration: added
CODECOV_TOKENsecret and pinnedcodecov-action@v5 - GitHub org avatar (500×500) and repository social preview banner (1280×640) added under
docs/
0.2.1 — 2026-04-26
Fixed
- Case number generation now uses
MAX(case_number)instead ofCOUNT(*), preventing a previously-issued case number from being reused after a report is hard-deleted - Test isolation: orphaned report in
test_delete_report_only_removes_matching_sessionscaused aUniqueViolationErroron CI; the test now cleans up all created reports
Security
- Resolved 4 additional CodeQL code scanning alerts:
py/url-redirection(set-language endpoint): redirect target resolved via a static_NEXT_ALLOWLISTdict, severing any taint flow from user-supplied inputpy/cookie-injection(reply endpoint): session cookie always rotated to a freshsecrets.token_urlsafe()value on every reply, never derived from the inbound cookiepy/clear-text-logging×2 (reset_admin_password.py): replaced variable-based error messages with explicit if-chains where everyprint()argument is a string literal, eliminating any data-flow path from the password variable to a logging sink
0.2.0 — 2026-04-26
Added
- Admin session expiry warning: a non-intrusive banner appears 5 minutes before the session expires with a live countdown and a one-click "Extend Session" button that silently refreshes the JWT and Redis TTL without losing work (
GET /admin/session/ttl,POST /admin/session/refresh) - Admin dashboard pagination with configurable page size (10 / 25 / 50 / 100), server-side
- Admin dashboard column sorting (submitted date, case number, category, status)
- Admin dashboard status filtering with clickable stat cards
- File attachment support: whistleblowers can upload evidence files (PDF, images, Word, Excel, CSV, TXT — up to 10 MB each, 5 files per report); admins can download attachments from the report detail page
- Email and webhook notifications when a new report is submitted (
NOTIFY_EMAIL_*andNOTIFY_WEBHOOK_*environment variables) - CSRF Double-Submit Cookie protection extended to all whistleblower POST endpoints (
/submit,/status,/reply) scripts/reset_admin_password.py: interactive CLI to reset any admin user's password without direct database access; supports--list,--username,--password; enforces password strength requirements; does not touch the TOTP secret- HTML error page for form validation errors (422) instead of raw JSON API response
- Company branding:
BRAND_PRIMARY_COLOR,BRAND_SECONDARY_COLOR,BRAND_LOGO_URLenv vars allow organisations to customise the UI with their own colours and logo - OIDC Authorization Code Flow: admins can log in via any OpenID Connect provider when
OIDC_ENABLED=true(authlib 1.7+, state stored in Redis with 5-minute TTL) - Docker image cleanup workflow (GHCR, Docker Hub, Quay.io — runs weekly, retains 10 most recent
sha-tagged images per registry) edgeDocker tag published on every push tomainfor tracking the latest unreleased state- Complete UI redesign: "Trusted Institution" aesthetic (Sora + Nunito Sans typography, white navigation bar, institutional blue + teal accent palette, elevation shadows, rounded corners)
- Professional dark mode with warm blue-gray palette (
#111827) - Submit-page sidebar redesigned with brand-colour background and subtle radial gradient
- SSO button on admin login page (shown only when
OIDC_ENABLED=true) - GitHub Pages website deployed from
docs/directory
Fixed
- Whistleblower status-session Redis keys are now cleaned up immediately when a report is hard-deleted (previously persisted for up to 2 hours as orphaned entries)
- SLA "days remaining" dashboard column no longer renders a double unit (e.g. "89d Tage verbleibend")
- Session cookie deletion now passes matching security attributes (httponly, samesite, secure) so browsers reliably remove the cookie on logout
- Theme toggle button now inherits the correct body font instead of falling back to the system font
- Public forms no longer bypass browser
required-attribute validation (removednovalidatefrom/submitand/statusforms) - Empty reply content and oversized descriptions now return 422 with server-side length enforcement (previously validated by HTML attribute only, bypassable via direct HTTP requests)
Security
- All whistleblower-facing cookies now use
secure=not settings.demo_mode(was hardcodedFalse, meaning cookies were sent over HTTP even in production) - Server-side max-length validation added for report description (≤ 10 000 chars) and reply content (≤ 5 000 chars) — previously enforced by HTML
maxlengthattribute only - CSRF Double-Submit Cookie pattern extended to
/statusand/replywhistleblower endpoints
0.1.0 — 2026-04-21
Added
- Complete rewrite from C# ASP.NET Core to Python 3.14 / FastAPI
- Whistleblower report submission with category and description
- Two-factor whistleblower access: case number (OW-YYYY-NNNNN) + UUID4 secret PIN
- Bidirectional communication thread between whistleblower and reporting office (HinSchG §17)
- First-run setup wizard for admin account creation with TOTP enrollment
- Mandatory TOTP (RFC 6238) MFA for all administrator accounts
- Optional OIDC login for administrators (authlib 1.7+)
- HinSchG SLA tracking: 7-day acknowledgement deadline (§17 Abs. 1) and 3-month feedback deadline (§17 Abs. 2)
- IP anonymity: nginx configured with
access_log off, application never reads or stores IP addresses - IP leakage detection: admin dashboard warning when upstream proxies forward IP headers
- Redis-based bruteforce protection with no IP tracking (session-token-based rate limiting)
- Hard deletion of reports (DSGVO Art. 17 right to erasure)
- Demo mode with seed data (
DEMO_MODE=true) - Automatic database migration check on every startup (alembic upgrade head)
- DSGVO-compliant: all fonts and static assets self-hosted (Spectral, Source Serif 4, JetBrains Mono)
- Light / dark mode with localStorage persistence and CSS media query fallback
- Security headers: CSP, HSTS, X-Frame-Options, Referrer-Policy, Permissions-Policy
- Multi-registry Docker publishing: ghcr.io, Docker Hub, quay.io
- Image signing with Cosign
- GitHub Actions CI: mypy --strict, ruff, pytest with coverage, docker build
- HinSchG reference document (
docs/hinschg_reference.md) - PostgreSQL 18 + Redis 8 support
Technical Decisions
- Python 3.14 over Go/Rust: team familiarity with Python; mypy --strict provides compile-like type safety guarantees in CI
- FastAPI for async performance and Pydantic validation
- SQLAlchemy 2.0 async for type-safe database access
- Authlib 1.7.0+ required due to CVE-2026-28498 in earlier versions
- SSR with Jinja2 over SPA: simpler security model, no client-side secrets, works without JavaScript
- Session tokens in Redis for instant revocation without database lookups
- Rate limiting by session token (not IP) to maintain full anonymity
- alembic upgrade head on every startup to guarantee migration consistency