# 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`) 1. Baseline over HTTP from the Mac: `/health/ready` 200/200; `debug snapshot` Z28061(1) Bad 126 / Good 144, Z28062(2) Bad 124 / Good 146, Z28061Sim(8) Bad 28 / Good 242 (270 attrs, 7 alarms each). 2. `sb-prep.ps1` (09:58:25): zip SHA-256 re-verified on the box, unpacked, staged Host/CLI ProductVersion = `fe89363b`; `reg export` of 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 CHECKSUM` valid; DB at 55 migrations. 3. `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), no `Host.exe` left; 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.** 4. `deploy artifacts` (last step per register row 126): `Dev Site` success; the site now reports `artifactRevisionHash 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) ```bash H=http://wonder-app-vd03.zmr.zimmer.com:8085/api; K='X-API-Key: ' 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`, and `AlarmFilter.IncludeAcked = false` returns nothing for this machine (it only bites on native mirrors). - `Description` falls 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_A` works), and strip a trailing `A`/`B` from the BTDB Code to find the instance (MesMoveOut's convention); `HierarchicalName` keeps the BTDB Code, as the legacy service did. - A machine without an `AlarmStatus` script (e.g. CvdReactor's Z28061) answers `Alarm status is not supported on machine ''`. ## 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() };`) 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**.