5cc09b08ef
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).
176 lines
13 KiB
Markdown
176 lines
13 KiB
Markdown
# 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**.
|