Second vd03 window of the day: no migrations, appsettings.Site.json gains ScriptFileAccess:AllowedRoots (pushed over SMB before the swap), sample recipes in E:\ApiInstall\ScadaBridge\recipes. The Delmia test receiver now reads + parses the downloaded file; real HTTP R100 -> 12 values loaded, R900 -> rejected with line numbers. verify-mes-test-machine.sh: D1's cleared check reads through BTDB, so it only runs with group C (the rig runs VERIFY_GROUPS=AD).
13 KiB
2026-09-28 — DEV (wonder-app-vd03) → fe89363b + in-memory MES/Delmia/alarm-API test machines
Build: main @ fe89363b (= origin/main; code tip 3a21cfc2), ProductVersion
1.0.0+fe89363b1a2dbc07c7578cd33c88b7162a1b19e7, published from a detached worktree at that SHA
(dotnet publish Host + CLI -c Release -r win-x64 --self-contained true -p:PublishSingleFile=false;
1029 app files, 223 cli files, zip 125 593 314 B, SHA-256 bb821df1…87167033).
Delta from the box's previous build 2b9a7c3c: 62 commits — MES-reportable computed-alarm flag
(IsMesReportable, Alarms.MesReportableAsync()), site artifact revision + pending-updates indicator,
Gitea #36 reset-aware handshake overload, Gitea #37 Flow Debugger structured method parameters.
QA (vq03/vq04) NOT touched — still 2b9a7c3c at migration 55 (register row 126).
Transport: Mac-side winrm CLI — WinRM 5985/NTLM for the two on-box scripts, winrm smb push
over 445 for the zip/SQL/scripts. First deploy under the 2026-09-28 deployment-only WinRM scope
(CLAUDE.md); every non-deploy read (baseline, logs, verification) went over HTTP or SMB.
Schema: 55 → 57 migrations (additive only)
| Migration | Effect |
|---|---|
20260911135501_AddAlarmMesReportable |
TemplateAlarms.IsMesReportable bit NOT NULL DEFAULT 0; InstanceAlarmOverrides.IsMesReportableOverride bit NULL |
20260914173147_AddSiteArtifactRevision |
Sites.ArtifactRevisionHash nvarchar(100) NULL; Sites.ArtifactsDeployedAt datetimeoffset NULL |
From→to script dotnet ef migrations script 20260909203046_EnforceGlobalInstanceNamesAndProtectProtocolConfiguration 20260914173147_AddSiteArtifactRevision (sb-mig-55to57.sql, NOT idempotent — reusable as-is for the QA
pair), applied with sqlcmd -C -b -I while both services were stopped. No data upgrade; the Central boot
logged no "Applying migration" (the script had already applied both).
Config
No appsettings change. The only new option in the delta, ScadaBridge:DeploymentManager:PendingUpdatesCacheTtl,
defaults to 60 s. The box's three appsettings*.json were copied out, restored after the swap and SHA-256-matched
(A6B6CF05… / 3382DD83… / 1A4333DA…). Service registry environment untouched.
Procedure (E:\psrem-stage\fe89363b\, log E:\ApiInstall\ScadaBridge\_DEPLOY-NOTES-20260928T095846.txt)
- Baseline over HTTP from the Mac:
/health/ready200/200;debug snapshotZ28061(1) Bad 126 / Good 144, Z28062(2) Bad 124 / Good 146, Z28061Sim(8) Bad 28 / Good 242 (270 attrs, 7 alarms each). sb-prep.ps1(09:58:25): zip SHA-256 re-verified on the box, unpacked, staged Host/CLI ProductVersion =fe89363b;reg exportof both service keys;BACKUP DATABASE [ScadaBridge] … COPY_ONLY, CHECKSUM, COMPRESSION→E:\ApiInstall\ScadaBridge-backups\ScadaBridge_pre-fe89363b_20260928T095825.bak(140 795 392 B),RESTORE VERIFYONLY … WITH CHECKSUMvalid; DB at 55 migrations.sb-swap.ps1(the 2026-09-10 script with the SHA and 55→57 step swapped in): 09:58:46 Site stopped (10 s), Central stopped (10 s), noHost.exeleft; 09:59:08 migrations 55 → 57;app/cli→*.bak.20260928T095846, staged copy in, appsettings restored (SHA match); 09:59:14 Central started → ready 09:59:30; Site started → ready 09:59:39. ~53 s total downtime.deploy artifacts(last step per register row 126):Dev Sitesuccess; the site now reportsartifactRevisionHash sha256:9e054ef0…,artifactsDeployedAt 2026-09-28T14:01:04Z.
Verification
| Check | Result |
|---|---|
:8085 / :8084 /health/ready |
200 Healthy / 200 Healthy |
| ProductVersion | live Host + CLI fe89363b |
| DB | 57 migrations |
| Snapshots after (settled) | Z28061 Bad 126 / Good 144, Z28062 Bad 124 / Good 146, Z28061Sim Bad 28 / Good 242 — identical to baseline (the first read ~20 s after start showed 20 tags Uncertain while subscriptions settled) |
| Logs after boot (pulled over SMB) | central + site: only the usual first-probe akka-cluster … member status: Removed ERR lines during startup; no other ERR/FTL |
In-memory MES / Delmia / alarm-API test machines (same session)
Seeded with docker/seed-mes-test-machine.sh (idempotent; second run reused every id and the gate
re-passed) and gated with docker/verify-mes-test-machine.sh — PASS 25/25. Nothing existing was
modified except the SimpleAlarmStatusRequest stub (below); CvdReactor and its instances are untouched.
| Object | Id | Notes |
|---|---|---|
Template TestMESReceiver |
28 | 26 STATIC attributes with the prod MESReceiver names; ready flags default true |
Template TestDelmiaReceiver |
29 | 10 STATIC attributes with the prod DelmiaReceiver names; ReadyFlag true |
Template TestMesMachine |
30 | standalone base (not CvdReactor-derived); composes LeftMESReceiver/RightMESReceiver/DelmiaReceiver; Call scripts MesMoveIn, MesMoveOut, ProcessRecipeDownload, AlarmStatus; simulators SimLeft/RightMoveIn, SimLeft/RightMoveOut, SimDelmiaRecipe |
Instance SBTEST01 |
9 | Dev Site, deployed |
Instance SBTEST02 |
10 | Dev Site, deployed; alarm override SensorFault → MES-reportable |
Inbound SimpleAlarmStatusRequest |
9 | was a stub returning [] — now real: SAPID → BTDB → AlarmStatus, flagged-only, acked included |
Inbound AlarmStatusRequest |
12 | new: legacy /mes/alarmstatus shape — MachineFilter (SAPID → Code → ZTag → MachineID, via BTDB) + AlarmFilter |
BTDB dbo.Machine |
203, 204 | SBTEST01 SAPID 990001, SBTEST02 SAPID 990002 (Code = Name = ZTag = instance name, Eastern Standard Time, LastUpdateBy = scadabridge-test-seed), inserted through a temporary inbound method (zzTmpBtdbSchema, id 11) that was deleted afterwards |
Alarms (static source → computed alarm): VacuumPressure → HighVacuumFault (HiLo hi 5 / hihi 10, 800,
MES), ArgonPressure → LowArgonPressure (lo 20 / lolo 10, 600, MES), FurnaceTemp → FurnaceOverTemp
(hi 1050 / hihi 1100, 900, MES), DoorOpen → DoorOpenAlarm (true, 300, not MES), SensorStatus →
SensorFault (−1, 700, not MES on the template; MES on SBTEST02 via override).
Simulator knobs (root static attributes): SimNextBatchID (5000, increments per move-in),
SimMoveInReject, SimMoveOutReject, SimRecipeReject, SimNoResponse (never answer → the 25 s timeout
path). Input-driven rejects: a work order containing FAIL; a Delmia path ending .bad or a blank work
order; a move-out with nothing moved in on that side. Ready flags on the receivers can be set false to hit
the "ready flag not set" paths.
How to test (real HTTP)
H=http://wonder-app-vd03.zmr.zimmer.com:8085/api; K='X-API-Key: <key with the method in scope>'
curl -s -X POST $H/MesMoveIn -H "$K" -H 'Content-Type: application/json' -d '{"SAPID":"990001_A","ContainerNumber":"CN-1","JobSequenceNumber":"10","WorkOrders":[{"WorkOrderNumber":"WO-1","PartNumber":"PN-1"}],"OperatorName":"me"}'
curl -s -X POST $H/MesMoveOut -H "$K" -H 'Content-Type: application/json' -d '{"SAPID":"990001_A","ContainerNumber":"CN-1","NumberWorkOrders":1,"WorkOrders":[{"WorkOrderNumber":"WO-1","PartNumber":"PN-1"}],"OperatorName":"me"}'
curl -s -X POST $H/DelmiaRecipeDownload -H "$K" -H 'Content-Type: application/json' -d '{"MachineCode":"SBTEST01","DownloadPath":"\\\\share\\r1.xml","WorkOrderNumber":"WO-1","PartNumber":"PN-1","JobStepNumber":"20","Username":"me"}'
curl -s -X POST $H/SimpleAlarmStatusRequest -H "$K" -H 'Content-Type: application/json' -d '{"SAPID":"990001"}'
curl -s -X POST $H/AlarmStatusRequest -H "$K" -H 'Content-Type: application/json' -d '{"MachineFilter":{"Code":"SBTEST01"},"AlarmFilter":{"MinSeverity":500}}'
_A/_B picks the Left/Right receiver, _LT makes it a leak-test move (MoveOut then returns the six LT*
rows instead of the 16 cycle rows). Trip an alarm without a key via the Flow Debugger (CLI
flow-debug run-value-change --instance-id 9 --attribute VacuumPressure --value 12, clear with 0.5), or
from the Central UI Debug View. The gate needs no key — it drives the methods through flow-debug run-method.
Known semantics (not bugs)
- Computed alarms are always
Acknowledged→StatusCode = "Triggered.Acked",AckDT = null, andAlarmFilter.IncludeAcked = falsereturns nothing for this machine (it only bites on native mirrors). Descriptionfalls back to the alarm name (computed alarms carry no message/type text).- The alarm methods accept an exact BTDB SAPID first, then its leading digits (so
990001_Aworks), and strip a trailingA/Bfrom the BTDB Code to find the instance (MesMoveOut's convention);HierarchicalNamekeeps the BTDB Code, as the legacy service did. - A machine without an
AlarmStatusscript (e.g. CvdReactor's Z28061) answersAlarm status is not supported on machine '<code>'.
Rollback
Upgrade: stop Site, stop Central; RESTORE DATABASE [ScadaBridge] FROM DISK = 'E:\ApiInstall\ScadaBridge-backups\ScadaBridge_pre-fe89363b_20260928T095825.bak' WITH REPLACE;
rename app → app.failed, app.bak.20260928T095846 → app, same for cli; start Central, then Site.
(The migrations are additive, so 2b9a7c3c would also run against the 57-migration DB — the restore just
makes it exact.) Test machines: instance delete 9/10, template delete 30/29/28, restore the stub body of
method 9 (return new { WasSuccessful = true, Alarms = Array.Empty<Object>() };) and delete method 12;
DELETE FROM dbo.Machine WHERE MachineID IN (203, 204) AND LastUpdateBy = N'scadabridge-test-seed'.
Staging left on the box under E:\psrem-stage\fe89363b\ (zip, unpacked tree, migration SQL, prep + swap
scripts, .reg exports). The _appsettings-keep-20260928T095846 copy and *.bak.20260928T095846
directories can go once the build has soaked.
Second window, same day — fe89363b → b3021e7c (script file access + recipe-parsing receiver)
Build: main @ b3021e7c (ProductVersion 1.0.0+b3021e7c5ff68ba20abc510f644045c7dbbccd86; zip
125 585 217 B, SHA-256 5b7d7c68…2d66df). Delta: the read-only, allow-listed Files script global
(design docs/plans/2026-09-28-script-file-access-design.md) and the TestMesMachine Delmia simulator now
reading + parsing the downloaded recipe file. No migrations (57 before and after — asserted, nothing applied).
Config (the only change): appsettings.Site.json gained
ScadaBridge:SiteRuntime:ScriptFileAccess:AllowedRoots = ["E:\\ApiInstall\\ScadaBridge\\recipes"] (+ a
_comment_SiteRuntime), edited on the Mac and pushed over SMB before the window so the swap script's
keep/restore carried it through (new SHA-256 3E7AD9A4…); original saved as
E:\psrem-stage\b3021e7c\appsettings.Site.json.pre-b3021e7c. The three sample recipes
(docker/recipes/SBTEST-R100.txt, SBTEST-R200.txt, SBTEST-R900-invalid.txt) were pushed to
E:\ApiInstall\ScadaBridge\recipes\. Central's appsettings unchanged.
Window: prep 10:41:47 (COPY_ONLY backup ScadaBridge_pre-b3021e7c_20260928T104147.bak, 140 064 768 B,
VERIFYONLY valid); swap 10:42:08 → site ready 10:42:57 (~50 s); the swap script asserted the recipes root
(3 files) and logged site AllowedRoots = E:\ApiInstall\ScadaBridge\recipes; boot validation accepted it.
Backups: app.bak.20260928T104207 / cli.bak.20260928T104207.
Re-seed + gate: seed-mes-test-machine.sh added the 14 recipe attributes to TestDelmiaReceiver and
updated all nine scripts; verify-mes-test-machine.sh (with RECIPE_ROOT='E:\ApiInstall\ScadaBridge\recipes',
RECIPE_OUTSIDE='E:\ApiInstall\ScadaBridge\app\appsettings.Site.json') PASS 31/31 — group A now proves
accept (12 typed values loaded), re-download, invalid-line rejection with line numbers and nothing loaded,
NotFound, and a file outside the root refused (OutsideAllowedRoots). Real HTTP with a key:
DelmiaRecipeDownload R100 -> {"Result":true,"ResultText":"Loaded 12 recipe values from SBTEST-R100.txt for WO-1001"}
DelmiaRecipeDownload R900 -> {"Result":false,"ResultText":"Recipe rejected: line 4: expected RecipeValueName,RecipeValue;
line 5: unknown recipe value 'MysterySetting'; line 6: PotTempSP expects Int32, got '980.5'"}
Recipe file format (RecipeValueName,RecipeValue per line; value = int / double (invariant .) / bool /
text; # comments and blank lines ignored; everything after the first comma is the value; surrounding double
quotes unwrapped). Loaded all-or-nothing into TestDelmiaReceiver's typed recipe tags (RecipeName,
RecipeRevision, CoatingType, ReactorTempSP, PotTempSP, ArgonFlowSP, HydrogenFlowSP,
ChlorineFlowSP, VacuumSP, TargetRuntime, PurgeEnabled, OperatorNote) plus RecipeValueCount /
RecipeLoadedFile. To test your own file: drop it in E:\ApiInstall\ScadaBridge\recipes\ (SMB) and call
DelmiaRecipeDownload with that full path.
Rollback: stop Site + Central, rename app → app.failed, app.bak.20260928T104207 → app (same for
cli), restore appsettings.Site.json.pre-b3021e7c if the key must go (the old build ignores it), start
Central then Site. No DB restore needed (no schema change).
Rig: docker/deploy.sh from b3021e7c (site-a both nodes mount docker/recipes read-only at
/app/recipes, AllowedRoots ["/app/recipes"]); seeded on site-a; VERIFY_GROUPS=AD PASS 8/8.