Status: AUTHORISED 2026-09-29 by Matthew, written the same day for OpenCode — then BUILT, DEPLOYED AND VERIFIED LIVE
the same day (feature/pharmacy-satellite-view @ d11cbd0; the acceptance record is §9 below). It sits ahead of
stage 3 in the supplier-portal programme and does not consume it: it is the mothership's own surface for the
satellite that stage 2
built. Read the scoping note for the security
footing, which this spec does not restate at length — every rule below is that footing expressed as UI behaviour.
The stage-2 satellite works, and the only way to use it today is a terminal: run a command, pass it a port, read six
lines of text. Matthew's instruction, 2026-09-29: "an admin user or the business manager in a pharmacy should not have
to use a terminal to access this feature — the controls and settings need to be made available in a dedicated view on the
app with appropriate controls."
So this spec is one view, plus the small things it needs to be honest: a status block (what is configured for
this pharmacy), the controls (on/off, the port, per-supplier portal settings — the last of which already exist and
are linked rather than duplicated), a download of the satellite itself, an instruction sheet written in plain
words, and the app-side record of what the app has sent. It changes nothing about how the satellite works: the
configuration still travels on the order page, and the program still finds our page by the signal it carries.
| Decision | Taken |
|---|---|
| Who may use the view | The pharmacy's Business Manager, and the platform administrator for support. Everyone else in the pharmacy is refused (403), exactly as the staff-user screen does. |
| Where it lives | The pharmacy stock section — /stock/satellite/ — with a row on the pharmacy dashboard and a link from the supplier list. (My recommendation; a few lines to move.) |
| The configuration stays on the page | Unchanged from the scoping note: the supplier's method, address and evidence are read from the order page by the satellite. The download therefore needs to carry nothing about this pharmacy — no address, no supplier, no portal detail. |
| The download is generated from the code that ships | The zip is built from the repository's own satellite/ directory at request time, so the file a pharmacy downloads can never drift from the satellite the platform was verified with. |
| Controls are records, not magic | The view records intent (on/off, the browser's debugging port) and shows it; it never starts, schedules or drives anything on the PC. |
| Appearance | "I am not too concerned about how these controls appear" (2026-09-29) — so this spec fixes the words and the states, not the layout. |
Deferred, and the view must say so rather than imply otherwise:
| Thing | What it is now |
|---|---|
| The signal | The order page renders #veripath-probe-signal — contract 2, carrying status, order, supplier, method, portal target, filename, last export time, reason and demo marker |
| The supplier's portal side | Supplier.portal_method / portal_target / portal_verified_on / portal_evidence, with the evidence rule enforced in Supplier.clean() and a DEMO marker when the configuration is synthetic |
| The satellite program | satellite/ in the repository — the standard library plus a websocket client, no Django import, no configuration of ours, read-only on the page, no portal, no credential. Standalone by design and its README says so |
| The record the app keeps | The order turns EXPORTED with who, when and how many times — the only app-side facts about a satellite run's subject |
| The role mixins | The app already gates screens by role (the staff-user screen is Business-Manager-only; the platform console uses PlatformAdminRequiredMixin) |
A new view at /stock/satellite/, rendered by a template in the app's own idiom (no Bootstrap, per house
convention), reachable from the pharmacy dashboard's button row and from the supplier list. Gated by the
app's Business-Manager-required mixin (the one the staff-user screen uses) or PlatformAdminRequiredMixin — nobody
else, and not anonymous. It adds no new URL that writes an order, touches a portal or exposes a feed.
Read from the supplier records — no new model:
NONE / FILE_DROP / GRID), the recorded address, the date it was seen,StockSettings, whichconfig.json. It is the only setting the package needs; there is no address in it.The one consequence, and it uses machinery that exists. When the switch is off, the order page's signal reads
waiting with a sixth fixed reason — "The order satellite is switched off for this pharmacy." — so the satellite
reports the app's own sentence rather than a puzzle, exactly as it does for the other reasons. The stage-1 spec's reason
list gains that sentence in the same change. With the switch on, the signal is byte-identical to today's.
A single zip, generated at request time from satellite/ in the repository (never a copy of it), containing:
signal.py, browser.py, downloads.py, report.py, __main__.py, __init__.py),requirements.txt, run-satellite.bat (the launcher, W5), README.md,config.json — the port and the version, and nothing else,READ-ME-FIRST.txt — the instruction sheet (W6), per tenant.Excluded: __pycache__, state.json, any fixture, and anything at all about the pharmacy, a supplier or a portal.
A test asserts the absence, by unzipping the response and searching every member for an address, a portal target, a
credential or a tenant hostname — the same rule the price-file samples already live under. The download is
authenticated and Business-Manager-only; anonymous gets a redirect, a technician a 403.
run-satellite.bat becomes the double-clickable route and does the whole job for the user:
--remote-debugging-port=<the recorded port> on a dedicatedIt must not install anything, register a service or an autostart, change the registry, open a portal, or run
anything unattended. It is one program, one browser and one window.
The same text in both places, generated per tenant:
run-satellite.bat → read the report → then log into the wholesaler's own portal andNo address, no credential and no terminal command appears in it — the port is the only number, and the launcher
already knows it.
Below the controls, the orders the app has exported (reference, supplier, when, how many times, the filename that
the signal names), newest first, with a one-line caveat:
"This list is what the app sent. The satellite does not report back to the app yet, so it cannot show what happened on
the PC — that is the next stage of this work."
--versionFour statements, each marked yes / no / cannot tell from what the app itself can see:
Plus python -m satellite --version, so a user can tell support which copy they hold, and the view names the same
version next to the download.
The platform console (/infrastructure/) can open the same view read-only, tenant by tenant, for support — the
mirror of the backend reference-data view. No setting may be changed from there in this spec; if it is wanted, it is
a one-line follow-up, not a silent capability.
Access: Business Manager 200; platform administrator 200; Responsible Pharmacist, Dispensing Technician and Counter
Assistant 403; anonymous redirected to login. The view appears on the dashboard's button row for the allowed roles
and not for the others.
The switch: off → the order page's signal reads waiting with the new sixth reason, and the satellite prints it
verbatim (the stage-2 harness covers this case already); on → the signal is identical to today's, asserted attribute by
attribute.
The download: authenticated and BM-only; the zip's members are exactly the expected set; every member is searched
for an address, a portal target, a credential and a tenant hostname, and none is present; the program files in the zip
are byte-identical to the repository's (so a stale copy cannot be served); the config.json carries the port and the
version and nothing else.
The record and the panel: the exported-orders list matches the orders the app has actually exported; the self-check
panel says "cannot tell" about a satellite run — asserted, so nobody later "improves" it into a false positive.
--version prints the version the view names.
The app side: no new write endpoint, no new public URL, the full pharmacy suite green, and satellite/ still
importable by nothing in Django (the existing isolation test stands).
tc.bm.one on the test client: the readiness block matches the supplier records (three DEMOconfig.json carrying only the port and version, and no address, portal target, credential or tenantwaiting with the new sentence when off,| # | Question | My recommendation |
|---|---|---|
| 1 | Is the view BM-only (plus the platform admin), or should the Dispensing Technician and Responsible Pharmacist be able to open it and download the satellite too? They are the people who will run it daily. | BM only in this spec, since the settings are commercial and the daily routine needs no settings. Widening it is a one-line change if the pilot wants it. |
| 2 | The on/off switch with the order page's new waiting reason — wanted, or is a switch without a consequence enough? | Wanted as specified: a control that changes nothing is worse than no control. |
| 3 | The download carries only the port and the version, because the satellite reads everything else off the page. | As specified — it is the security footing, not a preference. Confirm you are happy with it. |
| 4 | The package still needs Python on the PC until the packaging decision. Is a provisional package acceptable now, or should the download wait for the Windows pass? | Ship it provisional, labelled as such, so the view is usable today. |
| 5 | W8's dry run: a button that creates a DEMO test order for a first-time setup, or instructions only? | Instructions and the panel only for now — a button that writes orders into the pharmacy's list is the sort of thing that annoys an accountant. |
Status: accepted. Built by OpenCode from §4–§6, deployed to the app engine and verified live the same day by me,
with my own scripts and my own runs: 96 live checks passed (veripath-ops/scripts/verify-satellite-view-live.py),
plus 9 host-side checks for the platform administrator
(verify-satellite-view-admin-host.py), the expectations in both read read-only out of the tenant database
(recon-satellite-view.py) rather than assumed.
Revision and migration. feature/pharmacy-satellite-view @ d11cbd0, off the deployed 87cc874;
origin/dev fast-forwarded to it, and the deployed revision confirmed on the app host by me (git log -1 equal to
the local HEAD). One migration, applied to the tenant: pharmacy_stock.0011 — applied and read back, on
test-client-pharmacy only, nothing to the central database. Nothing else in the pass wrote to the app.
W1–W2 — the view and the status block. The Business Manager gets 200 on /stock/satellite/, the page renders in
a real browser with the right title, and the readiness line read "2 of 6 suppliers are configured — 4 still need a
portal method.", matching the tenant's own records (6 suppliers, 2 configured, both DEMO). Every row's name,
method display, recorded address, date-seen and DEMO marker was compared against the database row by row, and each
supplier links to its own edit screen (/stock/suppliers/<pk>/edit/). The page offers no password field of any kind,
and carries neither the tenant hostname nor the tenant slug, no credential, no 172.18. address and no terminal command.
The supplier's own recorded address is shown, as §5 allows.
W3 — the switch, both ways, on the live order page. Switched off: the exported order's signal became waiting
with reason = "The order satellite is switched off for this pharmacy." and every other attribute was unchanged; the
satellite program run against that off-state page printed "The app says this order is not ready: The order satellite
is switched off for this pharmacy." and exited 0. Switched on: the signal came back identical
attribute-for-attribute to the reading taken before the switch. The setting was left exactly as found (on, port
9222), verified from the database at the end of the pass.
W4 and W6 — the download, taken by a real browser. The zip was downloaded by headless Chrome (CDP download
behaviour, the session cookie set into the browser) — not only by an HTTP client — and the file on disk is byte-for-byte
the app's own authenticated response. 12 members, exactly the expected set and order; every packaged file
byte-identical to the repository's (10/10); no __pycache__, no state.json, no fixture; config.json is exactly
{"port": 9222, "version": "0.1.0"}. The archive was searched member by member: no supplier name, no portal target,
no address of ours or a portal's (the only URL anywhere is 127.0.0.1), the tenant hostname or slug, our own domain,
or the demo credential appears in it. The instruction sheet is the app's own generated text for this port, the same
text the view shows, and quotes all eight report states — each quote compared with the program's own literals in
satellite/report.py — with no address, no credential and no terminal command in it.
W5 — the launcher, end to end. The .bat's Linux equivalent (satellite-launcher-linux.sh: the same steps in the
same order) ran against a real export of ORD-20260921-898C9C: it found Chrome itself, read the port out of
config.json, started the browser on its own profile beside the program, and the program reported the real order,
supplier, method, filename and 3 lines, exit 0. With no browser installed it printed "No Chrome or Edge was
found on this PC." and exited 2. It opened no portal, sent nothing, and installed nothing.
W7–W8 — the record and the panel. The exported-orders list matched the database row for row (newest first, the
newest row's reference, supplier, count and filename all equal), with the caveat sentence verbatim. The self-check
panel's four statements matched the records — Yes — 2 supplier(s), the newest exported order named and linked, the live
filename quoted — and the fourth is exactly "cannot tell — the satellite does not report back yet".
Roles. Anonymous → the app's own /oidc/authenticate/ route → Keycloak, on both URLs. Live logins: the Counter
Assistant, Responsible Pharmacist and Dispensing Technician are 403 on the view and on the download; the satellite
link is on the dashboard and the supplier list for the Business Manager and absent for the technician.
W9 — deferred, as agreed, and one honest caveat. The platform administrator (a SYSTEM_ADMIN user) gets 200 on
the view with the "Support view — these settings can only be changed by the Business Manager." sentence, no settings
form and no download link, and both the save and the download are refused with 403; the settings were unchanged
afterwards. This is the one check in the pass that is not a live HTTP login: a pharmacy client host authenticates
against the tenant's own Keycloak realm, where no platform-administrator account exists, so there is no live login
for one on that host. The check drove the deployed code and the live tenant database through the app's own session
machinery with that user's session forced on, and it is recorded that way rather than described as a login.
Footprint. No order was created or cancelled; the only writes were the switch (off and back on) and one
re-export of ORD-20260921-898C9C per verifier run (its export count moved 14 → 15 → 16 over the two runs); no price
file was touched, no portal was contacted, and no credential was involved anywhere. The full pharmacy suite was
run by me on the deployed revision: 592 tests, OK (5 skipped), 350 s. The rig's own browsers (the verifier's and the
one the launcher started, both headless) were closed after the pass, and the working directories removed.
§8's five recommendations were built as written and stand as the rulings until he says otherwise: the view is
Business-Manager-plus-platform-admin, the switch carries a real consequence, the download holds the port and the version
and nothing else, the package ships provisional (labelled), and W8 offers the panel and the instructions rather than
a button that writes a test order. Each remains a one-line change if he wants it different.
One correction, not a defect. The build handback described the live tenant as holding "three DEMO configurations";
it holds two (6 suppliers, 2 configured, both DEMO). The view was right and the note was stale — recorded here so the
number is not carried forward.