Status: SPEC v1.0, 2026-10-15 — raised on Matthew's decision this session (pharmacy app port is the next build). NOTHING BUILT YET. This is the pharmacy half of the Client VPS patching managed
service: the client's own pharmacy can see, and hand to an
inspector, the patching and maintenance carried out on its server. Model to copy: the dental port, which
is deployed and verified — the pharmacy app is the same fork.
Related: module-map, index,
staff-user-management (the users
half, already built), client guide.
The managed service patches each client's server; the client's need is the record of it, on a page of
their own, exportable as the evidence an auditor asks for. The GP and dental apps have that surface today
(card + run list + two CSV exports, gated to the client's own practice admin/manager). The pharmacy app has
no maintenance surface at all — a client pharmacy's Business Manager cannot see anything about their
server, which means the pharmacy tier cannot yet be presented as a managed service with evidence.
Probed inside the deployed pharmacy_app container, opening a session as the client tenant's own
Business Manager (tc.bm.one, role PHARMACY_BUSINESS_MANAGER, org_id=test-client-pharmacy) on the
client host:
| Path | Result | Meaning |
|---|---|---|
/pharmacy/staff/ |
200 | the pharmacy staff users screen works for the client's BM (spec) |
/partner/users/ ("Manage Users" in the nav) |
403 | the GP/dental-shaped nav item is dead for every pharmacy role |
/partner/audit_log/ ("Audit Log" in the nav) |
403 | same — dead for pharmacy roles |
/dashboard/ |
403 | practice-manager dashboard, not a pharmacy role |
/infrastructure/ |
403 | platform surface, correctly not a client's |
/infrastructure/my-maintenance/ |
404 | the client surface does not exist |
/infrastructure/maintenance/ |
404 | and neither does the platform maintenance list |
Also measured, and they are what the port has to deal with:
MaintenanceRecord, no MaintenanceCadence, noClientPatchJob, and no ingest endpoint (its infrastructure/urls.py has api/backup-heartbeat/ only).dsp_clinic database holdsinfrastructure_maintenancerecord, infrastructure_maintenancecadence, infrastructure_clientpatchjob,infrastructure_clientvps{,_tenants} — created by the GP app and shared by the dental app. The pharmacyinfrastructure app already reads the same infrastructure_clientvps table (its own migration0009), so this is a model-and-views port, not new tables.ClientVPS model is behind the table: it has no maintenance_token, no job_secret andtenants M2M, while the shared table carries all three. Declare the fields (and the M2M) to match thesource is the warning here (a model that does not match the table fails only on write).Pharmacy identities live centrally (users_customuser in dsp_clinic) with a role (PHARMACY_*) and an
org_id naming the tenant, and the logins live in the sector/client Keycloak realms. So the client's own
manager is already identifiable without building a users module:
| tenant | users (measured) |
|---|---|
test-client-pharmacy |
tc.bm.one PHARMACY_BUSINESS_MANAGER; tc.rp.one, tc.rp.two, tc.test.user PHARMACY_RESPONSIBLE_PHARMACIST; tc.tech.one PHARMACY_DISPENSING_TECHNICIAN; tc.ca.one PHARMACY_COUNTER_ASSISTANT — all with org_id=test-client-pharmacy |
The client's own admin/manager role for this app is PHARMACY_BUSINESS_MANAGER — the same standing as
PARTNER_REGISTERED_MANAGER in the GP app and DENTAL_PRACTICE_MANAGER in the dental app, and the role that
already holds the staff-users screen. That role plus the tenant's org_id is the whole gate.
MaintenanceRecord, MaintenanceCadence, ClientPatchJob declared against the existingsource), and the ClientVPS fields this fork ismaintenance_token, job_secret, tenants).client_maintenance_for(tenant) /client_maintenance_cadence_for(tenant), so every client read is scoped by the box(es) linked to their/infrastructure/my-maintenance/, and the two CSV exports (runs, and package detail) with thePHARMACY_BUSINESS_MANAGERorg_id matches the request's), refusing platform staff and every other pharmacy/partner/users/, 403) and/partner/audit_log/, 403) to pharmacy roles. Point the first at the pharmacy screen that/pharmacy/staff/) for PHARMACY_BUSINESS_MANAGER; hide the second until a pharmacy auditPharmacyAuditEvent is written but has no screen — out of scope here, and a dead link isrole + org_id. The gate must use theClientVPS.tenants does not exist here yet, and thetest-client-pharmacy andtest-client-dental run on one box (both databases, two reverse tunnels), and its history is one history./infrastructure/my-maintenance/date, finished, box, kind, trigger, outcome, applied, outstanding, reboot, summary, evidence_path; packages: date, box, package, from, to, security).PHARMACY_RESPONSIBLE_PHARMACIST, PHARMACY_DISPENSING_TECHNICIAN or PHARMACY_COUNTER_ASSISTANT is/infrastructure/vps/, /infrastructure/fasthosts/ and the platform maintenance list.MaintenanceCadence.next_run (from-hermes note 21, item 1). PharmacyMFA, a new users module (it exists), a pharmacy audit-log screen, and any change to the agent or the box
(its agent is already installed and posting).