Files
ScadaBridge/docs/deployment/deployment-records/2026-09-28-wonder-app-vd03-fe89363b.md
T
Joseph Doherty 5cc09b08ef deploy(vd03): b3021e7c — script file access live (AllowedRoots E:\ApiInstall\ScadaBridge\recipes); gate 31/31, rig 8/8
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).
2026-09-28 10:45:54 -04:00

176 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: <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`, 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 '<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**.