# ScadaBridge CLI

Command-line tool for managing the ScadaBridge SCADA system. Connects to a Central node via HTTP and routes commands through the Management API endpoint (`POST /management`), which dispatches to the ManagementActor. Authentication is handled server-side via LDAP.

## Installation

```sh
dotnet build src/ZB.MOM.WW.ScadaBridge.CLI
```

The output binary is `scadabridge` (or `scadabridge.exe` on Windows).

## Connection

Every command requires a connection to a running Central node. The management URL can be supplied three ways, evaluated in this priority order:

1. `--url` flag on the command line
2. `SCADABRIDGE_MANAGEMENT_URL` environment variable
3. `managementUrl` field in `~/.scadabridge/config.json`

```sh
scadabridge --url http://central-host:5000 <command>
```

**Docker:** With the Docker setup, the Central UI (and management API) is available at `http://localhost:9001`:

```sh
scadabridge --url http://localhost:9001 <command>
```

For HA failover, use a load balancer URL. The management API is stateless (Basic Auth per request), so any central node can handle any request without sticky sessions.

## Global Options

These options are accepted by the root command and inherited by all subcommands.

| Option | Description |
|--------|-------------|
| `--url <value>` | Management API URL (e.g., `http://localhost:9001`) |
| `--username <value>` | LDAP username for authentication |
| `--password <value>` | LDAP password for authentication |
| `--format <json\|table>` | Output format (default: `json`) |

## Configuration File

`~/.scadabridge/config.json` is loaded at startup. All fields are optional.

```json
{
  "managementUrl": "http://localhost:9001"
}
```

For the Docker test environment, see `docker/README.md` for a ready-to-use config.

## Environment Variables

| Variable | Description |
|----------|-------------|
| `SCADABRIDGE_MANAGEMENT_URL` | Management API URL (overrides config file) |
| `SCADABRIDGE_FORMAT` | Default output format (overrides config file) |
| `SCADABRIDGE_USERNAME` | LDAP username (fallback when `--username` is not supplied) |
| `SCADABRIDGE_PASSWORD` | LDAP password (fallback when `--password` is not supplied). Preferred over `--password` on the command line, which leaks into process listings and shell history. |

## Output

All commands write JSON to stdout on success. Errors are written as JSON to stderr:

```json
{ "error": "Human-readable message", "code": "ERROR_CODE" }
```

Exit codes:

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | Command error |
| `2` | Authorization failure |

---

## Command Reference

### `template` — Manage templates

#### `template list`

List all templates. The server projects this listing in the database and returns
**summary rows** — no child collections, member counts only. **Table** output
(`--format table`) renders them as id, name, description, parent, and member **counts**
(`#attrs`, `#alarms`, `#scripts`, `#comps`, `#nativeAlarms`), so it stays readable in a
terminal. `--detail` skips that column projection and renders the summary payload
verbatim; it does **not** add attribute/alarm/script definitions — a listing never
carries them. Use `template get --id <int>` for a template's full definition. **JSON**
output (`--format json`) is always the unmodified payload regardless of `--detail`.

```sh
scadabridge --url <url> template list                            # compact table
scadabridge --url <url> --format table template list --detail    # raw summary rows
scadabridge --url <url> --format json template list              # unmodified JSON (always)
```

| Option | Required | Description |
|--------|----------|-------------|
| `--detail` | no | Render the raw summary payload instead of the compact column projection (no effect on JSON; does not add definitions) |
| `--skip` | no | Offset paging: number of items to skip (default 0) |
| `--take` | no | Offset paging: max items to return (clamped 1..1000; omit for unlimited) |

#### `template get`

Get a single template by ID. Unlike `template list`, this returns the **full** template
entity with every attribute, alarm, script and composition inline. Table output is a
compact summary (the same counts as `template list`) unless `--detail` is supplied, which
dumps the full definitions; JSON output is always full.

```sh
scadabridge --url <url> template get --id <int> [--detail]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Template ID |
| `--detail` | no | Include full definitions in table output (no effect on JSON) |

#### `template create`

Create a new template, optionally inheriting from a parent.

```sh
scadabridge --url <url> template create --name <string> [--description <string>] [--parent-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Template name |
| `--description` | no | Template description |
| `--parent-id` | no | Parent template ID for inheritance |

#### `template update`

Update an existing template. Optional fields use **leave-unchanged** semantics: omitting
`--description` or `--parent-id` keeps the stored value rather than wiping it. To
explicitly clear the description, pass an empty string (`--description ""`). The parent
template is immutable after creation — omit `--parent-id` (or pass the current value);
passing a different value is rejected.

```sh
scadabridge --url <url> template update --id <int> --name <string> [--description <string>] [--parent-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Template ID |
| `--name` | yes | Template name |
| `--description` | no | Updated description. Omit to leave unchanged; pass `""` to clear. |
| `--parent-id` | no | Immutable; omit to leave unchanged. |

#### `template delete`

Delete a template by ID.

```sh
scadabridge --url <url> template delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Template ID |

#### `template validate`

Run pre-deployment validation on a template (flattening, naming collisions, script compilation).

```sh
scadabridge --url <url> template validate --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Template ID |

#### `template attribute add`

Add an attribute to a template.

```sh
scadabridge --url <url> template attribute add --template-id <int> --name <string> --data-type <string> [--value <string>] [--element-type <string>] [--description <string>] [--data-source <string>] [--locked]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--template-id` | yes | Template ID |
| `--name` | yes | Attribute name |
| `--data-type` | yes | Attribute data type (`Boolean`, `Int32`, `Float`, `Double`, `String`, `DateTime`, `List`) |
| `--value` | no | Default value. For a `List` attribute, supply a JSON array (e.g. `'["WO-1","WO-2"]'`); the raw string is forwarded to the API, which parses it |
| `--element-type` | no | Element scalar type for a `List` attribute (`String`, `Int32`, `Float`, `Double`, `Boolean`, `DateTime`). **Required when `--data-type` is `List`**; must be omitted otherwise |
| `--description` | no | Description |
| `--data-source` | no | Data source reference |
| `--locked` | no | Lock the attribute in derived templates |

**List example** — add a multi-value String attribute with two default elements:

```sh
scadabridge --url <url> template attribute add --template-id 7 --name WorkOrders \
  --data-type List --element-type String --value '["WO-1","WO-2"]'
```

The CLI validates the data-type / element-type combination locally before calling the
API: `--data-type List` requires a valid `--element-type`, and `--element-type` may only
be supplied when `--data-type` is `List`. The Management API re-validates server-side.

#### `template attribute update`

Update an attribute on a template. An update **replaces** the whole entity — every
required field below must be supplied with its post-update value, even if unchanged.

```sh
scadabridge --url <url> template attribute update --id <int> --name <string> --data-type <string> [--value <string>] [--element-type <string>] [--description <string>] [--data-source <string>] [--locked]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Attribute ID |
| `--name` | yes | Attribute name |
| `--data-type` | yes | Attribute data type (`Boolean`, `Int32`, `Float`, `Double`, `String`, `DateTime`, `List`) |
| `--value` | no | Default value. For a `List` attribute, supply a JSON array (e.g. `'["WO-1","WO-2"]'`) |
| `--element-type` | no | Element scalar type for a `List` attribute (`String`, `Int32`, `Float`, `Double`, `Boolean`, `DateTime`). **Required when `--data-type` is `List`**; must be omitted otherwise |
| `--description` | no | Description |
| `--data-source` | no | Data source reference |
| `--locked` | no | Lock the attribute in derived templates |

#### `template attribute delete`

Remove an attribute from a template.

```sh
scadabridge --url <url> template attribute delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Attribute ID |

#### `template alarm add`

Add an alarm definition to a template.

```sh
scadabridge --url <url> template alarm add --template-id <int> --name <string> --trigger-type <string> --priority <int> [--description <string>] [--trigger-config <json>] [--locked] [--trigger-kind <kind>] [typed-setpoint-flags]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--template-id` | yes | Template ID |
| `--name` | yes | Alarm name |
| `--trigger-type` | yes | Trigger type (e.g. `ValueMatch`, `RangeViolation`, `RateOfChange`, `HiLo`, `Expression`) |
| `--priority` | yes | Alarm priority (0–1000) |
| `--description` | no | Description |
| `--trigger-config` | no | Trigger configuration as a JSON string (overrides typed flags when both are supplied) |
| `--locked` | no | Lock the alarm in derived templates |
| `--trigger-kind` | no | Expression-trigger analysis kind: `advisory` (default — non-blocking advisory finding on a blank expression) or `strict` (blank expression is a deploy-blocking error). Ignored for non-`Expression` trigger types. |
| `--attribute` | no | Attribute name the trigger watches (all trigger types except `Expression`) |
| `--match-value` | no | `ValueMatch`: value to compare against |
| `--not-equals` | no | `ValueMatch`: match when the value is NOT equal |
| `--min` | no | `RangeViolation`: minimum allowed value |
| `--max` | no | `RangeViolation`: maximum allowed value |
| `--threshold-per-second` | no | `RateOfChange`: rate threshold per second |
| `--window-seconds` | no | `RateOfChange`: sliding window in seconds |
| `--direction` | no | `RateOfChange`: direction (`rising`, `falling`, or `either`) |
| `--lolo` | no | `HiLo`: low-low setpoint |
| `--lo` | no | `HiLo`: low setpoint |
| `--hi` | no | `HiLo`: high setpoint |
| `--hihi` | no | `HiLo`: high-high setpoint |
| `--expression` | no | `Expression`: boolean trigger expression |

#### `template alarm update`

Update an alarm definition on a template. An update **replaces** the whole entity — every
required field below must be supplied with its post-update value, even if unchanged.

```sh
scadabridge --url <url> template alarm update --id <int> --name <string> --trigger-type <string> --priority <int> [--description <string>] [--trigger-config <json>] [--locked] [--trigger-kind <kind>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Alarm ID |
| `--name` | yes | Alarm name |
| `--trigger-type` | yes | Trigger type |
| `--priority` | yes | Alarm priority (0–1000) |
| `--description` | no | Description |
| `--trigger-config` | no | Trigger configuration JSON |
| `--locked` | no | Lock the alarm in derived templates |
| `--trigger-kind` | no | Expression-trigger analysis kind: `advisory` (default) or `strict`. Ignored for non-`Expression` trigger types. **Requires `--trigger-config`** — the kind is written into the trigger-config JSON, so on `update` it has no effect without one (the CLI prints a warning to stderr and continues). |

#### `template alarm delete`

Remove an alarm definition from a template.

```sh
scadabridge --url <url> template alarm delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Alarm ID |

#### `template native-alarm-source add`

Bind a native alarm source to a template — a read-only mirror of an OPC UA Alarms &
Conditions server's or the MxAccess Gateway's alarms (no ack-back).

```sh
scadabridge --url <url> template native-alarm-source add --template-id <int> --name <string> --connection <string> --source-ref <string> [--filter <string>] [--description <string>] [--locked]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--template-id` | yes | Template ID |
| `--name` | yes | Source binding name |
| `--connection` | yes | Alarm-capable data connection name (OPC UA or MxGateway) |
| `--source-ref` | yes | Source reference (OPC UA SourceNode nodeId, or MxAccess object/area) |
| `--filter` | no | Condition filter (omit to mirror all conditions under the source) |
| `--description` | no | Description |
| `--locked` | no | Lock the binding in derived templates |

#### `template native-alarm-source list`

List the native alarm source bindings on a template.

```sh
scadabridge --url <url> template native-alarm-source list --template-id <int>
```

#### `template native-alarm-source remove`

Remove a native alarm source binding from a template (by its ID).

```sh
scadabridge --url <url> template native-alarm-source remove --id <int>
```

#### `template script add`

Add a script to a template.

```sh
scadabridge --url <url> template script add --template-id <int> --name <string> --trigger-type <string> --code <string> [--trigger-config <json>] [--locked] [--parameters <json>] [--return-def <json>] [--min-time-between-runs <duration>] [--execution-timeout-seconds <int>] [--trigger-kind <kind>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--template-id` | yes | Template ID |
| `--name` | yes | Script name |
| `--trigger-type` | yes | Trigger type (e.g. `OnChange`, `Periodic`, `OnAlarm`) |
| `--code` | yes | Script source code |
| `--trigger-config` | no | Trigger configuration as a JSON string (e.g. interval, attribute name) |
| `--locked` | no | Lock the script in derived templates |
| `--parameters` | no | Parameter definitions JSON |
| `--return-def` | no | Return definition JSON |
| `--min-time-between-runs` | no | Minimum time between runs — a throttle for triggered scripts, or the re-fire interval for a `WhileTrue` trigger. Accepts a unit suffix `ms`, `s`/`sec`, or `m`/`min` (e.g. `500ms`, `5s`, `2min`); a bare number is seconds. Omit, or pass `0`, to leave it unset (no throttle). |
| `--execution-timeout-seconds` | no | Per-script execution timeout in seconds. Omit (or pass a non-positive value) to use the site's global default (`SiteRuntimeOptions.ScriptExecutionTimeoutSeconds`). |
| `--trigger-kind` | no | Expression-trigger analysis kind: `advisory` (default) or `strict`. Only meaningful when `--trigger-type` is `Expression`. **Requires `--trigger-config`** — the kind is written into the trigger-config JSON, so passing `--trigger-kind` without `--trigger-config` has no effect (the CLI prints a warning to stderr and continues). |

#### `template script update`

Update a script on a template. An update **replaces** the whole entity — every required
field below must be supplied with its post-update value, even if unchanged.

```sh
scadabridge --url <url> template script update --id <int> --name <string> --trigger-type <string> --code <string> [--trigger-config <json>] [--locked] [--parameters <json>] [--return-def <json>] [--min-time-between-runs <duration>] [--execution-timeout-seconds <int>] [--trigger-kind <kind>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Script ID |
| `--name` | yes | Script name |
| `--trigger-type` | yes | Trigger type |
| `--code` | yes | Script source code |
| `--trigger-config` | no | Trigger configuration JSON |
| `--locked` | no | Lock the script in derived templates |
| `--parameters` | no | Parameter definitions JSON |
| `--return-def` | no | Return definition JSON |
| `--min-time-between-runs` | no | Minimum time between runs (throttle / `WhileTrue` re-fire interval). Unit suffix `ms`, `s`/`sec`, or `m`/`min` (e.g. `500ms`, `5s`, `2min`); a bare number is seconds. Omit or `0` leaves it unset. As an update replaces the whole entity, omitting this **clears** any existing value. |
| `--execution-timeout-seconds` | no | Per-script execution timeout in seconds; omit (or non-positive) uses the site default. As an update replaces the whole entity, omitting this **clears** any existing override. |
| `--trigger-kind` | no | Expression-trigger analysis kind: `advisory` (default) or `strict`. Only meaningful when `--trigger-type` is `Expression`. **Requires `--trigger-config`** — without it the kind is ignored (the CLI warns to stderr and continues). |

#### `template script delete`

Remove a script from a template.

```sh
scadabridge --url <url> template script delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Script ID |

#### `template composition add`

Add a feature module composition to a template.

```sh
scadabridge --url <url> template composition add --template-id <int> --composed-template-id <int> --instance-name <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--template-id` | yes | Target template ID |
| `--composed-template-id` | yes | Feature module template ID to compose |
| `--instance-name` | yes | Instance name for the composed module (used in path-qualified addressing) |

#### `template composition delete`

Remove a feature module composition from a template, identified by its own composition ID.

```sh
scadabridge --url <url> template composition delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Composition ID to remove |

#### `template resync-members`

Reconcile a template's stored *inherited* member rows (and those of its whole derived subtree) with the resolved effective inherited set: materialize missing inherited placeholders (e.g. base members added after a derived template was created), re-sync drifted ones to the live base value, and remove orphaned ones. After a resync the template editor's member tabs are complete and the "Base template changed" banner clears. Authored overrides are never touched. Base member changes auto-propagate going forward; this command repairs templates that drifted before that was in place. Returns the per-subtree change counts (templates changed, members added/updated/removed).

```sh
scadabridge --url <url> template resync-members --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Template ID (its derived subtree is included) |

#### `template folder list`

List all template folders. Folders are a Central UI organizational device only — they
have no effect on template resolution or flattening.

```sh
scadabridge --url <url> template folder list
```

Takes no options beyond the global ones.

#### `template folder create`

Create a template folder. Omit `--parent-id` to create it at the tree root. Sibling
names must be unique (case-insensitive).

```sh
scadabridge --url <url> template folder create --name <string> [--parent-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Folder name |
| `--parent-id` | no | Parent folder ID; omit to create at the tree root |

#### `template folder rename`

Rename a template folder. Enforces sibling-name uniqueness.

```sh
scadabridge --url <url> template folder rename --id <int> --name <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Folder ID |
| `--name` | yes | New folder name |

#### `template folder move`

Move a template folder under a new parent folder. Omit `--parent-id` to move it to the
tree root. Rejects cycles (a folder cannot move under one of its own descendants).

```sh
scadabridge --url <url> template folder move --id <int> [--parent-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Folder ID to move |
| `--parent-id` | no | New parent folder ID; omit to move to the tree root |

#### `template folder reorder`

Swap a folder's `SortOrder` with an adjacent sibling. `up` swaps with the previous
sibling, `down` with the next; reordering past either end is a no-op.

```sh
scadabridge --url <url> template folder reorder --id <int> --direction <up|down>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Folder ID |
| `--direction` | yes | `up` or `down` (lowercase literals only) |

#### `template folder delete`

Delete a template folder. Blocked if the folder still contains any subfolders or templates.

```sh
scadabridge --url <url> template folder delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Folder ID |

---

### `instance` — Manage instances

#### `instance get`

Get a single instance by ID.

```sh
scadabridge --url <url> instance get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

#### `instance list`

List instances, with optional filters.

```sh
scadabridge --url <url> instance list [--site-id <int>] [--template-id <int>] [--search <string>] [--skip <int>] [--take <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | no | Filter by site ID |
| `--template-id` | no | Filter by template ID |
| `--search` | no | Search term matched against instance name |
| `--skip` | no | Offset paging: number of items to skip (default 0). Applied after site-scope filtering. |
| `--take` | no | Offset paging: max items to return (clamped 1..1000; omit for unlimited) |

#### `instance create`

Create a new instance of a template at a site.

```sh
scadabridge --url <url> instance create --name <string> --template-id <int> --site-id <int> [--area-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Unique instance name |
| `--template-id` | yes | Template to instantiate |
| `--site-id` | yes | Site where the instance will run |
| `--area-id` | no | Area within the site |

#### `instance native-alarm-source set`

Override an inherited native alarm source binding for a single instance (upsert). Blank
options keep the inherited value.

```sh
scadabridge --url <url> instance native-alarm-source set --instance-id <int> --source <string> [--connection <string>] [--source-ref <string>] [--filter <string>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--instance-id` | yes | Instance ID |
| `--source` | yes | Source binding canonical name (e.g. `Pressure` or `Module.Pressure`) |
| `--connection` | no | Connection name override (blank = inherited) |
| `--source-ref` | no | Source reference override (blank = inherited) |
| `--filter` | no | Condition filter override (blank = inherited) |

#### `instance native-alarm-source clear`

Clear an instance's native alarm source override, reverting to the inherited binding.

```sh
scadabridge --url <url> instance native-alarm-source clear --instance-id <int> --source <string>
```

#### `instance native-alarm-source import`

Bulk-apply native alarm source overrides for a single instance from a CSV file
(all-or-nothing — the native-source parity of `instance import-overrides`). The CSV
header is required: `SourceName,Connection,SourceReference,Filter`; a blank override
field keeps the inherited value. Every named source must resolve for the instance and
be unlocked, or the whole batch is rejected before any write.

```sh
scadabridge --url <url> instance native-alarm-source import --instance-id <int> --file <path>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--instance-id` | yes | Instance ID |
| `--file` | yes | Path to the override CSV (`SourceName,Connection,SourceReference,Filter`; blank field = inherited) |

Example CSV:

```csv
SourceName,Connection,SourceReference,Filter
Pressure,OpcB,ns=2;s=Pump.Alarm,Active
Module.Temp,,,
```

#### `instance deploy`

Deploy an instance to its site. Acquires the per-instance operation lock.

```sh
scadabridge --url <url> instance deploy --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

#### `instance enable`

Enable a previously disabled instance.

```sh
scadabridge --url <url> instance enable --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

#### `instance disable`

Disable a running instance without deleting it.

```sh
scadabridge --url <url> instance disable --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

#### `instance delete`

Delete an instance. The instance must be disabled first.

```sh
scadabridge --url <url> instance delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

#### `instance set-bindings`

Set data connection bindings for an instance's attributes.

```sh
scadabridge --url <url> instance set-bindings --id <int> --bindings <json>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |
| `--bindings` | yes | JSON array of binding entries, each either `[attributeName, dataConnectionId]` or `[attributeName, dataConnectionId, dataSourceReferenceOverride]` |

The optional **third element** sets the per-instance data-source reference override
(the OPC UA node id / protocol address used in place of the template's
`DataSourceReference` for that attribute). Pass a string to set it, or `null` / omit
it to use the template default:

```sh
# Bind Speed with an address override; bind Mode using the template default reference.
scadabridge --url <url> instance set-bindings --id 42 \
  --bindings '[["Speed",7,"ns=2;s=Reactor.Speed"],["Mode",7]]'
```

> **Note:** this command **replaces** all bindings for the instance. Include the
> override on every entry that needs one — omitting the third element clears any
> previously-set override for that attribute.

#### `instance set-overrides`

Set attribute value overrides for an instance.

```sh
scadabridge --url <url> instance set-overrides --id <int> --overrides <json>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |
| `--overrides` | yes | JSON object of attribute name to value (e.g. `{"Speed": "100", "Mode": null}`); null clears an override |

#### `instance import-overrides`

Apply attribute value overrides from a CSV file. The CSV must have a header row (`AttributeName,Value,ElementType`; the `ElementType` column is optional). An empty `Value` cell clears the override for that attribute. If the file contains any parse errors, all errors are printed and no overrides are applied.

```sh
scadabridge --url <url> instance import-overrides --id <int> --file <path.csv>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |
| `--file` | yes | Path to the override CSV file |

CSV format example:

```csv
AttributeName,Value,ElementType
Speed,100,
Mode,,
Label,"Hello, World",
```

(`Value` empty = clear the override for that attribute.)

#### `instance alarm-override set`

Set (upsert) an alarm override on an instance.

```sh
scadabridge --url <url> instance alarm-override set --instance-id <int> --alarm <string> [--trigger-config <json>] [--priority <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--instance-id` | yes | Instance ID |
| `--alarm` | yes | Alarm canonical name (e.g. `TempLevels` or `Pump.TempSensor.Heat`) |
| `--trigger-config` | no | JSON override for TriggerConfiguration (HiLo: partial merge; others: whole-replace) |
| `--priority` | no | Priority override (0–1000) |

#### `instance alarm-override delete`

Remove an alarm override on an instance.

```sh
scadabridge --url <url> instance alarm-override delete --instance-id <int> --alarm <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--instance-id` | yes | Instance ID |
| `--alarm` | yes | Alarm canonical name |

#### `instance alarm-override list`

List all alarm overrides for an instance.

```sh
scadabridge --url <url> instance alarm-override list --instance-id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--instance-id` | yes | Instance ID |

#### `instance set-area`

Reassign an instance to a different area (or clear its area assignment).

```sh
scadabridge --url <url> instance set-area --id <int> [--area-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |
| `--area-id` | no | Area ID; omit to clear area assignment |

#### `instance diff`

Show the deployment diff between the currently deployed configuration and the current template state.

```sh
scadabridge --url <url> instance diff --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

---

### `site` — Manage sites

#### `site get`

Get a single site by ID.

```sh
scadabridge --url <url> site get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Site ID |

#### `site list`

List all registered sites.

```sh
scadabridge --url <url> site list
```

#### `site create`

Register a new site.

```sh
scadabridge --url <url> site create --name <string> --identifier <string> [--description <string>] [--node-a-address <string>] [--node-b-address <string>] [--grpc-node-a-address <string>] [--grpc-node-b-address <string>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Human-readable site name |
| `--identifier` | yes | Unique machine identifier used for cluster routing (e.g. `site-a`) |
| `--description` | no | Site description |
| `--node-a-address` | no | Akka.NET cluster address for Node A |
| `--node-b-address` | no | Akka.NET cluster address for Node B |
| `--grpc-node-a-address` | no | gRPC streaming address for Node A |
| `--grpc-node-b-address` | no | gRPC streaming address for Node B |

#### `site update`

Update an existing site. An update **replaces** the whole entity — every required field
below must be supplied with its post-update value, even if unchanged.

```sh
scadabridge --url <url> site update --id <int> --name <string> [--description <string>] [--node-a-address <string>] [--node-b-address <string>] [--grpc-node-a-address <string>] [--grpc-node-b-address <string>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Site ID |
| `--name` | yes | Human-readable site name |
| `--description` | no | Site description |
| `--node-a-address` | no | Akka.NET cluster address for Node A |
| `--node-b-address` | no | Akka.NET cluster address for Node B |
| `--grpc-node-a-address` | no | gRPC streaming address for Node A |
| `--grpc-node-b-address` | no | gRPC streaming address for Node B |

#### `site delete`

Delete a site. Fails if any instances are assigned to it.

```sh
scadabridge --url <url> site delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Site ID |

#### `site deploy-artifacts`

Push compiled artifacts to one or all sites.

```sh
scadabridge --url <url> site deploy-artifacts [--site-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | no | Target site ID; deploys system-wide artifacts to exactly that site. Omit to deploy fleet-wide to all sites. A site-scoped (non-Administrator) Deployer must supply an in-scope `--site-id`; fleet-wide deployment requires a system-wide Deployer or Administrator (arch-review C2). |

#### `site area list`

List all areas for a site.

```sh
scadabridge --url <url> site area list --site-id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site ID |

#### `site area create`

Create an area within a site.

```sh
scadabridge --url <url> site area create --site-id <int> --name <string> [--parent-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site ID |
| `--name` | yes | Area name |
| `--parent-id` | no | Parent area ID for nested areas |

#### `site area update`

Update an area's name. An update **replaces** the whole entity — the required field
below must be supplied, even if unchanged.

```sh
scadabridge --url <url> site area update --id <int> --name <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Area ID |
| `--name` | yes | Area name |

#### `site area move`

Re-parent an area within its own site. Omit `--parent-id` to move the area to the
site root. Rejected for: self-parent, moving under one of the area's own descendants
(cycle), a parent in a different site, and a sibling name collision at the target level.

```sh
scadabridge --url <url> site area move --id <int> [--parent-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Area ID to move |
| `--parent-id` | no | New parent area ID; omit to move to the site root |

#### `site area delete`

Delete an area. Fails if any instances are assigned to it.

```sh
scadabridge --url <url> site area delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Area ID |

---

### `deploy` — Deployment operations

#### `deploy instance`

Deploy a single instance (same as `instance deploy`).

```sh
scadabridge --url <url> deploy instance --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

#### `deploy site`

Deploy **every deployable instance** at one site in a single batched operation.

Each instance keeps the full single-instance semantics — its own deployment ID and
revision hash, its own per-instance operation lock, its own optimistically-concurrent
status record, and the same query-before-redeploy idempotency check. The batch is
**not** all-or-nothing: an instance that cannot be deployed (wrong state, failed
validation, lock already held, site round-trip timed out) is reported as a failed row
and the rest proceed. Each failed instance is individually retryable with
`instance deploy --id`.

Site round-trips run with bounded concurrency
(`ScadaBridge:DeploymentManager:SiteDeploymentMaxParallelism`, default 4), each under
`SiteDeploymentTimeoutPerInstance` (default 120 s), so one wedged instance cannot stall
the batch. The client waits up to 5 minutes.

`--site-id` is **required** — unlike `deploy artifacts`, omitting it does not mean
fleet-wide.

```sh
scadabridge --url <url> deploy site --site-id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Target site ID. A site-scoped (non-Administrator) Deployer must supply an in-scope site. |

Output is a per-instance result matrix (`instanceId`, `uniqueName`, `deploymentId`,
`success`, `errorMessage`) plus `successCount` / `failureCount`.

#### `deploy artifacts`

Deploy compiled artifacts to one or all sites (same as `site deploy-artifacts`).

```sh
scadabridge --url <url> deploy artifacts [--site-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | no | Target site ID; deploys to exactly that site. Omit to deploy fleet-wide. A site-scoped (non-Administrator) Deployer must supply an in-scope `--site-id`; fleet-wide requires a system-wide Deployer or Administrator (arch-review C2). |

#### `deploy status`

Query deployment records, with optional filters and pagination.

```sh
scadabridge --url <url> deploy status [--instance-id <int>] [--status <string>] [--page <int>] [--page-size <int>]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--instance-id` | no | — | Filter by instance ID |
| `--status` | no | — | Filter by deployment status string |
| `--page` | no | `1` | Page number |
| `--page-size` | no | `50` | Results per page |

---

### `data-connection` — Manage data connections

#### `data-connection get`

Get a single data connection by ID.

```sh
scadabridge --url <url> data-connection get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Data connection ID |

#### `data-connection list`

List data connections, optionally filtered by site.

```sh
scadabridge --url <url> data-connection list [--site-id <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | no | Filter by site ID |

#### `data-connection create`

Create a new data connection belonging to a specific site.

```sh
scadabridge --url <url> data-connection create --site-id <int> --name <string> --protocol <string> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site ID the connection belongs to |
| `--name` | yes | Connection name |
| `--protocol` | yes | Protocol identifier (e.g. `OpcUa`) |
| `--primary-config` | no | Primary protocol-specific configuration as a JSON string (alias `--configuration`) |
| `--backup-config` | no | Backup protocol-specific configuration as a JSON string |
| `--failover-retry-count` | no | Retries before failover to the backup configuration (default: `3`) |

#### `data-connection update`

Update a data connection definition. An update **replaces** the whole entity — every
required field below must be supplied, even if unchanged.

```sh
scadabridge --url <url> data-connection update --id <int> --name <string> --protocol <string> [--primary-config <json>] [--backup-config <json>] [--failover-retry-count <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Data connection ID |
| `--name` | yes | Connection name |
| `--protocol` | yes | Protocol identifier |
| `--primary-config` | no | Primary protocol-specific configuration as a JSON string (alias `--configuration`) |
| `--backup-config` | no | Backup protocol-specific configuration as a JSON string |
| `--failover-retry-count` | no | Retries before failover to the backup configuration (default: `3`) |

#### `data-connection delete`

Delete a data connection.

```sh
scadabridge --url <url> data-connection delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Data connection ID |

#### `data-connection browse`

Browse the immediate children of an OPC UA node on the live server backing a site data connection. `--site` routes the command to the target site. Designer role.

```sh
scadabridge --url <url> data-connection browse --site <identifier> --connection <name> [--node-id <nodeId>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site` | yes | Target site identifier |
| `--connection` | yes | Data connection name |
| `--node-id` | no | Parent node id to browse (server root / ObjectsFolder if omitted) |

#### `data-connection search`

Bounded recursive search of an OPC UA address space (substring on DisplayName / root-relative path). Designer role.

```sh
scadabridge --url <url> data-connection search --site <identifier> --connection <name> --query <text> [--max-depth <int>] [--max-results <int>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site` | yes | Target site identifier |
| `--connection` | yes | Data connection name |
| `--query` | yes | Case-insensitive substring matched on DisplayName and root-relative path |
| `--max-depth` | no | Maximum levels below the root to descend (default 5) |
| `--max-results` | no | Maximum number of matches to return (default 100) |

#### `data-connection verify-endpoint`

Probe an endpoint configuration on a site without persisting it (read-only; never trusts the server certificate). The endpoint JSON is read verbatim from `--config-file`. Designer role.

```sh
scadabridge --url <url> data-connection verify-endpoint --site <identifier> --protocol <string> --config-file <path> [--connection <name>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site` | yes | Target site identifier |
| `--protocol` | yes | Protocol type (e.g. `OpcUa`) |
| `--config-file` | yes | Path to the endpoint configuration JSON file (read into `ConfigJson`) |
| `--connection` | no | Connection name for logging/correlation |

#### `data-connection certs list`

List the certificates in a site's trusted-peer and rejected PKI stores. Admin role.

```sh
scadabridge --url <url> data-connection certs list --site <identifier> [--connection <name>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site` | yes | Target site identifier |
| `--connection` | no | Data connection name (contextual; not part of the request) |

#### `data-connection certs trust`

Trust an OPC UA server certificate on every node of a site. The DER file is base64-encoded into the request. Admin role.

```sh
scadabridge --url <url> data-connection certs trust --site <identifier> --connection <name> --der-file <path> --thumbprint <hex>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site` | yes | Target site identifier |
| `--connection` | yes | Data connection the certificate was captured from (correlation) |
| `--der-file` | yes | Path to the DER-encoded certificate file (base64-encoded into the request) |
| `--thumbprint` | yes | Certificate thumbprint (store filename key) |

#### `data-connection certs remove`

Remove a trusted OPC UA server certificate from every node of a site, identified by thumbprint. Admin role.

```sh
scadabridge --url <url> data-connection certs remove --site <identifier> --thumbprint <hex> [--connection <name>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site` | yes | Target site identifier |
| `--thumbprint` | yes | Thumbprint of the certificate to remove |
| `--connection` | no | Data connection name (contextual; not part of the request) |

---

### `external-system` — Manage external HTTP systems

#### `external-system get`

Get a single external system definition by ID.

```sh
scadabridge --url <url> external-system get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | External system ID |

#### `external-system list`

List all external system definitions.

```sh
scadabridge --url <url> external-system list
```

#### `external-system create`

Register an external HTTP system that scripts can call.

```sh
scadabridge --url <url> external-system create --name <string> --endpoint-url <url> --auth-type <string> [--auth-config <json>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Display name |
| `--endpoint-url` | yes | Base URL of the external system |
| `--auth-type` | yes | Authentication type: `ApiKey` or `BasicAuth` |
| `--auth-config` | no | Auth credentials as a JSON string |

#### `external-system update`

Update an external system definition. An update **replaces** the whole entity — every
required field below must be supplied, even if unchanged.

```sh
scadabridge --url <url> external-system update --id <int> --name <string> --endpoint-url <url> --auth-type <string> [--auth-config <json>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | External system ID |
| `--name` | yes | Display name |
| `--endpoint-url` | yes | Base URL |
| `--auth-type` | yes | Authentication type |
| `--auth-config` | no | Updated auth credentials as a JSON string |

#### `external-system delete`

Delete an external system definition.

```sh
scadabridge --url <url> external-system delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | External system ID |

---

### `notification` — Manage notification lists

#### `notification get`

Get a single notification list by ID.

```sh
scadabridge --url <url> notification get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Notification list ID |

#### `notification list`

List all notification lists.

```sh
scadabridge --url <url> notification list
```

#### `notification create`

Create a notification list with one or more recipients. The `--type` flag selects the
delivery channel and decides which recipient flag is required: `email` (the default)
uses `--emails`; `sms` uses `--phones`. Supplying the wrong recipient flag for the
channel is rejected before the command is sent.

```sh
# Email list (default channel)
scadabridge --url <url> notification create --name <string> --emails <email1,email2,...>

# SMS list
scadabridge --url <url> notification create --name <string> --type sms --phones <e164a,e164b,...>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Notification list name |
| `--type` | no | Delivery channel: `email` or `sms` (case-insensitive; default `email`) |
| `--emails` | conditional | Comma-separated recipient email addresses. **Required for `--type email`; rejected for `--type sms`.** |
| `--phones` | conditional | Comma-separated recipient phone numbers in E.164. **Required for `--type sms`; rejected for `--type email`.** |

#### `notification update`

Update a notification list. An update **replaces** the whole entity — every required
field below must be supplied, even if unchanged. As with `create`, `--type` selects the
channel and decides whether `--emails` or `--phones` is required.

```sh
# Email list
scadabridge --url <url> notification update --id <int> --name <string> --emails <email1,email2,...>

# SMS list
scadabridge --url <url> notification update --id <int> --name <string> --type sms --phones <e164a,e164b,...>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Notification list ID |
| `--name` | yes | List name |
| `--type` | no | Delivery channel: `email` or `sms` (case-insensitive; default `email`) |
| `--emails` | conditional | Comma-separated recipient email addresses. **Required for `--type email`; rejected for `--type sms`.** |
| `--phones` | conditional | Comma-separated recipient phone numbers in E.164. **Required for `--type sms`; rejected for `--type email`.** |

#### `notification delete`

Delete a notification list.

```sh
scadabridge --url <url> notification delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Notification list ID |

#### `notification smtp list`

Show the current SMTP configuration.

```sh
scadabridge --url <url> notification smtp list
```

#### `notification smtp update`

Update the SMTP configuration.

```sh
scadabridge --url <url> notification smtp update --id <int> --server <string> --port <int> --auth-mode <string> --from-address <string> [--tls-mode <string>] [--credentials <string>] [--oauth2-authority <string>] [--oauth2-scope <string>] [--transport <string>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | SMTP config ID |
| `--server` | yes | SMTP server hostname (under `--transport Ews`, the full EWS URL — `https://…/ews/exchange.asmx`) |
| `--port` | yes | SMTP server port |
| `--auth-mode` | yes | Authentication mode (e.g. `OAuth2ClientCredentials`, `Basic`) |
| `--from-address` | yes | Sender email address |
| `--tls-mode` | no | TLS mode: `None`, `StartTLS`, or `SSL` (preserves existing if omitted) |
| `--credentials` | no | SMTP credentials — `username:password` for Basic, or client secret for OAuth2 (preserves existing if omitted) |
| `--oauth2-authority` | no | OAuth2 token-endpoint URL — a `{tenant}` placeholder is substituted from the credential (preserves existing / Microsoft 365 default if omitted; OAuth2 auth mode only) |
| `--oauth2-scope` | no | OAuth2 scope requested from the token endpoint (preserves existing / Microsoft 365 default if omitted; OAuth2 auth mode only) |
| `--transport` | no | Email transport: `Smtp` or `Ews` (preserves existing if omitted) |

Under `--transport Ews`, `--server` carries the full EWS URL, `--auth-mode` must be `Basic`,
and `--port` / `--tls-mode` are unused.

#### `notification sms list`

Show the current SMS (Twilio) configuration. The Twilio **Auth Token is never returned**
— the listing reports it only as a `hasAuthToken` presence flag.

```sh
scadabridge --url <url> notification sms list
```

#### `notification sms update`

Update the SMS (Twilio) configuration.

```sh
scadabridge --url <url> notification sms update --id <int> --account-sid <string> --from-number <string> [--messaging-service-sid <string>] [--api-base-url <string>] [--auth-token <string>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | SMS config ID |
| `--account-sid` | yes | Twilio Account SID |
| `--from-number` | yes | Sender phone number (E.164) |
| `--messaging-service-sid` | no | Twilio Messaging Service SID. **Omitting it CLEARS the stored value** (the update overwrites it). |
| `--api-base-url` | no | API base URL override (preserves existing if omitted) |
| `--auth-token` | no | Twilio Auth Token. **Omitting it PRESERVES the stored token.** Never printed back. |

---

### `security` — Security settings

#### `security api-key list`

List all inbound API keys.

```sh
scadabridge --url <url> security api-key list
```

#### `security api-key create`

Create a new inbound API key. The generated key value is returned in the response and not stored in plaintext — save it immediately.

```sh
scadabridge --url <url> security api-key create --name <string> --methods <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Descriptive label for the key |
| `--methods` | yes | Comma-separated API method names this key may call (e.g. `"MethodA,MethodB"`) |

#### `security api-key update`

Enable or disable an API key.

```sh
scadabridge --url <url> security api-key update --key-id <string> --enabled <bool>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--key-id` | yes | API key ID |
| `--enabled` | yes | Enable or disable the key (`true` or `false`) |

#### `security api-key set-methods`

Replace the method scopes on an existing API key.

```sh
scadabridge --url <url> security api-key set-methods --key-id <string> --methods <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--key-id` | yes | API key ID |
| `--methods` | yes | Comma-separated API method names (replaces the existing set) |

#### `security api-key delete`

Revoke and delete an API key.

```sh
scadabridge --url <url> security api-key delete --key-id <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--key-id` | yes | API key ID |

#### `security role-mapping list`

List all LDAP group → role mappings.

```sh
scadabridge --url <url> security role-mapping list
```

#### `security role-mapping create`

Map an LDAP group to a ScadaBridge role.

```sh
scadabridge --url <url> security role-mapping create --ldap-group <string> --role <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--ldap-group` | yes | LDAP group distinguished name or CN |
| `--role` | yes | ScadaBridge role: `Admin`, `Design`, or `Deployment` |

#### `security role-mapping update`

Update an LDAP role mapping. An update **replaces** the whole entity — every required
field below must be supplied, even if unchanged.

```sh
scadabridge --url <url> security role-mapping update --id <int> --ldap-group <string> --role <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Mapping ID |
| `--ldap-group` | yes | LDAP group distinguished name or CN |
| `--role` | yes | ScadaBridge role |

#### `security role-mapping delete`

Remove an LDAP role mapping.

```sh
scadabridge --url <url> security role-mapping delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Mapping ID |

#### `security scope-rule list`

List all site scope rules for a role mapping.

```sh
scadabridge --url <url> security scope-rule list --mapping-id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--mapping-id` | yes | Role mapping ID |

#### `security scope-rule add`

Add a site scope rule to a role mapping, restricting it to a specific site.

```sh
scadabridge --url <url> security scope-rule add --mapping-id <int> --site-id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--mapping-id` | yes | Role mapping ID |
| `--site-id` | yes | Site ID to scope the mapping to |

#### `security scope-rule delete`

Remove a site scope rule from a role mapping.

```sh
scadabridge --url <url> security scope-rule delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Scope rule ID |

---

### `health` — Health monitoring

#### `health summary`

Return the current health state for all known sites as a JSON object keyed by site identifier.

```sh
scadabridge --url <url> health summary
```

#### `health site`

Return the health state for a single site.

```sh
scadabridge --url <url> health site --identifier <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--identifier` | yes | Site identifier (e.g. `site-a`) |

#### `health event-log`

Query the site event log for a specific site. Events are fetched remotely from the site's local SQLite store.

```sh
scadabridge --url <url> health event-log --site <string> [--event-type <string>] [--severity <string>] [--keyword <string>] [--from <datetime>] [--to <datetime>] [--page <int>] [--page-size <int>] [--instance-name <string>]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--site` | yes | — | Site identifier |
| `--event-type` | no | — | Filter by event type |
| `--severity` | no | — | Filter by severity |
| `--keyword` | no | — | Keyword search term |
| `--from` | no | — | Start timestamp in ISO 8601 format |
| `--to` | no | — | End timestamp in ISO 8601 format |
| `--page` | no | `1` | Page number |
| `--page-size` | no | `50` | Results per page |
| `--instance-name` | no | — | Filter by instance name |

#### `health parked-messages`

Query parked (dead-letter) messages at a specific site.

```sh
scadabridge --url <url> health parked-messages --site <string> [--page <int>] [--page-size <int>]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--site` | yes | — | Site identifier |
| `--page` | no | `1` | Page number |
| `--page-size` | no | `50` | Results per page |

---

### `debug` — Runtime debugging

#### `debug snapshot`

Request a point-in-time snapshot of a running instance's current attribute values and alarm states from the site. The instance must be deployed and enabled.

```sh
scadabridge --url <url> debug snapshot --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

The command resolves the instance's site internally and routes the request to the correct site cluster. Returns all attribute values (name, value, quality, timestamp) and alarm states (name, state, priority, timestamp) at the moment the request reaches the site.

#### `debug stream`

Stream live attribute values and alarm state changes in real-time using a SignalR WebSocket connection to the central server's `/hubs/debug-stream` hub. Events are printed as they arrive. Press Ctrl+C to disconnect.

```sh
scadabridge --url <url> debug stream --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Instance ID |

The default JSON format outputs one NDJSON object per event line with a `type` field (`snapshot`, `attributeChanged`, or `alarmChanged`). Table format (`--format table`) shows a formatted initial snapshot followed by streaming rows prefixed with `ATTR` or `ALARM`.

Features:
- Automatic reconnection on connection loss with re-subscribe.
- Works through the Traefik load balancer (WebSocket upgrade proxied natively).
- Requires the `Deployment` role.

---

### `audit` — Centralized Audit Log

Read access to the central append-only **Audit Log** (#23) — the record of every
script-trust-boundary action: outbound API calls (sync + cached), outbound DB
operations (sync + cached), notifications, and inbound API calls. This is distinct
from the configuration-change audit trail exposed by [`audit-config`](#audit-config--configuration-change-audit-log).

The subcommands map directly onto the `GET /api/audit/query`,
`GET /api/audit/export`, `GET /api/audit/tree`, and
`POST /api/audit/backfill-source-node` management endpoints. Filters and the
result columns mirror the Central UI **Audit** page, so a CLI query and a UI
query with the same filters return the same rows — CLI ↔ UI filter parity is
intentional.

**Permissions.** Querying and tree traversal require the `OperationalAudit`
permission (roles `Admin`, `Audit`, or `AuditReadOnly`). Exporting requires the
stricter `AuditExport` permission (roles `Admin` or `Audit`) — read access does
*not* imply export access. The `backfill-source-node` maintenance command requires
the `Admin` role. A request without the required role returns exit code `2`.

#### `audit query`

Query audit log events with optional filters and keyset pagination.

```sh
scadabridge --url <url> audit query [options]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--since` | no | — | Start time: relative (`1h`, `24h`, `7d`) or ISO-8601 |
| `--until` | no | — | End time: relative (`1h`, `24h`, `7d`) or ISO-8601 |
| `--channel` | no | — | Filter by channel (`ApiOutbound`, `DbOutbound`, `Notification`, `ApiInbound`); repeatable — multiple values are OR-combined |
| `--kind` | no | — | Filter by event kind (`ApiCall`, `ApiCallCached`, `DbWrite`, `DbWriteCached`, `NotifySend`, `NotifyDeliver`, `InboundRequest`, `InboundAuthFailure`, `CachedSubmit`, `CachedResolve`); repeatable — multiple values are OR-combined |
| `--status` | no | — | Filter by status (`Submitted`, `Forwarded`, `Attempted`, `Delivered`, `Failed`, `Parked`, `Discarded`, `Skipped`); repeatable — multiple values are OR-combined |
| `--site` | no | — | Filter by source site ID; repeatable — multiple values are OR-combined |
| `--target` | no | — | Filter by target (external system, DB connection, notification list) |
| `--actor` | no | — | Filter by actor |
| `--correlation-id` | no | — | Filter by correlation ID |
| `--execution-id` | no | — | Filter by execution ID |
| `--parent-execution-id` | no | — | Filter by parent execution ID |
| `--errors-only` | no | `false` | Show only failed events (`status=Failed`; overrides `--status`) |
| `--page-size` | no | `100` | Events per page (1–1000) |
| `--all` | no | `false` | Fetch every page, following the keyset cursor |
| `--format` | no | `json` | Output format: `json` (JSONL, one event per line) or `table` |

The `--channel`/`--kind`/`--status`/`--site` filters accept multiple values —
either as repeated flags (`--channel ApiOutbound --channel DbOutbound`) or
space-separated after one flag (`--channel ApiOutbound DbOutbound`). Values
within one filter are OR-combined; the different filters are AND-combined.

With `--format table`, events render as an aligned text table with columns
`OccurredAtUtc`, `Channel`, `Kind`, `Status`, `Target`, `Actor`, `DurationMs`,
`HttpStatus`; long `Target`/`Actor` values are truncated with an ellipsis. With
`--format json` (the default), each page is emitted as JSONL — one JSON object per
line — which streams cleanly under `--all` across many pages.

#### `audit export`

Export audit log events to a file. The export streams from the server, so it is not
bounded by the query page size.

```sh
scadabridge --url <url> audit export --since <time> --until <time> --format <fmt> --output <path> [filters]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--since` | yes | — | Start time: relative (`1h`, `24h`, `7d`) or ISO-8601 |
| `--until` | yes | — | End time: relative (`1h`, `24h`, `7d`) or ISO-8601 |
| `--format` | yes | — | Export format: `csv`, `jsonl`, or `parquet` |
| `--output` | yes | — | Destination file path |
| `--channel` | no | — | Filter by channel |
| `--kind` | no | — | Filter by event kind |
| `--status` | no | — | Filter by status |
| `--site` | no | — | Filter by source site ID |
| `--target` | no | — | Filter by target |
| `--actor` | no | — | Filter by actor |

> `--format parquet` is accepted by the CLI but the server returns `501 Not
> Implemented` — Parquet archival is deferred to v1.x (see `Component-AuditLog.md`).
> Use `csv` or `jsonl`.

#### `audit tree` (M5.3 T8)

Display the full execution-chain tree for a given execution ID. The server walks
`ParentExecutionId` to find the root, then traverses downward to collect all
reachable executions in the chain.

```sh
scadabridge --url <url> audit tree --execution-id <guid> [--format table|json]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--execution-id` | yes | — | Any `ExecutionId` in the chain (root or child) |
| `--format` | no | `json` | Output format: `json` (structured tree) or `table` (indented tree) |

The `--execution-id` can be any node in the chain — the server resolves the root
automatically. With `--format table` the tree is printed as an indented text
representation. With `--format json` (the default) a structured JSON tree is
returned, suitable for scripting. Backed by `GET /api/audit/tree?executionId=<guid>`.
Requires `OperationalAudit` permission.

#### `audit backfill-source-node` (M5.6 T5)

Set `SourceNode` to a sentinel value on pre-feature rows where `SourceNode IS NULL`
and `OccurredAtUtc` is older than `--before`. Admin-only maintenance command.

```sh
scadabridge --url <url> audit backfill-source-node --before <ISO-8601-UTC> [--sentinel <value>] [--batch <n>]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--before` | yes | — | ISO-8601 UTC datetime; only rows older than this date are eligible |
| `--sentinel` | no | `unknown` | Value to write (must be non-empty) |
| `--batch` | no | `5000` | Max rows updated per batch; controls transaction size |

The command is idempotent — running it multiple times converges (only rows where
`SourceNode IS NULL` are eligible; already-set rows are untouched). Backed by
`POST /api/audit/backfill-source-node`. Requires `Admin` role.

#### `audit verify-chain`

Verify the audit log hash chain for a given month.

```sh
scadabridge --url <url> audit verify-chain --month <YYYY-MM>
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--month` | yes | — | Month to verify, `YYYY-MM` (e.g. `2026-05`) |

> **v1 no-op.** Hash-chain tamper-evidence is not enabled in this release (T1
> deferred to v1.x). The subcommand validates the `--month` argument and prints a
> notice pointing at the v1.x roadmap in `Component-AuditLog.md`; it exits `0`
> without contacting the server. The command exists now so scripts and operator
> habits do not need to change when tamper-evidence ships.

---

### `audit-config` — Configuration-change audit log

Query the configuration-change audit trail (the `IAuditService` record of admin
edits to templates, instances, sites, etc.). This is **separate** from the
centralized [`audit`](#audit--centralized-audit-log) group above.

> **Renamed.** This group was named `audit-log` before #23 M8. The old name still
> works as a deprecated alias — `scadabridge audit-log query ...` resolves the full
> subcommand tree unchanged — but prints a deprecation warning to stderr:
> `Warning: 'audit-log' is deprecated and will be removed in a future release. Use 'audit-config' instead.`
> Migrate scripts to `audit-config`; the `audit-log` alias will be removed in a
> future release.

#### `audit-config query`

Query the configuration-change audit log with optional filters and pagination.

```sh
scadabridge --url <url> audit-config query [options]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--user` | no | — | Filter by username |
| `--entity-type` | no | — | Filter by entity type (e.g. `Template`, `Instance`) |
| `--action` | no | — | Filter by action (e.g. `Create`, `Delete`) |
| `--from` | no | — | Start timestamp in ISO 8601 format |
| `--to` | no | — | End timestamp in ISO 8601 format |
| `--page` | no | `1` | Page number |
| `--page-size` | no | `50` | Results per page |

---

### `shared-script` — Manage shared scripts

#### `shared-script list`

List all shared script definitions.

```sh
scadabridge --url <url> shared-script list
```

#### `shared-script get`

Get a single shared script by ID.

```sh
scadabridge --url <url> shared-script get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Shared script ID |

#### `shared-script create`

Create a new shared script.

```sh
scadabridge --url <url> shared-script create --name <string> --code <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Shared script name |
| `--code` | yes | Script source code (or `@filepath` to read from file) |

#### `shared-script update`

Update a shared script. An update **replaces** the whole entity — every required field
below must be supplied, even if unchanged.

```sh
scadabridge --url <url> shared-script update --id <int> --name <string> --code <string> [--parameters <json>] [--return-def <json>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Shared script ID |
| `--name` | yes | Shared script name |
| `--code` | yes | Script source code |
| `--parameters` | no | Parameter definitions JSON |
| `--return-def` | no | Return type definition JSON |

#### `shared-script delete`

Delete a shared script.

```sh
scadabridge --url <url> shared-script delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Shared script ID |

---

### `db-connection` — Manage database connections

#### `db-connection list`

List all database connection definitions.

```sh
scadabridge --url <url> db-connection list
```

#### `db-connection get`

Get a single database connection by ID.

```sh
scadabridge --url <url> db-connection get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Database connection ID |

#### `db-connection create`

Create a new database connection definition.

```sh
scadabridge --url <url> db-connection create --name <string> --connection-string <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--name` | yes | Connection name |
| `--connection-string` | yes | Database connection string |

#### `db-connection update`

Update a database connection definition. An update **replaces** the whole entity —
every required field below must be supplied, even if unchanged.

```sh
scadabridge --url <url> db-connection update --id <int> --name <string> --connection-string <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Database connection ID |
| `--name` | yes | Connection name |
| `--connection-string` | yes | Database connection string |

#### `db-connection delete`

Delete a database connection definition.

```sh
scadabridge --url <url> db-connection delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | Database connection ID |

---

### `api-method` — Manage inbound API methods

#### `api-method list`

List all inbound API method definitions.

```sh
scadabridge --url <url> api-method list
```

#### `api-method get`

Get a single inbound API method by ID.

```sh
scadabridge --url <url> api-method get --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | API method ID |

#### `api-method create`

Create a new inbound API method.

```sh
scadabridge --url <url> api-method create --name <string> --script <string> [--timeout <int>] [--parameters <json>] [--return-def <json>]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--name` | yes | — | Method name (used as the URL path segment in `POST /api/{methodName}`) |
| `--script` | yes | — | Script source code implementing the method |
| `--timeout` | no | `30` | Timeout in seconds |
| `--parameters` | no | — | Parameter definitions JSON |
| `--return-def` | no | — | Return type definition JSON |

#### `api-method update`

Update an inbound API method. An update **replaces** the whole entity — every required
field below must be supplied, even if unchanged.

```sh
scadabridge --url <url> api-method update --id <int> --script <string> [--timeout <int>] [--parameters <json>] [--return-def <json>]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--id` | yes | — | API method ID |
| `--script` | yes | — | Script source code implementing the method |
| `--timeout` | no | `30` | Timeout in seconds |
| `--parameters` | no | — | Parameter definitions JSON |
| `--return-def` | no | — | Return type definition JSON |

Script changes take effect immediately — the updated code is recompiled in-memory on the active central node. No restart is required.

#### `api-method delete`

Delete an inbound API method.

```sh
scadabridge --url <url> api-method delete --id <int>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--id` | yes | API method ID |

---

### `bundle` — Transport bundle export / preview / import

Export, preview, and import Transport (#24) bundles. Bundles carry templates, shared
scripts, external systems, database connections, notification lists, SMTP configurations,
and API methods between environments. As of M8 (T18) they also carry **site-scoped**
configuration — `Site` definitions, site-scoped `DataConnection`s (protocol connections),
and `Instance`s (with their override children and area membership). Inbound API keys are
**not** transported — recreate them on the destination via the CLI or UI.

Bundle commands use a 5-minute timeout (larger payloads may be slow over WAN).

#### `bundle export`

Export selected entities to a `.scadabundle` file.

```sh
scadabridge --url <url> bundle export --output <path> [--passphrase <string>] [--all] [--include-dependencies] [entity-selectors]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--output` | yes | Output file path (e.g. `env-baseline.scadabundle`) |
| `--passphrase` | no | Encryption passphrase; omit to produce an unencrypted bundle |
| `--all` | no | Export every entity of every supported type (ignores per-type name flags) |
| `--include-dependencies` | no | Pull transitive dependencies (referenced shared scripts, parents, composed members) into the bundle |
| `--templates` | no | Comma-separated template names to include |
| `--shared-scripts` | no | Comma-separated shared-script names to include |
| `--external-systems` | no | Comma-separated external-system names to include |
| `--db-connections` | no | Comma-separated database-connection names to include |
| `--notification-lists` | no | Comma-separated notification-list names to include |
| `--smtp-configs` | no | Comma-separated SMTP host names to include |
| `--sms-configs` | no | Comma-separated SMS provider account SIDs to include |
| `--api-methods` | no | Comma-separated API-method names to include |
| `--sites` | no | Comma-separated site identifiers (preferred) or friendly names to include |
| `--instances` | no | Comma-separated instance unique-names to include (also pulls their site + bound data connections) |
| `--source-environment` | no | `SourceEnvironment` value stamped into the bundle manifest (default: `cli`) |

**Example** — export two templates and all their dependencies:

```sh
scadabridge --url <url> bundle export --output baseline.scadabundle \
  --templates "PumpTemplate,TankTemplate" --include-dependencies
```

#### `bundle preview`

Load a bundle and print the diff preview (Added / Modified / Unchanged per entity) without applying any changes. When the bundle carries sites or site-scoped connections, the preview also prints a **required-mapping summary**: for each source site/connection it states either the auto-match (`auto-matches 'X'`) or the directive to supply `--map-site` / `--map-connection` or `--create-missing-*`.

```sh
scadabridge --url <url> bundle preview --input <path> [--passphrase <string>]
```

| Option | Required | Description |
|--------|----------|-------------|
| `--input` | yes | Bundle file path (`.scadabundle`) |
| `--passphrase` | no | Passphrase for encrypted bundles |

#### `bundle import`

Load and apply a bundle with a single global conflict policy. Preview first with `bundle preview` to review the diff and the required-mapping summary.

```sh
scadabridge --url <url> bundle import --input <path> [--passphrase <string>] [--on-conflict <policy>] \
  [--map-site <src=dst> ...] [--map-connection <srcSite/srcName=dstName> ...] \
  [--create-missing-sites] [--create-missing-connections]
```

| Option | Required | Default | Description |
|--------|----------|---------|-------------|
| `--input` | yes | — | Bundle file path (`.scadabundle`) |
| `--passphrase` | no | — | Passphrase for encrypted bundles |
| `--on-conflict` | no | `overwrite` | Resolution policy for `Modified` rows: `skip`, `overwrite`, or `rename` |
| `--map-site` | no | — | Map a source site to a destination: `srcIdentifier=dstIdentifier`. A token with no/empty `=dst` means **create-new**. Repeatable. |
| `--map-connection` | no | — | Map a source connection to a destination: `srcSiteIdentifier/srcName=dstName`. A token with no/empty `=dst` means **create-new**. Repeatable. |
| `--create-missing-sites` | no | `false` | Create any still-unmapped/unmatched source site from the bundle payload instead of aborting |
| `--create-missing-connections` | no | `false` | Create any still-unmapped/unmatched source connection from the bundle payload instead of aborting |

Site identifiers and connection names are environment-specific; the importer first auto-matches them (sites by `SiteIdentifier`, connections by the site-qualified `{SiteIdentifier}/{Name}`). Any source reference that neither auto-matches nor is mapped/created aborts the import. Imported instances always land `NotDeployed` — redeploy via the normal flow.

---

### `cached-call` — Retry or discard parked cached-call operations

Manage parked `ExternalSystem.CachedCall` and `Database.CachedWrite` operations at a
site. Both sub-commands relay via the central `SiteCallAuditActor` → site relay and
require the **Deployer** role.

#### `cached-call retry`

Retry a parked cached-call operation at the specified site.

```sh
scadabridge --url <url> cached-call retry --site-id <string> --tracked-operation-id <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site identifier where the operation is parked |
| `--tracked-operation-id` | yes | Tracked operation ID (MessageId) of the parked cached call |

#### `cached-call discard`

Discard a parked cached-call operation at the specified site.

```sh
scadabridge --url <url> cached-call discard --site-id <string> --tracked-operation-id <string>
```

| Option | Required | Description |
|--------|----------|-------------|
| `--site-id` | yes | Site identifier where the operation is parked |
| `--tracked-operation-id` | yes | Tracked operation ID (MessageId) of the parked cached call |

---

## Architecture Notes

The CLI connects to the Central cluster over **HTTP** (`ManagementHttpClient`), posting to the `/management` endpoints at the configured `managementUrl` — normally the Traefik load balancer, which routes to the active central node. It carries HTTP **Basic** credentials from `--username`/`--password`. Central's endpoint handler asks the `ManagementActor` in-process via `ManagementActorHolder`.

There is no Akka dependency in the CLI at all: it does not join the cluster, does not use `ClusterClient`, and does not contact a `ClusterClientReceptionist`. (Earlier revisions of this document described a ClusterClient transport that was never built.)

An `HttpClient` is created per command invocation and disposed when the command completes.

Role enforcement is applied by the ManagementActor on the server side. The CLI authenticates against LDAP using `--username` / `--password`, resolves LDAP group memberships, then maps groups to ScadaBridge roles (Admin, Design, Deployment) via role mappings configured in the security settings. Operations require the appropriate role — for example, creating templates requires `Design`, deploying requires `Deployment`. In the test environment, use the `multi-role` user (password: `password`) which has all three roles.

## Issues & Missing Features

If you encounter bugs, unexpected behavior, or missing features in the CLI, log them in [`cli_issues.md`](../../cli_issues.md) in the project root. Include a brief description, the command involved, and any relevant error output.
