docs(deploy): prepare the #52 / #131 site-script fixes for DEV and QA (not applied)

Read-only export of DEV vd03 and QA vq03 (2026-11-12): the only
Children["DelmiaRecv"] user is CvdReactor.ProcessRecipeDownload (script 4,
identical on both envs; DEV 3 / QA 9 instances), and the only ScriptAlarm
.Severity reader is DEV TestMesMachine.AlarmStatus (script 42, SBTEST01/02).
QA has no #131 work.

Adds the original (rollback) and fixed bodies, a guarded check/apply/rollback
script that re-sends every script field, and the apply plan: #52 any time,
#131 with or after the dd4f1c2a binary. The old #131 body still compiles on
main but returns banded values; the fixed body is what fails to compile on the
deployed binaries. Rig evidence for both gates is in the README.

Refs #52 #131

Claude-Session: https://claude.ai/code/session_01GT67ACmpMR7b9LGRPLijMi
This commit is contained in:
Joseph Doherty
2026-11-12 23:19:07 -05:00
parent a2e9d71824
commit a92a418d7f
8 changed files with 378 additions and 0 deletions
@@ -0,0 +1,144 @@
# Deploy prep: site-script fixes for Gitea #52 and #131
**Prepared:** 2026-11-12. **Not applied** to DEV or QA. Applying it is a deploy and needs the owner's approval.
**Where the facts come from:** a read-only export of DEV vd03 and QA vq03 taken at 2026-11-12 ~23:10 EST. The export used `template list|get --detail`, `shared-script list`, `api-method list|get`, `instance list` and `instance diff`, and nothing was written.
- **DEV** runs DisableLogin, so any credentials work.
- **QA** has login enabled. It authenticates against the box-local GLAuth (`ScadaBridge-LDAP` service), so the export used the documented `multi-role` test user there. The username and password were passed through environment variables, never on the command line.
**Validated:** on the docker rig (main binary, which includes `dd4f1c2a`). See [Rig evidence](#rig-evidence).
## Files
| Path | What |
|---|---|
| `original/CvdReactor.ProcessRecipeDownload.cs` | The live body of CvdReactor script 4. It is byte-identical on DEV and QA. **Rollback body.** |
| `original/TestMesMachine.AlarmStatus.cs` | The live body of DEV TestMesMachine script 42. **Rollback body.** |
| `original/*.meta.json` | Each script's other fields: trigger, parameters, return definition, lock and timeouts. |
| `fixed/CvdReactor.ProcessRecipeDownload.cs` | Changes one line: `Children["DelmiaRecv"]` becomes `Children["DelmiaReceiver"]`. |
| `fixed/TestMesMachine.AlarmStatus.cs` | Changes three `a.Severity` reads to `a.Priority`. It is byte-identical to `ALARM_CODE` in `docker/seed-mes-test-machine.sh` at `dd4f1c2a`. |
| `apply.sh` | Takes `check`, `apply` or `rollback` and a gate number (`52` or `131`). Details below. |
### What `apply.sh` does
- **`check`** only reads.
- **`apply` and `rollback`** refuse to touch a script unless its live body is byte-identical to the expected file.
- **Metadata:** they re-send every metadata field. `template script update` *replaces* the parameter and return definitions, so a bare `--code` update would wipe them. They then read the fields back and compare them.
- **Deploy:** they deploy the affected instances. `--no-deploy` skips this step.
## Inventory (live, 2026-11-12)
The export searched every template script and inbound API method on each env for `Children["…"]` names that aren't compositions, and for `.Severity` reads on a `ScriptAlarm`. There are no shared scripts on either env. Every instance's `instance diff` reports `isStale=false`, so the deployed scripts are the template bodies listed below.
| Env | Template / script | Problem | Instances to redeploy |
|---|---|---|---|
| DEV vd03 | `CvdReactor` (1) / `ProcessRecipeDownload` (4) | `Children["DelmiaRecv"]`. The only composition is `DelmiaReceiver`. **#52** | Z28061 (1), Z28062 (2), Z28061Sim (8) |
| QA vq03/vq04 | `CvdReactor` (1) / `ProcessRecipeDownload` (4) | Same body, byte for byte. **#52** | Z28061 (1), Z28062 (2), Z28063 (9), Z28064 (11), Z28065 (12), Z28066 (13), Z28067 (10), Z28068 (14), Z28069 (15) |
| DEV vd03 | `TestMesMachine` (30) / `AlarmStatus` (42) | Reads `a.Severity` in 3 places: the MinSeverity and MaxSeverity predicates and `Severity = a.Severity`. **#131** | SBTEST01 (9), SBTEST02 (10) |
| QA | — | **No #131 work.** QA has no TestMesMachine. Its `CvdReactor.AlarmStatus` (39) is a stub that returns `Alarms = []`. No other script calls `Alarms.*`. | — |
Every other `Children[...]` reference on both envs resolves: TestMesMachine's `LeftMESReceiver`, `RightMESReceiver` and `DelmiaReceiver`.
The inbound methods `SimpleAlarmStatusRequest` and `AlarmStatusRequest` only *pass* `MinSeverity`/`MaxSeverity` to the site script as arguments. They never read a `ScriptAlarm`, so they need no change.
Two corrections to the issue texts:
- **#131: the old body still compiles on main.** `ScriptAlarm.Severity` still exists after `dd4f1c2a`; it now holds the banded value. So the deployed scripts keep compiling and running on the new binary, but they return **wrong numbers and inverted filters** (rig evidence, Q1–Q3). The compile problem runs the other way: the **fixed** body references `ScriptAlarm.Priority`, which the deployed binaries don't have. Neither DEV `424db155` nor QA `b3021e7c` contains `dd4f1c2a`. On the rig, a body with an unknown `ScriptAlarm` member saves fine, because save does no compile check. Deploying it is refused with `Pre-deployment validation failed: … ScriptCompilation`.
- **#131: DEV's CvdReactor has no `AlarmStatus` script at all.** So `SimpleAlarmStatusRequest` for Z2806x answers "not supported". The `CvdReactor.SimpleAlarmStatus`/`AlarmStatus` pattern in `docs/plans/2026-06-30-mes-alarm-status-api.md` §5.3 is a doc pattern and is not deployed anywhere. If it is ever authored, write it with `a.Priority`.
## Order relative to the ScadaBridge binary deploy
### #52: any time, independent of the binary
The fixed body uses only APIs that the DEV and QA binaries already run, because the current body already uses them. The fix is one string literal.
- **Recommended:** apply it in the same window as the next binary deploy, so the instances redeploy only once.
- **Z28061 on the wonder boxes is also used by another team.** Agree a slot before redeploying it; the redeploy restarts the instance actor. Z28062 is the safe one to test against.
### #131: DEV only, with or after the binary that contains `dd4f1c2a`, in the same window
1. Deploy the new ScadaBridge binary to central and site.
2. Immediately run `apply.sh apply 131`, before MES alarm traffic is trusted again.
**Applied before the binary:** the template saves, but the SBTEST instance deploys are refused by pre-deployment compilation. The site keeps the old body, which is harmless. Run `apply.sh rollback 131 --no-deploy` to put the template back.
**Binary without this step:** MES alarm-status output for SBTEST01/02 changes. Values come back banded (800→250, 300→750, 700→500), and the MinSeverity and MaxSeverity filters select the wrong alarms.
**Alternative:** re-run `docker/seed-mes-test-machine.sh` against vd03. It produces the same AlarmStatus body. But it also rewrites the inbound methods `SimpleAlarmStatusRequest` and `AlarmStatusRequest` and every other TestMesMachine script, so diff those first. `apply.sh` changes only the one script.
## Apply (from the Mac, after approval)
```bash
cd ~/Desktop/ScadaBridge && dotnet build src/ZB.MOM.WW.ScadaBridge.CLI # apply.sh uses bin/Debug/net10.0/scadabridge (override: SB_CLI)
P=docs/deployment/deploy-prep/2026-11-12-gates-52-131
# DEV (DisableLogin: any credentials)
export SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 SCADABRIDGE_USERNAME=x SCADABRIDGE_PASSWORD=x
bash $P/apply.sh check 52; bash $P/apply.sh apply 52 # any time
bash $P/apply.sh check 131 # after the dd4f1c2a+ binary is up on DEV…
bash $P/apply.sh apply 131 # …in the same window
# QA (login enabled; box-local GLAuth; export the password from a prompt, not argv)
export SCADABRIDGE_URL=http://wonder-app-vq03.zmr.zimmer.com:8085 SCADABRIDGE_USERNAME=multi-role
read -rs SCADABRIDGE_PASSWORD && export SCADABRIDGE_PASSWORD
bash $P/apply.sh check 52; bash $P/apply.sh apply 52 # 9 instances; vq03 is the active central
```
**Manual equivalent:**
1. Update the script with `scadabridge template script update`, passing every flag:
- `--id <script id> --name <name> --trigger-type <type> --code "$(cat fixed/<x>.cs)"`
- `--parameters "<parameterDefinitions>"` and `--return-def "<returnDefinition>"`, both from `original/<x>.meta.json`
2. Run `scadabridge instance deploy --id <n>` for each instance in the inventory.
## Verify
- **#52 (read-only first):** run `scadabridge debug snapshot --id 2` on Z28062. `DelmiaReceiver.ReadyFlag` is the value the fixed script now reads.
- **#52 end-to-end:** a `DelmiaRecipeDownload` call drives the real Galaxy handshake on Z28062, so it needs the owner's go-ahead. Use either a live `WWNotifier` call or `scadabridge flow-debug run-method --method-id 4 --params '{"MachineCode":"Z28062",…}'`.
- The answer must no longer be `Receiver ready flag not set to true` while the flag is true.
- The DEV site log must stop showing `GetAttribute: attribute 'DelmiaRecv.ReadyFlag' not found`.
- **#131:** on vd03, run `SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 bash docker/verify-mes-test-machine.sh` (the `RECIPE_ROOT`/`RECIPE_OUTSIDE` values are in its header). Groups C and D must pass: C3 `Severity == 800`, C7 MinSeverity 500, C9 MaxSeverity 750. These are the same values the scripts returned before #129.
- Record the apply in `docs/deployment/deployment-records/`.
## Rollback
`bash $P/apply.sh rollback 52` and `bash $P/apply.sh rollback 131` write back `original/*.cs`, byte for byte, and redeploy. Rollback is guarded the same way: it refuses if the live body is not the fixed file.
Rolling #131 back while the new binary is still running brings back the banded values. Only roll it back together with the binary.
## Rig evidence
**Setup:** the docker rig on main, with `AlarmPriorityBands` in the deployed Commons. The rig was idle: no management calls in the previous 60 min.
### #52
A temporary rig template `CvdReactor` held the DEV `ProcessRecipeDownload` body and its parameter schema. It composed the rig's static `DelmiaReceiverSim` as `DelmiaReceiver`. The template and its instance GATE52RIG were deleted afterwards. Each run was driven by `flow-debug run-script`, and the `ReadyFlag` value was confirmed in `debug snapshot` first.
| Body | ReadyFlag | Result |
|---|---|---|
| original | true | `Receiver ready flag not set to true`; site WRN `attribute 'DelmiaRecv.ReadyFlag' not found on instance 'GATE52RIG'` |
| original | false | `Receiver ready flag not set to true` |
| fixed | true | Got past the flag check into the batch write: `Batch write failed: Attribute 'DelmiaReceiver.DownloadPath' is not a data-sourced attribute` (expected, see below) |
| fixed | false | `Receiver ready flag not set to true` (negative control: the flag really is read) |
The rig receiver is static, and `WriteBatchAndWaitAsync` only accepts data-sourced attributes. On the wonder boxes, `DelmiaReceiver.*` is Galaxy-bound, so the batch write proceeds there. The handshake code after the flag check is unchanged by this fix.
`apply → rollback → apply` round-tripped. The revision hash returned to `sha256:b2058525…` on rollback and `sha256:1da04831…` on apply. A repeat apply was a no-op.
### #131
The rig's `TestMesMachine` (5305) `AlarmStatus` was byte-identical to DEV's script 42 when the test started. Alarms were tripped with `VacuumPressure=12`, `DoorOpen=true` and `SensorStatus=-1`, which raises HighVacuumFault (priority 800, MES), DoorOpenAlarm (300) and SensorFault (700; MES on SBTEST02). Values are shown as (name, Severity, IsFlaggedForMES).
| Query | Original body on main (wrong) | Fixed body on main |
|---|---|---|
| Q1 SBTEST01 all | Door 750, HighVac 250, Sensor 500 | Door 300, HighVac 800, Sensor 700 |
| Q2 MinSeverity=500 | Door, Sensor | HighVac, Sensor |
| Q3 SBTEST02 FlaggedOnly + MaxSeverity=750 | HighVac, Sensor | Sensor |
| Q4 FlaggedOnly | HighVac 250 | HighVac 800 |
- **The fixed column matches the pre-#129 expectations** in `docker/verify-mes-test-machine.sh` (C3, C7, C9).
- **Compile gate (positive control):** a body with an unknown `ScriptAlarm` member saved, and its deploy was refused with `Pre-deployment validation failed: … ScriptCompilation`. That is what the fixed body would hit on the current DEV binary.
- **Guard:** with that unknown body live, `apply.sh apply 131` answered `REFUSED: live body is UNKNOWN`.
- **Round trip:** `rollback → apply` round-tripped.
- **Rig end state:** SBTEST01/02 are left on the fixed body (what `seed-mes-test-machine.sh` seeds since `dd4f1c2a`) with the alarm sources reset.
@@ -0,0 +1,100 @@
#!/bin/bash
set -euo pipefail
# Apply / roll back the Gitea #52 and #131 site-script fixes on one ScadaBridge central.
# Prepared 2026-11-12; see README.md in this folder for the order relative to the binary deploy.
#
# SCADABRIDGE_URL=http://wonder-app-vd03.zmr.zimmer.com:8085 \
# SCADABRIDGE_USERNAME=... SCADABRIDGE_PASSWORD=... \
# bash apply.sh <check|apply|rollback> <52|131> [--no-deploy]
#
# check READ-ONLY. Prints, per target script, whether the live body is the ORIGINAL (deployed
# 2026-11-12), the FIXED body, or something else, and whether each instance is stale.
# apply Guarded update: refuses unless the live body is byte-identical to original/<script>.cs
# (or already fixed -> no-op). Re-sends every other field from original/<script>.meta.json
# (`template script update` REPLACES parameters/return-def/trigger-config — omitting them
# would wipe them). Then deploys the affected instances unless --no-deploy.
# rollback The reverse: requires the live body to be fixed/<script>.cs, writes original/<script>.cs.
#
# Credentials come from the environment only (SCADABRIDGE_USERNAME / SCADABRIDGE_PASSWORD); nothing
# is passed on argv. DEV vd03 runs DisableLogin (any value works); QA needs a real login.
#
# #52 -> template CvdReactor, script ProcessRecipeDownload; deploys every CvdReactor instance.
# #131 -> template TestMesMachine, script AlarmStatus; deploys every TestMesMachine instance
# (SBTEST01/SBTEST02, DEV only — QA has no TestMesMachine and no script reading .Severity).
HERE="$(cd "$(dirname "$0")" && pwd)"
REPO="$(cd "$HERE/../../../.." && pwd)"
export SB_CLI="${SB_CLI:-$REPO/src/ZB.MOM.WW.ScadaBridge.CLI/bin/Debug/net10.0/scadabridge}"
export SB_URL="${SCADABRIDGE_URL:?set SCADABRIDGE_URL}" SB_HERE="$HERE"
export SB_MODE="${1:?check|apply|rollback}" SB_GATE="${2:?52|131}" SB_NODEPLOY="${3:-}"
: "${SCADABRIDGE_USERNAME:?set SCADABRIDGE_USERNAME}" "${SCADABRIDGE_PASSWORD:?set SCADABRIDGE_PASSWORD}"
python3 - <<'PY'
import json, os, subprocess, sys
CLI, URL, HERE = os.environ["SB_CLI"], os.environ["SB_URL"], os.environ["SB_HERE"]
MODE, GATE, NODEPLOY = os.environ["SB_MODE"], os.environ["SB_GATE"], os.environ["SB_NODEPLOY"] == "--no-deploy"
TARGET = {"52": ("CvdReactor", "ProcessRecipeDownload"), "131": ("TestMesMachine", "AlarmStatus")}[GATE]
KEY = f"{TARGET[0]}.{TARGET[1]}"
def cli(*args, timeout=300):
r = subprocess.run([CLI, *args, "--url", URL, "--format", "json"], capture_output=True, text=True, timeout=timeout)
if r.returncode != 0:
raise SystemExit(f"FAILED {' '.join(args[:3])}: {(r.stderr or r.stdout).strip()[:600]}")
return json.loads(r.stdout) if r.stdout.strip() else None
def body(kind): return open(os.path.join(HERE, kind, KEY + ".cs"), encoding="utf-8").read()
ORIG, FIXED = body("original"), body("fixed")
META = json.load(open(os.path.join(HERE, "original", KEY + ".meta.json"), encoding="utf-8"))
tmpl = next((t for t in cli("template", "list") if t["name"] == TARGET[0] and not t.get("parentTemplateId")), None)
if tmpl is None:
print(f"{URL}: no base template {TARGET[0]} — nothing to do for #{GATE}"); sys.exit(0)
detail = cli("template", "get", "--id", str(tmpl["id"]), "--detail")
script = next((s for s in detail["scripts"] if s["name"] == TARGET[1] and not s.get("isInherited")), None)
if script is None:
print(f"{URL}: template {TARGET[0]} has no own script {TARGET[1]}"); sys.exit(0)
live = script.get("code") or ""
state = "ORIGINAL" if live == ORIG else "FIXED" if live == FIXED else "UNKNOWN"
insts = [i for i in cli("instance", "list") if i["templateId"] == tmpl["id"]]
print(f"{URL}: {KEY} template={tmpl['id']} script={script['id']} live body = {state}")
for i in insts:
d = cli("instance", "diff", "--id", str(i["id"]))
print(f" instance {i['uniqueName']} (id {i['id']}) stale={d.get('isStale')} deployedAt={d.get('deployedAt')}")
if MODE == "check":
sys.exit(0)
want, need = (FIXED, "ORIGINAL") if MODE == "apply" else (ORIG, "FIXED")
if live == want:
print(" already in the target state — no script update")
if not any(cli("instance", "diff", "--id", str(i["id"])).get("isStale") for i in insts):
print(" every instance current — nothing to deploy"); sys.exit(0)
elif state != need:
raise SystemExit(f" REFUSED: live body is {state}, expected {need}. Diff it against {HERE} before touching it.")
else:
args = ["template", "script", "update", "--id", str(script["id"]), "--name", script["name"],
"--code", want, "--trigger-type", script["triggerType"]]
for opt, field in (("--trigger-config", "triggerConfiguration"), ("--parameters", "parameterDefinitions"),
("--return-def", "returnDefinition"), ("--min-time-between-runs", "minTimeBetweenRuns"),
("--execution-timeout-seconds", "executionTimeoutSeconds")):
v = META.get(field)
if v not in (None, ""):
args += [opt, str(v)]
if META.get("isLocked"):
args.append("--locked")
cli(*args)
after = next(s for s in cli("template", "get", "--id", str(tmpl["id"]), "--detail")["scripts"] if s["id"] == script["id"])
for field in ("parameterDefinitions", "returnDefinition", "triggerConfiguration"):
if (after.get(field) or None) != (META.get(field) or None):
raise SystemExit(f" {field} changed by the update — restore it from original/{KEY}.meta.json")
if (after.get("code") or "") != want:
raise SystemExit(" body read back does not match the target file")
print(f" script {script['id']} updated ({MODE}); metadata read back unchanged")
if NODEPLOY:
print(" --no-deploy: instances left stale; deploy them yourself"); sys.exit(0)
for i in insts:
r = cli("instance", "deploy", "--id", str(i["id"]))
print(f" deploy {i['uniqueName']}: {json.dumps(r)[:200]}")
PY
@@ -0,0 +1,26 @@
// 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["DelmiaReceiver"];
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 };
}
@@ -0,0 +1,29 @@
var code = Parameters["MachineCode"]?.ToString() ?? "";
var flaggedOnly = Parameters.Get<bool?>("FlaggedOnly") ?? false;
var includeAck = Parameters.Get<bool?>("IncludeAcked") ?? true;
var minSev = Parameters.Get<int?>("MinSeverity");
var maxSev = Parameters.Get<int?>("MaxSeverity");
var nameFilter = Parameters.ContainsKey("NameFilter") ? Parameters["NameFilter"]?.ToString() : null;
try {
var alarms = flaggedOnly ? await Alarms.MesReportableAsync() : await Alarms.CurrentAsync();
var infos = alarms
.Where(a => a.Active && !a.IsConfiguredPlaceholder)
.Where(a => minSev == null || a.Priority >= minSev)
.Where(a => maxSev == null || a.Priority <= maxSev)
.Where(a => string.IsNullOrEmpty(nameFilter) || a.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase))
.Where(a => includeAck || !a.Acknowledged)
.Select(a => new {
Name = a.Name,
HierarchicalName = code + "." + a.Name,
Description = !string.IsNullOrEmpty(a.Message) ? a.Message : !string.IsNullOrEmpty(a.AlarmTypeName) ? a.AlarmTypeName : a.Name,
IsFlaggedForMES = a.IsMesReportable,
Severity = a.Priority, // MES Severity = the raw 0-999 priority (ScriptAlarm.Priority, #129)
StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered",
TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp).ToString("o"),
AckDT = a.AckTime?.ToString("o"),
AckComment = a.OperatorComment })
.ToList();
return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos };
} catch (Exception ex) {
return new { WasSuccessful = false, ErrorText = "AlarmStatus failed: " + ex.Message, Alarms = Array.Empty<object>() };
}
@@ -0,0 +1,26 @@
// 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 };
}
@@ -0,0 +1,12 @@
{
"id": 4,
"templateId": 1,
"name": "ProcessRecipeDownload",
"triggerType": "call",
"triggerConfiguration": null,
"isLocked": false,
"parameterDefinitions": "{\"type\":\"object\",\"properties\":{\"DownloadPath\":{\"type\":\"string\"},\"WorkOrderNumber\":{\"type\":\"string\"},\"PartNumber\":{\"type\":\"string\"},\"JobStepNumber\":{\"type\":\"string\"},\"Username\":{\"type\":\"string\"}},\"required\":[]}",
"returnDefinition": null,
"minTimeBetweenRuns": null,
"executionTimeoutSeconds": null
}
@@ -0,0 +1,29 @@
var code = Parameters["MachineCode"]?.ToString() ?? "";
var flaggedOnly = Parameters.Get<bool?>("FlaggedOnly") ?? false;
var includeAck = Parameters.Get<bool?>("IncludeAcked") ?? true;
var minSev = Parameters.Get<int?>("MinSeverity");
var maxSev = Parameters.Get<int?>("MaxSeverity");
var nameFilter = Parameters.ContainsKey("NameFilter") ? Parameters["NameFilter"]?.ToString() : null;
try {
var alarms = flaggedOnly ? await Alarms.MesReportableAsync() : await Alarms.CurrentAsync();
var infos = alarms
.Where(a => a.Active && !a.IsConfiguredPlaceholder)
.Where(a => minSev == null || a.Severity >= minSev)
.Where(a => maxSev == null || a.Severity <= maxSev)
.Where(a => string.IsNullOrEmpty(nameFilter) || a.Name.Contains(nameFilter, StringComparison.OrdinalIgnoreCase))
.Where(a => includeAck || !a.Acknowledged)
.Select(a => new {
Name = a.Name,
HierarchicalName = code + "." + a.Name,
Description = !string.IsNullOrEmpty(a.Message) ? a.Message : !string.IsNullOrEmpty(a.AlarmTypeName) ? a.AlarmTypeName : a.Name,
IsFlaggedForMES = a.IsMesReportable,
Severity = a.Severity,
StatusCode = a.Acknowledged ? "Triggered.Acked" : "Triggered",
TriggeredDT = (a.OriginalRaiseTime ?? a.Timestamp).ToString("o"),
AckDT = a.AckTime?.ToString("o"),
AckComment = a.OperatorComment })
.ToList();
return new { WasSuccessful = true, ErrorText = (string)null, Alarms = infos };
} catch (Exception ex) {
return new { WasSuccessful = false, ErrorText = "AlarmStatus failed: " + ex.Message, Alarms = Array.Empty<object>() };
}
@@ -0,0 +1,12 @@
{
"id": 42,
"templateId": 30,
"name": "AlarmStatus",
"triggerType": "Call",
"triggerConfiguration": null,
"isLocked": false,
"parameterDefinitions": "{\"type\":\"object\",\"properties\":{\"MachineCode\":{\"type\":\"string\"},\"FlaggedOnly\":{\"type\":\"boolean\"},\"IncludeAcked\":{\"type\":\"boolean\"},\"MinSeverity\":{\"type\":\"integer\"},\"MaxSeverity\":{\"type\":\"integer\"},\"NameFilter\":{\"type\":\"string\"}},\"required\":[\"MachineCode\"]}",
"returnDefinition": "{\"type\":\"object\",\"properties\":{\"WasSuccessful\":{\"type\":\"boolean\"},\"ErrorText\":{\"type\":\"string\"},\"Alarms\":{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"Name\":{\"type\":\"string\"},\"HierarchicalName\":{\"type\":\"string\"},\"Description\":{\"type\":\"string\"},\"IsFlaggedForMES\":{\"type\":\"boolean\"},\"Severity\":{\"type\":\"integer\"},\"StatusCode\":{\"type\":\"string\"},\"TriggeredDT\":{\"type\":\"string\"},\"AckDT\":{\"type\":\"string\"},\"AckComment\":{\"type\":\"string\"}}}}},\"required\":[\"WasSuccessful\",\"Alarms\"]}",
"minTimeBetweenRuns": null,
"executionTimeoutSeconds": null
}