# WhileTrue Trigger Mode for Conditional & Expression Script Triggers — Design

**Date:** 2026-05-18
**Status:** Implemented

## Context

Template script triggers of type `Conditional` and `Expression` currently fire
*once* when their condition first holds:

- **Expression** is edge-triggered — the script runs once per `false→true`
  transition of the boolean expression.
- **Conditional** runs the script on each change of the monitored attribute for
  which the `{operator, threshold}` comparison is true.

There is no way to keep a script running *while* a condition remains true. This
design adds a second mode, **WhileTrue**, alongside the existing behavior
(referred to as **OnTrue**).

### Decisions taken during brainstorming

- **WhileTrue is a timer re-fire cadence.** On the `false→true` edge the script
  fires immediately; while the condition stays true it re-fires on a periodic
  timer; on the `true→false` edge the timer stops. It re-fires even if no
  attribute updates arrive (e.g. a static condition).
- **The re-fire interval is the script's existing `MinTimeBetweenRuns`** — no
  new configuration field. One value serves as both the WhileTrue cadence and
  the general per-script throttle.
- **OnTrue is unchanged.** A trigger config with no `mode` field parses as
  `OnTrue`, so every existing deployed template behaves identically.
- **Alarms are out of scope** — alarm triggers are already level-based ("active
  while true").

## Design

### 1. Trigger model & storage

`Conditional` and `Expression` trigger `TriggerConfiguration` JSON gains an
optional `mode` field:

| Trigger     | JSON shape                                              |
|-------------|---------------------------------------------------------|
| Conditional | `{ attributeName, operator, threshold, mode? }`         |
| Expression  | `{ expression, mode? }`                                 |

`mode` is `"OnTrue"` (default; absent ⇒ `OnTrue`) or `"WhileTrue"`. Entity
types are unchanged — `TemplateScript.TriggerConfiguration` stays `string?`.

### 2. Runtime evaluation (`ScriptActor`)

The internal trigger-config records gain a `Mode`:

```csharp
internal enum TriggerMode { OnTrue, WhileTrue }
internal record ConditionalTriggerConfig(
    string AttributeName, string Operator, double Threshold, TriggerMode Mode) : ScriptTriggerConfig;
internal record ExpressionTriggerConfig(string Expression, TriggerMode Mode) : ScriptTriggerConfig;
```

**OnTrue** — current behavior, unchanged:
- Conditional: fire on each monitored-attribute change for which the comparison
  is true.
- Expression: fire once per `false→true` transition.

**WhileTrue** — the actor tracks the condition's current truth state:
- **`false→true` edge:** fire once immediately (via `TrySpawnExecution`, which
  still respects `MinTimeBetweenRuns` against any prior run), then start a
  periodic Akka timer with both initial delay and interval set to
  `MinTimeBetweenRuns`.
- **Each timer tick:** spawn an execution **directly** — the timer interval is
  itself the cadence, so ticks bypass the `MinTimeBetweenRuns` skip-check (which
  could otherwise drop a tick to sub-millisecond timing jitter).
- **`true→false` edge:** cancel the timer.
- The state transitions naturally re-arm: a later `true` after a `false` starts
  a fresh cycle.

Truth-state sources:
- Conditional depends only on the monitored attribute, so it is re-evaluated on
  each change of that attribute (unchanged gate).
- Expression is re-evaluated on every attribute change (unchanged).

A throwing/non-bool expression is treated as `false` (existing behavior); for
WhileTrue that cleanly stops the timer.

**Edge case — WhileTrue with no `MinTimeBetweenRuns`:** a periodic timer has no
interval to use, so the trigger degrades to a single edge fire and logs a
warning. Deployment-time validation of this combination is a deliberate
non-goal of this change.

New surface in `ScriptActor`: a `TriggerMode` on the two config records, one
`WhileTrueTick` internal message, and start/stop timer helpers (the actor
already implements `IWithTimers` for Interval triggers). Timer key
`"whiletrue-trigger"`.

### 3. Editors & codec

- **`ScriptTriggerConfigCodec`:** `ScriptTriggerModel` gains a `Mode`
  (`ScriptTriggerMode` enum, default `OnTrue`). `Parse` reads `mode` for the
  Conditional and Expression kinds; `Serialize` writes it for those kinds.
  Absent/unrecognized `mode` ⇒ `OnTrue`.
- **`ScriptTriggerEditor.razor`:** the Conditional and Expression panels gain a
  mode `<select>` — "When the condition becomes true — once" (`OnTrue`) vs
  "Repeatedly while true" (`WhileTrue`) — with a hint that WhileTrue re-fires at
  the script's *Min time between runs* interval. `BuildHint()` is updated to
  describe the selected mode.

### 4. Error handling

- Expression throws / returns non-bool → treated as `false` (logged as a script
  error, as today); for WhileTrue this stops the re-fire timer.
- Malformed trigger JSON → trigger parses as inert (existing behavior).
- WhileTrue with no `MinTimeBetweenRuns` → single fire + warning (see above).

### 5. Testing & verification

- **Runtime (`ScriptActorTests`)** — a script body that calls
  `Instance.SetAttribute(...)` is observable as a `SetStaticAttributeCommand` on
  the instance-actor `TestProbe`; with a short `MinTimeBetweenRuns`:
  - WhileTrue conditional & expression: fire on the `false→true` edge, re-fire
    on the timer, stop on `true→false`, re-arm on a later `true`.
  - OnTrue regression: conditional & expression fire exactly as before.
  - WhileTrue with no `MinTimeBetweenRuns`: a single fire, no repeats.
- **Codec (`ScriptTriggerConfigCodecTests`, new)** — `mode` round-trips for
  Conditional and Expression; absent `mode` ⇒ `OnTrue`; `WhileTrue` serializes.

## Affected files

- `src/ScadaLink.SiteRuntime/Actors/ScriptActor.cs`
- `src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerConfigCodec.cs`
- `src/ScadaLink.CentralUI/Components/Shared/ScriptTriggerEditor.razor`
- `tests/ScadaLink.SiteRuntime.Tests/Actors/ScriptActorTests.cs`
- `tests/ScadaLink.CentralUI.Tests/Shared/ScriptTriggerConfigCodecTests.cs` (new)
- Docs: `docs/requirements/Component-SiteRuntime.md`,
  `docs/requirements/Component-TemplateEngine.md` (note the new mode).
