Claude-Session: https://claude.ai/code/session_013L3XdmVii9i5sY4FChU8pB
18 KiB
DelmiaRecipeDownload — ScadaBridge Inbound API spec (as deployed)
Captured: 2026-09-02 from the live prod management API on wonder-app-vd03 (method id 4,
API keys, the CvdReactor template's ProcessRecipeDownload script, the DelmiaReceiver template,
and the inbound audit log). QA (vq03/vq04) was NOT read — its management API requires real
LDAP credentials (prod runs with DisableLogin); re-capture QA with scadabridge api-method get --id …
once credentials are to hand. The definition is config-as-data in the central ConfigDb (not in git), so
this file is the durable record; the legacy predecessor is documented in
../former-api-specs/dnc/Delmia-Integration-API.md
(Surface B), and the client that calls it is the WWNotifier.exe drop-in
(DelmiaNotifier).
1. Endpoint
| Item | Value |
|---|---|
| Method / path | POST /api/DelmiaRecipeDownload |
| Auth | X-API-Key: sbk_<keyId>_<secret> or Authorization: Bearer sbk_<keyId>_<secret> (Authorization wins if both) |
| Content type | application/json, flat PascalCase object |
| Server timeout | 45 s (TimeoutSeconds on the method); the site-side handshake wait inside it is 25 s |
| Prod base URL | http://wonder-app-vd03.zmr.zimmer.com:8085 (single central node) |
| QA base URLs | http://10.220.158.2:8085 (vq03), http://10.220.158.3:8085 (vq04) — list both in the notifier's BaseUrls; a standby central relays to the active peer |
Unauthenticated / bad-key calls return 401 {"error":"Invalid or missing API key"} (audit kind
InboundAuthFailure). General Inbound API status semantics (413 body cap, 503 STANDBY_NODE /
ACTIVE_NODE_UNREACHABLE, unknown method) are in
Component-InboundAPI.md.
2. Request
Deployed ParameterDefinitions (JSON Schema, verbatim — note required is empty; the
script itself rejects a missing/blank MachineCode):
{"type":"object","properties":{
"MachineCode":{"type":"string"},
"DownloadPath":{"type":"string"},
"WorkOrderNumber":{"type":"string"},
"PartNumber":{"type":"string"},
"JobStepNumber":{"type":"string"},
"Username":{"type":"string"}},
"required":[]}
| Field | Notifier flag | Meaning | Where it lands on the site |
|---|---|---|---|
MachineCode |
-m |
Routing key — must equal an instance UniqueName exactly (trimmed). Prod today: Z28061, Z28062, Z28061Sim. |
Route.To(MachineCode) |
DownloadPath |
-d |
Path where the recipe/NC file was placed | DelmiaReceiver.DownloadPath |
WorkOrderNumber |
-w |
Work order | DelmiaReceiver.WorkOrderNumber |
PartNumber |
-p |
Part / item number | DelmiaReceiver.PartNumber |
JobStepNumber |
-s |
Job step / sequence op | DelmiaReceiver.JobStepNumber |
Username |
-u |
Operator (audit only) | DelmiaReceiver.Username |
Example (a real call taken from the prod audit log):
curl -s -X POST http://wonder-app-vd03.zmr.zimmer.com:8085/api/DelmiaRecipeDownload \
-H "X-API-Key: $SCADABRIDGE_API_KEY" -H "Content-Type: application/json" \
-d '{"MachineCode":"Z28061Sim","DownloadPath":"C:\\recipes\\test.nc","WorkOrderNumber":"WO-TEST-001","PartNumber":"PN-TEST","JobStepNumber":"0100","Username":"delmia-sim-test"}'
3. Response
Deployed ReturnDefinition: {"type":"object","properties":{"Result":{"type":"boolean"},"ResultText":{"type":"string"}},"required":["Result","ResultText"]}
HTTP 200 is returned for every business outcome — success is Result == true, never the status
code. ResultText values the deployed scripts can produce:
Result |
ResultText |
Produced by |
|---|---|---|
true |
echo of DelmiaReceiver.RecipeProcessResultText (often "") |
Galaxy object acknowledged the download |
false |
DelmiaReceiver.RecipeProcessResultText |
Galaxy object rejected the download (RecipeProcessResult false) |
false |
MachineCode is required |
central script, blank MachineCode |
false |
Receiver ready flag not set to true |
site script, ReadyFlag not true (or unreadable — see §7) |
false |
Timeout waiting for recipe to be processed |
site script, RecipeProcessedFlag not true within 25 s |
false |
RecipeDownload failed: <exception> |
either script — e.g. unknown instance, site unreachable, Script 'ProcessRecipeDownload' on instance 'X' failed: … |
4. Processing flow
Delmia ─► WWNotifier.exe ─► POST /api/DelmiaRecipeDownload (central, active node)
│ inbound method script (id 4): validate MachineCode
▼
Route.To(MachineCode).Call("ProcessRecipeDownload", {5 fields})
│ gRPC central→site, lands on the instance actor
▼
CvdReactor.ProcessRecipeDownload (site Call script) on the composed DelmiaReceiver child:
1. read ReadyFlag → must be true, else "Receiver ready flag not set to true"
2. write RecipeProcessedFlag = false
3. WriteBatchAndWaitAsync({DownloadPath, WorkOrderNumber, PartNumber, JobStepNumber, Username},
trigger RecipeDownloadFlag = true,
wait RecipeProcessedFlag == true, timeout 25 s)
4. read RecipeProcessResult / RecipeProcessResultText → {Result, ResultText}
Both scripts as deployed on prod (verbatim):
Central inbound method script (id 4, timeout 45 s)
try {
System.Func<string, object> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
var code = (P("MachineCode") as string ?? "").Trim();
if (string.IsNullOrEmpty(code)) return new { Result = false, ResultText = "MachineCode is required" };
var suf = code.Substring(code.Length - 1);
return await Route.To(code).Call("ProcessRecipeDownload", new {
DownloadPath = P("DownloadPath"),
WorkOrderNumber = P("WorkOrderNumber"),
PartNumber = P("PartNumber"),
JobStepNumber = P("JobStepNumber"),
Username = P("Username"),
});
} catch (Exception ex) {
return new { Result = false, ResultText = "RecipeDownload failed: " + ex.Message };
}
Site script CvdReactor.ProcessRecipeDownload (template 1, script id 4, trigger call)
// Generic DELMIA recipe-download — performed DIRECTLY on the composed DelmiaReceiver child
// (mirrors how MesMoveIn writes the MESReceiver child) instead of delegating via CallScript.
// No side logic; the receiver's own DelmiaReceiver.Download method holds the same handshake.
var recv = Children["DelmiaRecv"];
System.Func<object,bool> IsTrue = v => { var s=v?.ToString(); return (v is bool b && b)||s=="1"||string.Equals(s,"true",System.StringComparison.OrdinalIgnoreCase); };
System.Func<string,object?> P = k => Parameters.ContainsKey(k) ? Parameters[k] : null;
if (!IsTrue(recv.Attributes["ReadyFlag"]))
return new { Result=false, ResultText="Receiver ready flag not set to true" };
try {
recv.Attributes["RecipeProcessedFlag"] = false;
var data = new System.Collections.Generic.Dictionary<string, object?> {
{ "DownloadPath", P("DownloadPath") },
{ "WorkOrderNumber", P("WorkOrderNumber") },
{ "PartNumber", P("PartNumber") },
{ "JobStepNumber", P("JobStepNumber") },
{ "Username", P("Username") },
};
if (!await recv.Attributes.WriteBatchAndWaitAsync(data, "RecipeDownloadFlag", true, "RecipeProcessedFlag", true, System.TimeSpan.FromSeconds(25)))
return new { Result=false, ResultText="Timeout waiting for recipe to be processed" };
return new {
Result = IsTrue(recv.Attributes["RecipeProcessResult"]),
ResultText = recv.Attributes["RecipeProcessResultText"]?.ToString() ?? ""
};
} catch (Exception ex) {
return new { Result=false, ResultText="RecipeDownload failed: " + ex.Message };
}
5. Galaxy-side contract — the DelmiaReceiver template (id 12; composed clone id 26)
All ten attributes are data-sourced over the site MxGateway connection, bound per instance to the
reactor-level Galaxy object (prod: DelmiaReceiver_038 on Z28061, _039 on Z28062, _037 on
Z28061Sim; Left_00x.DelmiaReceiver / Right_00x.DelmiaReceiver back the unused side compositions).
| Attribute | Type | Direction | Role in the handshake |
|---|---|---|---|
ReadyFlag |
Boolean | Galaxy → SB | Precondition; must be true or the call is refused |
RecipeDownloadFlag |
Boolean | SB → Galaxy | Trigger — set true after the five data fields are written |
RecipeProcessedFlag |
Boolean | both | SB clears to false first; Galaxy sets true when done (waited ≤ 25 s) |
RecipeProcessResult |
Boolean | Galaxy → SB | Outcome → Result |
RecipeProcessResultText |
String | Galaxy → SB | Outcome text → ResultText |
DownloadPath, WorkOrderNumber, PartNumber, JobStepNumber, Username |
String | SB → Galaxy | Payload, written as one batch before the trigger |
6. API keys scoped to this method (prod, names only — secrets are shown once at creation)
mes, mes api key, delmia-mes-receivers, TestKey, JOE_TEST_KEY — all enabled, all also
scoped to the MES methods (MesMoveIn, MesMoveOut, MesSampleData, SimpleAlarmStatusRequest,
most also IpsenMESMoveIn). delmia-mes-receivers is the one whose name says it is the Delmia
caller's key; the audit actor column shows which key served each call.
7. Observed behaviour and open issues (from the prod audit log, 2026-06-17 → 2026-06-26)
Nine calls total, all test traffic (actors delmia-mes-receivers, delmia-sim-test), none since
2026-06-26 — no production Delmia traffic has hit this method yet. Every 200 answered
Result=false: mostly Receiver ready flag not set to true, one
Script 'ProcessRecipeDownload' … The given key 'side' was not present from an earlier script
revision, and one MachineCode must end with A (Left) or B (Right) from a retired left/right variant
(the current script routes on the whole MachineCode; the leftover suf variable is unused).
- Likely broken since the 2026-06-27 rename — verify before go-live. The site script still
addresses the child as
Children["DelmiaRecv"], but theCvdReactorcomposition was renamedDelmiaRecv→DelmiaReceiverthat day (../deployment/deployment-records/2026-06-27-wonder-app-vd03.md§2b), so the flattened paths areDelmiaReceiver.*.ChildrenAccessorjust concatenates the name into a path andGetAttributeon an unknown path logs a warning and yields null, soIsTrue(null)is false and every call will answerReceiver ready flag not set to trueregardless of the real flag. No call has been made since the rename, so this is inferred from the code, not observed. Fix = change that one line toChildren["DelmiaReceiver"]on the template (scadabridge template script update) and redeploy the three CvdReactor instances; check QA's copy of the script for the same string. - Not idempotent. The design requires idempotency on
(MachineCode, DownloadPath, WorkOrderNumber)because the notifier retries on timeout/connect failure (at-least-once). The deployed script re-runs the full handshake on a duplicate, so a retry after a slow first attempt will trigger a second download on the Galaxy object. requiredis empty in the parameter schema; onlyMachineCodeis enforced, in script. Missing payload fields are written to the receiver as null/empty.- QA definitions unread (credentials) — assumed identical (same build lineage), not verified.
8. Docker rig stand-in and notifier verification (2026-09-02)
The local rig (docker/, central-a :9001 / central-b :9002 / Traefik :9000, site-a = site id 1)
carries an in-memory stand-in for a vd03-style machine so the whole path can be exercised without a
Galaxy. Seeded by CLI only, no DB edits:
docker/seed-delmia-sim.sh— templateDelmiaReceiverSim(the ten §5 attributes, all static,ReadyFlagdefaulting totrue) composed intoSimMachineasDelmiaReceiver; instanceDelmiaSim01on site-a, deployed.SimMachinecarries two scripts:ProcessRecipeDownload(Call) — the §4 handshake ported to static attributes.WriteBatchAndWaitAsyncrejects static attributes (InstanceActor.HandleWriteAttributeBatch: "not a data-sourced attribute"), so the port writes the payload with per-attributeSetAsyncand waits withWaitAsync— same trigger/response flags, same 25 s budget, same result mapping. It addressesChildren["DelmiaReceiver"](the post-rename name; contrast §7 item 1).DelmiaReceiverSimulator(ValueChangeonDelmiaReceiver.RecipeDownloadFlag) — plays the Galaxy object: on the rising edge it accepts unlessDownloadPathends in.bador the work order is blank, writesRecipeProcessResult/RecipeProcessResultText, clears the trigger flag, then setsRecipeProcessedFlag = true. It lives on the root template because composed scripts' trigger configs are not prefixed at flattening, andScriptActormatchesValueChangeby exact canonical name.
docker/seed-delmia-api.sh— the §4 method verbatim (minus its deadsufline), timeout 45 s, plus one key (delmia-notifier) scoped to it; the token is printed once.Superseded 2026-09-02 by register row 108 / the shared SQL Server key store (docker/sync-inbound-api-keys.sh— required before any central failover test, see below.ScadaBridge:InboundApi:ApiKeyStore:Provider=SqlServer, now set on both rig centrals): the script is deleted andverify-delmia-notifier.shneeds no file copy before a failover run — both nodes read the samezbauthrows.docker/verify-delmia-notifier.sh— runs the managedWWNotifier.dllfrom a scratch copy whoseappsettings.jsoncarries the key (ScadaBridge:ApiKey), withBaseUrlslisting the two central nodes directly (not Traefik) so the notifier's own connect-failure failover is what gets exercised.
Whole-path latency through Traefik: a curl call returns in under 100 ms (simulator fires on the same
node, no device round-trip).
Verified outcomes (second run, 2026-09-02 16:51 UTC)
| Case | Setup | Result | Audit row (ApiInbound) |
|---|---|---|---|
| A | active (central-a) first in BaseUrls |
YES |
InboundRequest on central-a |
| B | standby (central-b) first — standby relays | YES |
one row, on central-a (the relay does not double-audit) |
| C | bad key | NO + HTTP 401 |
InboundAuthFailure on central-a |
| D | active docker killed, attempt inside the takeover window |
NO + HTTP 503 (ACTIVE_NODE_UNREACHABLE from the not-yet-active survivor) — expected, connect-failure-only failover |
none (infrastructure answer) |
| E | survivor /health/active = 200 — takeover measured 27 s after the kill |
YES (:9001 refused → :9002 200) |
InboundRequest on central-b |
| F | victim restarted, memberCount=2, victim /health/active = 503 (rejoined as standby) |
YES (victim relays) |
InboundRequest on central-b |
| G | DownloadPath = C:\recipes\x.bad |
NO + Simulated reject: bad path or missing work order |
InboundRequest on central-b, Result=false |
The notifier's first-line contract, exit codes (0 / -1), and the reason line on NO behaved as the
README states throughout.
Two findings from the first run (kept, not "fixed")
-
docker stopis not a crash. SIGTERM triggers CoordinatedShutdown's graceful singleton handover — the survivor was active 0 s after the stop. The drill usesdocker killso auto-down's ~25–30 s takeover is what gets measured (docker/failover-drill.shalready does the same). -
Inbound API keys are a per-node SQLite store, so a central failover invalidates every client's key. Keys live in
ScadaBridge:InboundApi:ApiKeyStore:SqlitePath(/app/data/inbound-api-keys.sqlite, on each central's own data volume), and key admin (ManagementActor, a per-nodeActorOfreached from that node's/managementendpoints) writes only the node that serves the call — through Traefik that is the active node. After the first run's takeover, central-b answered 401KeyNotFoundto the key central-a had minted; itssecurity api-key listwas[]. The standby-relay case (B) passes only because the standby forwards before authenticating. The same asymmetry means a key revoked on one node stays valid on the other. Rig workaround at the time:docker/sync-inbound-api-keys.shcheckpointed central-a's store on the host (sqlite3) and replaced central-b's (target stopped for the copy; both rig nodes share one pepper, so the hashes verify on either node); the verify script's preflight refused to start unless both nodes listed the key.Superseded 2026-09-02 (same day) by register row 108.
ZB.MOM.WW.Auth0.3.0 adds a SQL Server provider (ZB.MOM.WW.Auth.ApiKeys.SqlServer); both rig centrals now runScadaBridge:InboundApi:ApiKeyStore:Provider=SqlServer, sharing onezbauthrow set in the configuration database, so a key exists on both nodes the instant it's created and a revoke is cluster-wide immediately.docker/sync-inbound-api-keys.shis deleted; a re-run of this drill needs no file copy before the failover step. Re-measured 2026-09-03 without the workaround: A/B/E/FYES, C/GNO, active-centraldocker killtakeover 28 s (the earlier run measured 27 s with the file copy), victim rejoined as standby 2 s after restart. Prodwonder-app-vd03is single-node and unaffected; the QA pair (vq03/vq04) and prod still runProvider=Sqlitepending a deployment record.
Deferred-work register row 107 records §7 item 1 (Children["DelmiaRecv"]) as the prod follow-up.