Status: WORKING EXERCISE, opened 2026-09-30 at Matthew's request, and widened the same day at his steer to include the
satellite itself — "there is no point downloading a csv file until we have the satellite in place... it does form part
of the data journey". This page is the procedure we walk together, and it is expected to change: every step is either
a screen that exists today or a step plainly marked NOT BUILT. The proof of each attempt is stamped in the run log
at the end, not in a sentence here.
The goal, in his words: get the price data out of the portals and into the app, by satellite. Anything past that
point — what a discount means, what the prices then do — is further development, and not decided here.
This is the whole route, so it is plain where the work is:
| Stage | State | |
|---|---|---|
| 1 | the app knows which supplier, and that it has a portal | built — the supplier's portal record (step 1) |
| 2 | the satellite is installed on the PC | DONE — first real run 2026-09-30 on Matthew's PC (Linux), report Ready; steps 2–4. The PC is not Windows, so the package needs a Linux launcher (candidate for the packaging decision) |
| 3 | the satellite fetches the price file from the portal | NOT BUILT (inbound stages I1–I3) |
| 4 | the file comes out into the PC's Downloads folder | works by hand today (step 5); automatic in stage 3 |
| 5 | the file goes into the app, is mapped and recorded | built — step 6 |
So today the journey needs a human at stage 3. Stage 2 is not decoration: the same gadget that will fetch the file is
the one we have to get working first, it has never been run on a Windows PC at all, and that run owns a real decision
(the packaging). Steps 2–4 are therefore part of the exercise, not a footnote to it.
The exercise does not assume the satellite exists — it is not installed anywhere yet. These five things have to be true,
and this is where they stand, read out of the live system on 2026-09-30:
| What has to be true | Today |
|---|---|
| the app's satellite settings are recorded for this pharmacy (the switch, and the debugging port) | true — switch ON, port 9222 |
| the package is available to download from the app | true — version 0.1.0, from the Order satellite screen |
| the package is extracted on the PC that will run it, with Python present | true (2026-09-30) — extracted on Matthew's PC and run there; Python 3 and websocket-client present |
| the app has an exported order for the satellite to read | true — order ORD-20260921-898C9C (3 lines) is live and exported. But the file has to be in the PC's Downloads, which the export action is what produces — so export it once, on the PC |
| the satellite can fetch a price file from a portal | NOT BUILT (stage 3; step 7) |
So the order of the steps follows the first gap, not the first stage: get the gadget in place and running on the PC
(steps 2–4) before the file work means anything (steps 5–6). Step 1's supplier record can be done at any time — nothing
downstream depends on it yet, and the app is honest about the rest: its self-check panel cannot tell whether a satellite
has ever run on a PC, because the satellite does not report back to the app yet.
https.Order: this step can wait. It is stage 1 of the journey and nothing below depends on it yet — the satellite has no
portal address of its own and carries none. Do it when you want the app's own record to point at the real portal.
1a. The supplier's own portal record. Stock → Suppliers → Synthetic Wholesaler (DEMO) → edit. Four fields, all
required before the app calls a supplier ready: the portal method, the portal's own address as seen, the date it
was seen, and what was seen and by whom.
Read out of the live tenant on 2026-09-30, the four synthetic suppliers stand like this:
| Supplier | Method | Address on record | Seen |
|---|---|---|---|
| Synthetic Wholesaler (DEMO) | FILE_DROP |
https://portal1.veripath.co.uk/upload — recorded 2026-09-30 |
2026-09-30 |
| Synthetic Wholesaler Two (DEMO) | GRID |
a stand-in address on example.test |
2026-09-01 |
| Synthetic Wholesaler Three (DEMO) | none | — | — |
| Synthetic Wholesaler Four (DEMO) | none | — | — |
So the record was complete but its address predated the portals being hosted. Step 1a is done for
supplier One: Matthew recorded https://portal1.veripath.co.uk/upload with 2026-09-30 as the date seen, read
back out of the live tenant the same day — the record validates, method FILE_DROP ("Portal bulk upload (a file)"), and
it is still marked DEMO. Two notes on it, neither a blocker: the evidence sentence still reads "this portal does
not exist and is never contacted", which was true when it was written and is not any more — the portal is ours and it
does exist, so the line wants rewording (it must keep the DEMO marker at the front); and /upload is the rig's
POST endpoint, but a browser opening it lands on portal 1's sign-in page, so the recorded address behaves as an
address should. The other three suppliers are left alone on purpose — work is on portal 1 only for now.
1b. The satellite's own settings. Stock → Order satellite (/stock/satellite/). Already true on the live tenant,
read 2026-09-30: the switch is ON and the browser's debugging port is 9222. The same screen carries the status
block (which suppliers are configured), the orders the app has exported, the self-check panel — and the download,
which is step 2.
On the same screen, download the package — and download it on the PC that will run it, signed in as the Business Manager, or copy the zip across afterwards; the zip moves safely as a file. The download is a link on this screen, not an email or a separate portal. It is the app's own copy, generated when you ask for it, and the version is
named beside the download on the screen (today 0.1.0). What arrives is a zip whose folder is named satellite/:
__main__.py, signal.py, browser.py, downloads.py, origins.py, report.py,__init__.py), requirements.txt, README.md, VERSION;run-satellite.bat — the thing you double-click;config.json, generated for this pharmacy: the port and the version, and nothing else — no address of ours andREAD-ME-FIRST.txt, the instruction sheet, generated for this pharmacy, quoting the report's fixed sentences.Nothing about a supplier, a portal or a pharmacy's address is inside it — an automated test asserts that. Keep the whole
folder together wherever you extract it: the launcher steps out of satellite/ to run the program.
satellite/ to run the program, so keep satellite/browser-profile folder beside the.exe, no service, no autostart, no registry change — one folderrun-satellite.bat there hands it to Wine, which finds no Windows browser and noDouble-click run-satellite.bat. In order:
config.json (9222), on its own profile folder created besideThe signal it reads today is the order page's — the app's own note about an exported order. So the first run needs an
exported order to open (build and export one first: the C6 guide walks that). That is the honest state of the gadget:
one browser, one program, one window, read-only.
Read out of the live tenant on 2026-09-30, and corrected the same day: order ORD-20260921-898C9C (Synthetic Wholesaler (DEMO), 3 lines) is live and exported — last exported 2026-09-30 13:56, and it has been exported 21 times, all of them by verification runs. The other nine live orders are drafts never exported, and the cancelled ones do not count. So an order does not need to be built — exporting an existing one is enough, and the export is what puts the file into the PC's Downloads folder under the exact name the page then carries (synthetic-wholesaler-demo-order-ORD-20260921-898C9C.csv: the supplier's name, -order-, the order's reference, .csv). With no exported order page open in that browser, the report says "No VeriPath page is open in that browser" or the app's own not ready sentence — the gadget behaving, not failing. (An earlier line here said no order was both live and exported. That was wrong: the query had been sliced and missed this one.)
What the report says, and what it means. The sentences are fixed, and the instruction sheet quotes them:
| The report says | What it means |
|---|---|
| No browser is listening on the local debugging port | the launcher's browser is closed, or was started without the port |
| No VeriPath page is open in that browser | the app page is not open in that browser window (a different browser counts as none) |
| The VeriPath page is still loading | try again once it has finished |
| The page at <origin> is not a VeriPath page | the page is not ours — nothing has been read from it |
| The app says this order is not ready: <the app's own sentence> | the app's own reason (e.g. not exported) |
| The app named <file>, but it isn't in the Downloads folder yet | export it, or check the download completed |
| <file> is in Downloads but was written before this order was exported | the file is older than the order — not read |
| Already handled <file> for order <ref> | it has been read once already; nothing done |
| Ready: order <ref> for <supplier> by <method>. The file is <file> (n lines) | the whole point: the right file, for the order you think you are sending |
This run is the Windows pass, and it is the one thing about the satellite nobody has done: no machine here can
double-click a .bat. It settles whether the download can stay as it is or has to become a single file with nothing to
install. The steps and what to watch for are also on the
C8 guide.
There is no dedicated folder, and nothing to configure. The satellite reads the file from the PC's own Downloads
folder — C:\Users\<you>\Downloads on Windows (the program takes the standard user Downloads path; the launcher passes
no folder of its own). Three rules come with that:
The trap, and it is a Windows one: Downloads can be somewhere else. File Explorer's "Downloads" can be moved, and
OneDrive's folder backup very commonly redirects it to OneDrive\Downloads. The satellite looks at the user's own
Downloads path, so if the browser is saving somewhere else, the report will say the file "isn't in the Downloads folder
yet" while you can see it perfectly well. Before the first run, check where the browser actually saves
(Chrome/Edge → Settings → Downloads → Location) and make sure it is that same folder. This is one of the things the
Windows run is for, and it is worth noting as a finding if it happens.
And a gap in the package's own paperwork, recorded so it is fixed rather than rediscovered: the instruction sheet
inside the zip quotes every report sentence, but never says where the file must be. That belongs in the next version
of the sheet, in one line.
Today this is a human step, and it is repeatable on the other three portals:
Open https://portal1.veripath.co.uk/ and sign in with the account printed under its own form.
Use the download control. The file is synthetic-wholesaler-demo-price-list.csv, and its own header row is:
Product Code,Description,Pack Size,Trade Price,Discount %
So this file states a price and a discount, which is shape 1 of the two a price file can take
(the file contract).
Everything about it is invented: our own test portal, synthetic codes, synthetic prices, no real wholesaler.
Stock → Prices → Import a price file:
Discount % is offered asFound first, on purpose. The rig's files carried item codes of their own invention (AMOX500, AMOX250, BRUF400,
PARA500, SALB100, OMEP20, METO500, ATEN50). The app's stock items answer to entirely different ones — PIP
codes (0123456 Amoxicillin 500mg capsules, 2345678 Ibuprofen 400mg tablets, 5678901 Aciclovir 400mg, 6789012
Methylphenidate 10mg MR, 7890123 Brivaracetam 25mg, 8901234 Typhoid vaccine, 5670001 the synthetic four-supplier
item) or, where there is no PIP code, a barcode (5000124439750 Salbutamol 100mcg inhaler). The import matches the
code column against PIP code, then barcode — nothing else, and never fuzzily. So step 6 would have read the file,
mapped it cleanly, and matched nothing: 8 rows read, 0 matched, no price on any item.
Fixed the same day, on Matthew's go-ahead. The rig's catalogue now uses the app's own item codes — its PIP codes,
plus the barcode for the item that has no PIP code — with the same eight items and the same four different prices per
portal. veripath-ops c226b73 (source of record rig/synthetic-portal-rig/; the two verifier constants that named
the old first code were updated with it, and the rig's README now records the rule). The running rig was rebuilt and
reseeded from the versioned copy.
Proof, read out of the live system the same day: rig-side tests 11 OK; verify-synthetic-rig-live.py 88 PASS;
verify-synthetic-rig-browser-live.py 22 PASS; verify-rig-files-into-app-live.py 16 PASS. Then the point of the
change, run against the live tenant read-only with the app's own matcher over P1's and P3's real signed-in files:
P1 8 of 8 rows matched, P3 8 of 8 rows matched — including the leading-zero PIP (0123456 → the Amoxicillin item)
and the barcode-only item (5000124439750 → the Salbutamol item). Nothing was imported by that proof; the tenant's
price files are unchanged. The pictures on the test-environment page were re-captured and replaced, because they show the
portals' tables.
What this does not decide: the file now says the items exist, not what a discount means. Discount % still maps to
nothing (step 6, item 3), and that question stays app-side and open.
The satellite does not drive a portal today. It attaches to a browser you already have open, finds our page by the
signal it carries, takes the file the signal names, prints a report and stops. Making it fetch a price file — the thing
that would remove step 5 — is
Supplier Price Downloads I1–I3, and three of that
note's questions are still open and shape them: may the satellite drive our own page (I3 is the first stage that
writes into a page), the catch rule (exactly one new file appears; two is a refusal), and who sets the price date
when the file arrives automatically.
One thing to settle while building it, found on 2026-09-30. The four test portals sit inside veripath.co.uk, so
the satellite's origin allow-list — which allows our domain by suffix — accepts them. That is not a fault: they are ours.
But it means the origin alone cannot tell our app page from our test portal; the signal is what tells them apart. Worth
deciding deliberately when I2 is built rather than discovering it during the first drive.
Newest first. Each entry: date, what was done, what it showed.
2026-09-30 — the satellite's first real run on a real PC: READY. Matthew ran the program on his own PC against the
live app. The report, verbatim and unedited:
what Order ORD-20260921-898C9C — https://test-client-pharmacy.pharmacy.veripath.co.uk/stock/orders/1/ (read-only)
order ORD-20260921-898C9C · Synthetic Wholesaler (DEMO) · FILE_DROP · DEMO configuration
destination https://portal1.veripath.co.uk/upload (displayed, not opened)
file synthetic-wholesaler-demo-order-ORD-20260921-898C9C.csv · 38 bytes · 3 lines
state Ready: order ORD-20260921-898C9C for Synthetic Wholesaler (DEMO) by FILE_DROP. The file is
synthetic-wholesaler-demo-order-ORD-20260921-898C9C.csv (3 lines). Nothing has been opened and nothing
has been sent.
next Nothing has been sent anywhere. Nothing has been opened. This program does not contact portals yet.
That is the pass line, produced on the pharmacy side, not by a verifier — the gadget attached to a real browser, found
our page by its signal, read the order, found the exact file the app had named in Downloads, and stopped and said so.
Cross-checked afterwards, read-only, against the app's own record: the order was exported at 18:12 by
Bess Marginwell (tc.bm.one, Matthew's own Business Manager login), export count 23 — so the export was his, not
a verification run. The app would name the file synthetic-wholesaler-demo-order-ORD-20260921-898C9C.csv (the
profile writes no header row), the order carries 3 lines with codes 2345678 qty 32, 0123456 qty 21 and
5000124439750 qty 5 — and those three lines are exactly 38 bytes as int quantities, which is exactly the
38 bytes / 3 lines the report gave. So the file the satellite read is the file the app wrote, byte for byte. No
further export was made to check this — re-exporting would have made his local file stale by the program's own rule.
What it settles, and what it does not. Stage 2 — the skeleton, the signal, the exact-file rule and the report — is
now proved on the PC that will use it, and the packaging question has a real answer: that PC is Linux, and the
package ships only a Windows launcher (the Linux one is written and verified; the build item is in OpenCode's inbox).
What is not proved, and the report says so in its own last line: the satellite does not fetch anything from the
portal yet — stages 3–4 and I1–I3 — so step 5's manual download still carries the price file.
Two stumbles on the way, both now written down so they cost nothing next time: the browser must be started with the
trailing & (without it Chrome holds the terminal and the next command is silently never run), and the program is run
from the folder that contains satellite/, not from inside it. Both are on
The Satellite on a Linux PC — the Launcher.
2026-09-30 — the first attempt on the PC, and a real packaging finding: the PC is Linux and the package is
Windows-only. Matthew double-clicked run-satellite.bat; his desktop handed the file to Wine, which installed
itself, ran the batch file, and then — inside Wine there is no Windows Chrome or Edge and no Windows Python — printed its
message and closed when he pressed a key. Nothing was broken and nothing was sent anywhere: the package has no launcher
for this machine.
What I did about it. Wrote and verified a Linux launcher — the .bat's steps in the .bat's order: find a
browser (Chrome, Chromium, Edge, Brave or Vivaldi), read the port out of config.json, start that browser on its own
profile with the debugging port, bounded thirty-second wait, ask the user to open the order page, then run
python3 -m satellite --port … and keep the window open. It checks Python 3 and the one dependency up front and says what
to do instead of failing obscurely. veripath-ops 321624d; published, with the script and the quick
three-command route, on
The Satellite on a Linux PC — the Launcher.
Verified on the server against a real extracted copy of the package, with a real Chrome: the launcher starts the
browser on the port and waits for it; on a blank page it prints the "log in and open the order page" prompt, then the
program's own "No VeriPath page is open in that browser…" and "Nothing has been sent anywhere", exiting 3 (the
program's own no page code). An earlier pass of the same script, against a leftover verification browser that happened
to hold a saved order page, produced the full report — order reference, supplier, method, export filename — and the
app's own file is not in Downloads yet sentence, exiting 6. So both paths are exercised. What that does not
prove is the run on his desktop browser; that is his next step.
Also cleaned up while here: two headless Chrome processes from an earlier verification run were still holding port
9222 on the server (the first launcher test silently attached to one of them, and read a stale tab). Both were killed and
the port is free. Worth knowing generally: if something is already listening on 9222, the launcher's wait passes and
the program reads whatever browser is there.
For the record, and for the packaging decision: the C8 guide and the stage-2 spec both assume a Windows pharmacy
PC, and the package ships only the .bat. A Linux launcher, or a single file that needs nothing installed, is now a real
candidate for that decision — and the "does the PC have Python?" question is settled for this PC only once the launcher
has run there.
2026-09-30 — correction, and the next step settled. My earlier line to Matthew — "no order is both live and exported"
— was wrong, and it is corrected in the readiness table, in step 4 and in the pre-flight entry below: order
ORD-20260921-898C9C (Synthetic Wholesaler (DEMO), 3 lines) is live and exported, last exported 2026-09-30 13:56,
21 exports in total, every one of them from a verification run. The nine live orders that are not exported are all drafts,
and the cancelled ones do not count. The mistake was a sliced query: I asked for the live orders and printed eight of
nine.
So the next step is cheaper than building an order: open that order in the app and use Export. The export is what
puts the file in the PC's Downloads folder under the exact name the satellite will look for —
synthetic-wholesaler-demo-order-ORD-20260921-898C9C.csv — and the order page is then the page to open in the browser
the launcher starts (step 4). Building a fresh order (C6) does the same job and adds lines; either is a valid test of the
gadget.
2026-09-30 — step 6 done, and the result read back out of the live tenant. Matthew imported portal 1's price list.
It is not "seems fine" — it is recorded:
PARSED, 549 bytes, uploaded 17:37 by the Business Manager. The list went from 6 filesProduct Code, price Trade Price, description Description, pack Pack Size,PER_PACK — and Discount % unmapped, as it has to be: there is still no field to apply it with;5000124439750) — the leading-zero PIP code (0123456) matched too. Ten items now carry a price.So the manual half of the journey is proved end to end: portal → downloaded file → mapped → prices on items, with a
record of the file. The satellite took no part in it, which is exactly Matthew's own read: the gadget's test is the
outbound one — build an order, export it, then run the launcher and open that order page in the browser it starts.
That is step 4, and it is the next thing.
2026-09-30 — step 5 done: the price list is out of portal 1. Matthew downloaded
synthetic-wholesaler-demo-price-list.csv from portal 1 (as the portal's own sign-in page directs) — the headered
file with a price and a discount column, 8 data rows. I had pulled the same file signed in minutes earlier: HTTP
200, text/csv, 549 bytes, 9 lines, header
Product Code,Description,Pack Size,Trade Price,Discount %, every row carrying a 5.00 discount. So the file he holds is
the file the rig serves — step 5 is real, by hand, and repeatable on the other three portals.
Baseline taken before the import, read-only, so the change is provable: the tenant holds 6 price files; supplier
Synthetic Wholesaler (DEMO) holds two, both dated 2026-09-01 (one PARSED, one SUPERSEDED). So the import
should add one new file for the date he chooses and change nothing else — and if he leaves the date box at today
(2026-09-30) it will sit beside the 2026-09-01 pair rather than superseding it. What the 8 rows should do is settled:
8 of 8 match items (proved with the app's own matcher on 2026-09-30).
2026-09-30 — "does it need a dedicated folder for the downloads?" Answered from the program's own code: no folder to
set up. It reads the PC's standard Downloads folder, matches the exact filename the app names, refuses a file
that predates the order's export, and reads it locally (line and column counts only). The real Windows risk is that
Downloads is often redirected — OneDrive takes it over, or the browser is pointed at another folder — and then the
report says the file is not in Downloads while the user can see it. Step 4 now carries this, with the browser setting to
check before the first run. Also recorded: the package's instruction sheet never says where the file must be — a
one-line gap for the next version of the sheet.
2026-09-30 — the package is on the PC. Matthew downloaded the zip and can see the satellite/ folder with its files,
kept for now on his Desktop. Nothing has been run yet: extraction, Python and the first run are still to come, and the
first run is the one that needs an exported order (below). The extraction rules now sit in step 3 — Extract All, keep
satellite/ inside a folder, and expect a browser-profile folder to appear beside it on the first run.
2026-09-30 — step 1a done for portal 1, and the download pre-flighted. Matthew recorded portal 1's address on the
Synthetic Wholesaler (DEMO) record (https://portal1.veripath.co.uk/upload, seen 2026-09-30), which is stage 1 of
the journey. Nothing depends on it yet, and the evidence sentence wants rewording (above).
I then checked the download myself, signed in as the Business Manager on the live tenant: the satellite screen answers
200 and names the port and version; Download returns HTTP 200, application/zip, veripath-satellite-0.1.0.zip,
16,504 bytes, 13 members — the seven program files, requirements.txt, run-satellite.bat, README.md,
VERSION, config.json and READ-ME-FIRST.txt — and config.json reads {"port": 9222, "version": "0.1.0"}.
Nothing about a supplier, a portal or an address is in it. The package is therefore downloadable today; nothing is
installed anywhere yet, and no order is exported.
Two things decide whether that download is useful, and neither is in the app: Python on the PC (step 3 — if it is
not there, that is the packaging finding) and an exported order for the first run to read (step 4 — corrected 2026-09-30: ORD-20260921-898C9C is live and exported, so exporting it once on the PC is enough; the nine drafts need no work).
2026-09-30 (before the walk) — the codes. The rig's files matched none of the app's items, so the exercise could not
have reached its goal. The rig's catalogue now carries the app's own item codes, the running rig was rebuilt and
reseeded, and the app's own matcher was run read-only over P1's and P3's real signed-in files: 16 of 16 rows matched.
The rig's verifiers are green (11 / 88 / 22 / 16) and the pictures on the test-environment page are re-captured. Details
above, under The codes.
2026-09-30 (same session) — the exercise widened to include the satellite. Matthew's steer: the gadget has to be in
place before a CSV download means anything, and it is part of the data journey. Steps 2–4 now cover the download, the
install and the first run, and the journey table at the top says which stages exist. Nothing has been installed on his
PC yet; that is his, and it is the Windows pass.
(The walk itself has not started: the first entry for it goes in when we do steps 1–4 together.)