All documentation Download (Markdown) Technical

Dashboard · dashboard.pdhc.se

Technical manual


dashboard.pdhc — Technical Description

Purpose

Read-only visualisation dashboard over observations delivered to gateway.pdhc
from upstream providers. Serves an eligible-patient list, cohort time-activity
curves, and a per-patient dashboard. Part of the PDHC microservice suite.

Architecture

[provider.pdhc] ── push ──► [gateway.pdhc] ◄── GET Observation ── [dashboard.pdhc]
                                                                        │
                                                                 Postgres (9026)
                                                                        │
                                                                   Flask (9027)

Components

Data flow

  1. User clicks "Refresh from gateway" → POST /refresh.
  2. Server iterates OrgMembership rows for current user, calls
    refresh_org(user, org) for each.
  3. refresh_org fetches bundle → normalises → replaces ObservationCache
    rows for that org → writes RefreshLog with count + status.
  4. Landing page reads cache, filtered by scope_to_user_orgs.

Environment variables (.env)

ObservationCache retention (PDL Ch 4 §§ 3-4, ticket #213)

ObservationCache rows are a derived, time-bounded copy of patient
observations the dashboard pulled from gateway. They must not survive
indefinitely — the source of truth (gateway, CDRs) may block, scrub,
or update a row, and a stale dashboard copy would defeat both
right-to-block (PDL Ch 4 § 4) and need-to-know (PDL Ch 4 § 1).

The retention policy has two parts:

  1. Time-based sweep. Rows whose fetched_at is older than
    OBSERVATION_CACHE_TTL_HOURS (default 48h) are removed by the
    flask cache-sweep CLI. Run hourly from cron on the macmini:
    0 * * * * cd /usr/local/www/dashboard.pdhc/current \ && docker exec dashboard_pdhc_app flask cache-sweep \ >> shared/logs/cache_sweep.log 2>&1
    flask cache-sweep --dry-run reports the count that would be
    removed without touching the table — useful when tuning TTL.

  2. Targeted admin scrub. SU admins can immediately drop rows
    matching patient_guid / org_guid / both via
    POST /admin/cache/scrub:
    json { "patient_guid": "...", // optional "org_guid": "...", // optional; at least one required "reason": "patient deletion request" }
    The endpoint writes a dashboard_audit row with
    event_type='cache_scrub', admin_justification=<reason>,
    n_rows_returned=<deleted_count>, and a payload_snapshot JSONB
    carrying the verbatim filter + count. Non-SU callers get 403; an
    empty filter returns 400 (a no-filter scrub would wipe everything).

Audit consumers (/admin/audit, #215) can join cache_scrub
rows with the same patient's read / admin_override rows to
reconstruct who saw what before it was scrubbed.

API keys (Rule 8)

Endpoints

Path Method Purpose
/healthz GET liveness + current AUTH_MODE
/metadata GET FHIR R5 CapabilityStatement
/ GET landing page (eligible patients + optional cohort curves)
/patient/<guid> GET patient dashboard
/refresh POST pull observations from gateway for user's orgs
/api/v1/series?patient&concept GET FHIR Bundle of Observations
/admin/cache/scrub POST SU-only: drop ObservationCache rows by patient/org (#213)
/admin/audit GET SU-only: browse dashboard_audit rows with filters + pagination (#215)
/admin/audit/export.csv GET SU-only: CSV export of the filtered set (cap 50000 rows) (#215)

Running locally

./start.sh

Tests

app/.venv/bin/python -m pytest app/tests
./scripts/test_api.sh   # exercises live endpoints against a running instance