Working implementation plan for turning the dental app (currently a copy of the GP booking app) into a high-street dental surgery platform. Built on the research in Dental App Research (1), Research (2) and Research (3).
Status (2026-08-02, closing): Client VPS onboarding is COMPLETE and live. This session resolved the onboarding follow-ups (code push, migration ergonomics, runbook updates), verified Keycloak full-flow against a new per-client realm, and planned the dental→ERPNext integration (build in a dedicated session). Remaining open items are below.
Resolved this session (2026-08-02):
✅ WireGuard → SSH-tunnel-only accepted as the standard. UDP 51820 was confirmed provider-filtered in both directions between the primary (88.208.212.211) and client VPS (194.164.18.33) via live packet tests (primary→client :55501 and client→primary :55502 both dropped). Decision: accept the SSH reverse tunnel (TCP 22) as the DB path; no further Fasthosts firewall work. Runbook updated accordingly.
✅ 4 uncommitted dental repo files reviewed, fixed, tested, and pushed to develop (dsp_clinic/settings.py, tenancy/routers.py, users/auth.py, users/oidc_views.py). Fixed the settings.py default ALLOWED_HOSTS typo (test-client-dental.veripath.co.uk → test-client-dental.dental.veripath.co.uk). manage.py check passes (1 pre-existing unrelated warning). Commit abab9231.
✅ Client-DB migration ergonomics fixed — new migrate_client management command (python manage.py migrate_client --slug <slug> [--plan|--fake]). Loads dynamic aliases then runs call_command('migrate', database='sector_client_{slug}'). Verified working against test-client-dental; the plain CLI migrate --database=sector_client_... still fails by design (alias not present at argparse time). Commit 0302aa3e.
✅ Data migrations made DB-aware — clinical_data.0010, users.0013, tenancy.0002, dashboards.0004 now detect a client-family target DB (sector_client / sector_client_*) and no-op, so no --fake needed on client DBs. Commit 0302aa3e.
✅ ERPNext runbook step corrected — dental does NOT run ERPNext (Stripe-based billing). The runbook Phase 4 ERPNext step now explicitly applies to GP only, with a "do not configure ERPNext for dental" note.
✅ Keycloak full-flow verification COMPLETE (2026-08-02) — full login → callback → post-login redirect → logout verified on test-client-dental.dental.veripath.co.uk. Two real defects found and fixed:
* in a redirect URI only matches a fixed prefix; https://*.dental.veripath.co.uk/oidc/* treats the host *.dental literally, so it never matched test-client-dental.dental.... Fix: added explicit https://test-client-dental.dental.veripath.co.uk/oidc/* (+ root path for post-logout) to the dental-booking-app client. The earlier assumption that the wildcard covered client subdomains was wrong.role/org_id user attributes never reached the app as OIDC claims (login created users with role ADMIN). Fix: added oidc-usermodel-attribute-mapper mappers for role and org_id (matching the GP client), and created the Keycloak user admin.test-client-dental with attributes role=DENTAL_ADMIN, org_id=test-client-dental. Verified the Django user now maps to DENTAL_ADMIN and lands on /dental/practices/ (200).⏳ DNS cleanup (open — needs user at IONOS/livedns) — *.dental.veripath.co.uk wildcard exists and covers all dental client subdomains. The leftover test-client-dental.veripath.co.uk A record is unused; remove it at the DNS provider when convenient.
✅ Per-client Keycloak realm test-client-dental created (2026-08-02, closing). The dental client had been wired to the shared veripath realm (unlike GP's test-client, which has its own realm). Fixed:
test-client-dental Keycloak realm with a dental-booking-app client (confidential, redirect URIs for test-client-dental.dental.veripath.co.uk, role + org_id oidc-usermodel-attribute-mappers, generated secret).role and org_id in the realm's user profile — Keycloak 26 drops undeclared user attributes by default (this was why earlier attribute updates silently failed on a fresh realm).admin.test-client-dental (attrs role=DENTAL_ADMIN, org_id=test-client-dental).PartnerOrg(test-client-dental).keycloak_realm='test-client-dental' + client_secret.ef5c8557 (users/auth.py, users/oidc_views.py): reordered auth/view checks so a client subdomain (partner org) resolves to its own realm before the generic dental/veripath fallback — mirrors GP.test-client-dental.dental.veripath.co.uk/oidc/authenticate/ now redirects to realms/test-client-dental, login → /dental/practices/ (HTTP 200) as DENTAL_ADMIN → logout back to the site root.✅ infrastructure/SIAAS/client_vps_setup page corrected — was describing WireGuard as the sole DB path + native PostgreSQL; now documents the SSH reverse tunnel as the standard, the client-postgres:ssh-v1.1.0 Docker container, TUNNEL_PORT + socat forward units, and flags the backup gap (/opt/backup-client.py + cron absent on the dental client VPS).
Dental→ERPNext integration — PLANNED (2026-08-02), build scheduled for a dedicated session:
healthcare_accounting app, ported Django integration. Full billing scope (transactions + FP17 two-invoice + seats).erpnext_client.py/invoice_service.py dropped from the GP template; billing is Stripe-native via DentalTransaction + DentalPaymentConfig).test-client.accounts.dental.veripath.co.uk, *.accounts.dental DNS wildcard.Tools: /opt/kcadmin.sh (Keycloak), /opt/wiki-manage.py (wiki), docker on primary VPS (88.208.212.211). DB shared postgres 3ff9c011e629_gp_booking_postgres; veripath_dental is the shared dental DB.
Live: https://test-client-dental.dental.veripath.co.uk → serves the dental app → redirects to Keycloak login (dental-booking-app client, test-client-dental realm).
Phase 0 — Primary VPS prep:
/opt/erpnext-client-db/entrypoint.sh via TUNNEL_PORT env (default 5435); rebuilt + pushed git.veripath.co.uk/infra/client-postgres:ssh-v1.1.0./root/.ssh/authorized_keys with permitlisten="127.0.0.1:5436" + 172.18.0.1:5436.dental-tunnel-forward.service (socat 172.18.0.1:5436 → 127.0.0.1:5436), enabled + running./root/cloud-init-test-client-dental.yml (new image tag + TUNNEL_PORT=5436).Client VPS (194.164.18.33) — provisioned manually over SSH:
10.0.0.0/24 → 5432).client_db container running client-postgres:ssh-v1.1.0 with CLIENT_DB=test_client_dental, TUNNEL_PORT=5436, --network host.127.0.0.1:5436 and Docker bridge 172.18.0.1:5436). Port 5436 keeps the GP test-client (5435) untouched.PartnerOrg(slug='test-client-dental') + client-sector Tenant (test_client_dental, db_host=172.18.0.1, port 5436).apps.ready (aliases never loaded), and dynamically-added aliases were missing Django's default connection keys.allow_migrate change: GLOBAL_APPS (auth, contenttypes, sessions, authtoken, tenancy, partner, etc.) now also migrate to client-family DBs — required because users (GP_APPS) extends AbstractUser and depends on auth. This matches the working GP client DB schema.clinical_data.0010, users.0013, tenancy.0002, dashboards.0004) faked on the client DB — they route ORM through the router to the shared DB and would corrupt data / fail. (Since 2026-08-02 these are now DB-aware and no longer need --fake.)Phase 3 — Web:
*.dental.veripath.co.uk → 88.208.212.211 (same pattern as the existing *.gp wildcard).test-client-dental.dental.veripath.co.uk.conf symlinked → proxies to 127.0.0.1:8010.test-client-dental.dental.veripath.co.uk to ALLOWED_HOSTS (both settings.py default and config/secrets/env.production); recreated container via docker compose up -d app.Keycloak / OIDC fixes (code):
_is_dental_request in users/auth.py only matched hosts starting with dental. — now matches *.dental.veripath.co.uk (and dental.veripath.co.uk). Dental client check now runs before the partner-org check (which was taking precedence with an empty realm).DENTAL_CLIENT_SECRET in users/auth.py to the current Keycloak value.users/oidc_views.py (request view + logout view) and get_oidc_logout_url in auth.py.client_id=dental-booking-app, realm test-client-dental, redirect https://test-client-dental.dental.veripath.co.uk/oidc/callback/. (The *.dental.veripath.co.uk wildcard does not match subdomains in Keycloak — an explicit per-host redirect URI was required; see handshake item 8.)Phase 4 — Seed data:
DENTAL_ADMIN user (admin@test-client-dental.co.uk, pw TempPass123!) + Test Client Dental Practice (TESTCD01) created in the client DB only.veripath_dental, 1 each in test_client_dental./infrastructure/health/ → 200.sector_client_test-client-dental.develop)dsp_clinic/settings.py — added test-client-dental.dental.veripath.co.uk to ALLOWED_HOSTS default (fixed from earlier missing .dental.).tenancy/routers.py — fixed _load_dynamic_aliases flag + default connection keys; allow_migrate allows GLOBAL_APPS on client-family DBs.users/auth.py — dental-host matching fix + secret update + logout ordering.users/oidc_views.py — dental-first client/realm selection.abab9231 (onboarding fixes) + 0302aa3e (migrate_client + DB-aware data migrations).Phases 1–7 complete on branch develop, pushed to Forgejo. Live at https://dental.veripath.co.uk (own containers dental_app, dental_celery_worker, dental_celery_beat, dental_redis on port 8010).
Client VPS support ported (from GP app) and verified:
{slug}.dental.veripath.co.ukDENTAL_APPS routes client-sector tenants to per-tenant DB aliases; allow_migrate permits client aliasesget_tenant_db_alias(tenant) replaces f"sector_{tenant.sector.slug}" in appointments/views_config.py + users/views.pyKey working components:
DRAFT → QUEUED → TRANSMITTED → ACCEPTED/REJECTED/PARTIAL with payload/response/diagnostic logging.dental/mock_gateway.py + NHSBSAMockLog): per-claim accept/reject simulation; client switches to live when USE_MOCK_NHSBSA=false.PeriodontalExam + ToothPerioRecord (6-point FDI 11–48/51–85, pocket/recession/BOP/plaque, mobility, furcation).DentalClinicalChart.DentalRecallRule, DentalPatientProfile, nightly Celery orchestrator → NHS Notify.PracticeSubscription + enforcement on DentalProvider.clean.poll_imap_lab_mailbox) replaces Microsoft Graph; encrypted creds; attachments → storage → pdfplumber extraction.DentalRadiograph + rule-based AI analysis hook (radiograph_ai.py).has_feature), NHS→private upgrade options (Band 2/3), legacy data import command (import_legacy_dental_data).weekly_pip_audit Celery task → LynisScanResult (26 vulns found on first run).dental_booking_app is a copy of gp_booking_app; git remote points at matthew/dental_booking_app.git (separate repo); own containers dental_*.dental Django module wired at dsp_clinic/urls.py → routes to sector_dental → veripath_dental DB.DentalPractice, DentalProvider (GDC), DentalAppointment (NHS band / private / AI triage), DentalClinicalNote (FDI tooth_map JSON), DentalTransaction + Stripe, FP17TreatmentCode, FP17Claim, DentalRecallRule, zone + schedule-block slot management, ~30 templates, REST API.settings.py (PDS_BASE_URL, GP_CONNECT_*, NHS_API_KEY, USE_MOCK_EPS) and services/nhs_notify.py.| Decision | Choice |
|---|---|
| Repo strategy | Separate repo matthew/dental_booking_app.git (existing, history kept) + separate deployment |
| NHSBSA strategy | Real client + payload builder + state machine now, behind USE_MOCK_NHSBSA env flag; flip to live URLs on accreditation |
| Mock depth | Interactive mock gateway (per-claim outcome control), not canned responses |
| Data isolation | Existing SectorDatabaseRouter: dental app → sector_dental → veripath_dental; client tenants → sector_client_{slug} |
| Sovereign rule | No Microsoft Graph / AWS dependency; Fasthosts IMAP + MinIO + Roundcube stack |
matthew/dental_booking_app); git remote set-url origin (history kept); branch develop pushed.dental_app, dental_celery_worker, dental_celery_beat, dental_redis; shared network to reach shared Postgres; port 8010; entrypoint fixed to exec CMD.dental.veripath.co.uk → 127.0.0.1:8010; TLS 1.3-only + security headers./infrastructure/health/ endpoint added; smoke-tested dental/ routes.dental → veripath_dental) ✅FP17Claim: QUEUED / TRANSMITTED states, transmitted_at, request_payload/response_payload JSON, diagnostic_log.periodontal.py): PeriodontalExam, ToothPerioRecord (FDI 11–48 / 51–85, 6-point pocket/recession/BOP/plaque JSON, mobility, furcation).is_valid_fdi), 5-surface (M/D/O/B/L) charting, DentalClinicalChart + surface_charting on notes.PracticeSubscription (purchased seats, billing anchor) + seat enforcement on provider creation.DentalRecallRule NICE risk intervals (3–24 mo); DentalPatientProfile (recall_due_date, is_recall_active).ClinicLabConfig extended with sovereign IMAP creds; LabDocument lifecycle exists.DentalRadiograph with minio_object_name + AI fields.0005–0007 applied to veripath_dental.dental/services/nhsbsa_client.py: env-driven (NHSBSA_BASE_URL, NHSBSA_API_KEY, NHSBSA_ODS_CODE, USE_MOCK_NHSBSA=true, mTLS cert/key paths); TLS 1.3-only, client-cert ready.mock_gateway.py) + NHSBSAMockLog; per-claim accept/reject/partial outcome control.build_fp17_payload): header (ODS + GDC), demographics, band, UDA, exemption, FDI matrix.DentalTransaction (split_co_payment).nightly_recall_orchestrator → process_practice_recalls (per-tenant, 30-day lookahead) → dispatch_recall_notification (NHS Notify SMS + booking link + audit log).reconcile_claim_payment (patient share / NHS reimbursable split).poll_imap_lab_mailbox (Fasthosts IMAP/SSL, unread w/ PDF/STL → storage → pdfplumber extraction + local AI match). Graph path removed from schedule.radiograph_ai.py) with agent escalation hook.get_private_upgrade_options, calculate_upgrade_split for Band 2/3).import_legacy_dental_data CSV/JSON, --verify rollback).has_feature on DentalPractice); sector routing already separates veripath_dental vs dsp_clinic.pip-audit Celery task → LynisScanResult (26 vulns on first run — remediation backlog).USE_MOCK_NHSBSA=false + set live creds on accreditation.PracticeSubscription.From client VPS onboarding — RESOLVED 2026-08-02 (WireGuard→SSH-tunnel accepted; 4 files committed; migrate_client + DB-aware data migrations; runbook updated; Keycloak full flow verified with wildcard-redirect + mapper fixes). Remaining open:
test-client-dental.veripath.co.uk A record (covered by *.dental wildcard) — needs IONOS/livedns access.dashboards/ role mixins (required_roles) only cover GP roles (CLINICIAN, RECEPTIONIST, PRACTICE_MANAGER, SIRO, SYSTEM_ADMIN). Dental roles (DENTAL_ADMIN, DENTAL_CLINICIAN, DENTAL_HYGIENIST, DENTAL_RECEPTIONIST, DENTAL_PRACTICE_MANAGER) are NOT in those lists, so /dashboard/* views return 403 for dental users. The dental/ module views work (they are @login_required only). Fix in a new session: map dental roles to the appropriate dashboards (add to required_roles / DashboardPermission), or define a dental dashboard role set.Existing deferred backlog:
pip-audit findings (upgrade cryptography, django, pyjwt, markdown, bleach).DentalRadiographaed01416).ENV.dental file consolidation (still reuses env.production).============================================================
============================================================
============================================================
============================================================
============================================================
============================================================
============================================================
Page: https://wiki.veripath.co.uk/development/projects/dental_app/dental_app_developer_plan