# CLAUDE.md — ladle-wall-manager screen

## Purpose

Operator-facing screen used by the ladle wall crew at SDI Columbia to:

1. View per-ladle equipment counters (Plate Assembly, Upper Nozzle, Working Lining, Porous Plug North, Porous Plug South) and their last-modified timestamps.
2. Increment/decrement counters, **reset** them (which bumps the campaign for Working Lining), or restore them to a previous heat's value.
3. Send a ladle to a destination via the top button row: EAF A, EAF B, Maintenance, Drain.
4. Toggle a ladle's **ON HOLD** state (added in [dexteel/srd-ms#1278](https://github.com/dexteel/srd-ms/issues/1278)) — see the dedicated section below.

The screen is a Backbone.js + Backbone.Epoxy view living inside the ASP.NET IHBox shell. See `app/CLAUDE.md` for the SPA-wide conventions; the rest of this file documents what's specific to *this* screen.

---

## Files in this folder

| File | Purpose |
|---|---|
| `ladle-wall-manager.aspx` | HTML template — table layout: top button row, then a row per equipment piece |
| `ladle-wall-manager.js` | Backbone.Epoxy `Screen.Models.Main` + `Screen.Views.Main`. Single AMD module. |
| `ladle-wall-manager.css` | Screen-specific styles. Bootstrap 2.x utility classes for buttons. |
| `ladle-wall-manager.sql` | **Module setup script** — NOT a schema definition. See "The .sql file" below. |
| `ladle-wall-manager.jpg` | Static menu thumbnail. |
| `App_LocalResources/ladle-wall-manager.aspx.resx` | English-only `.resx` for the screen. **No `.es` / `.pt` / `.fr` variants exist.** Most labels in this screen are hardcoded in the .aspx, not localized — when adding a new label, follow that existing inconsistency rather than introducing partial i18n. |

---

## Architecture overview

### Model (Backbone.Epoxy)

`Screen.Models.Main` holds the currently-selected ladle's state. Naming pattern for equipment:
- `prev<EquipmentName>` — current value displayed in the input
- `prev<EquipmentName>Changed` — counter for unsaved-change tracking
- `equip<EquipmentName>Id` — currently-selected catalog equipment id
- `lm<EquipmentName>` — last-modified timestamp string

Backbone.Epoxy `computeds` are supported here — `onHoldButtonLabel` is the only one currently defined. Bindings in the .aspx use `data-bind="...: prop"` and helpers like `not()` / `any()` / `classes`.

### Collections

- `ladleCollection` — list of all ladles (from `LAD.GetLadles`)
- `ladleHistoryCollection` — pivoted heat history for the selected ladle (from `LAD.GetLadleInfo`, result set 1)
- `ladleHistoryEquipments` — per-equipment current state for the selected ladle (`GetLadleInfo` result set 2)
- `equipmentsCatalog` — full equipment catalog (`GetLadleInfo` result set 3)
- `equipmentsTypes` — equipment type catalog (from `LAD.GetEquipmentTypes`, used to translate UI element name attribute → equipment type ID)

### View lifecycle

1. `render()` calls `fetchLadles` (populates the left-side ladle picker via `ladleSubNavBar`) and `fetchEquipmentTypes`.
2. Operator clicks a ladle button → `fetchLadleInfo(ladleId)` → triggers `'ladle-history-fetched'` → `populateTable` runs.
3. `populateTable` reads `ladleHistoryCollection.at(0)` (the most recent row, sorted by `StartTap desc`) and copies fields onto the model: `prevHeatName`, `ladleCampaignValue`, `prevPlateAssembly`, ..., `onHold`. It also formats the `lm*` timestamps via moment.js.
4. Any write action triggers `'equipment-changed'` → `saveComplete` → `that.refresh()` → re-fetch.

### Connection string

**All SP calls — including writes — use `app.ConnectionStrings.app`** (the read-only `APP` user). Writes work anyway because of the secured-procedure mechanism: each write SP has a `securedsp-<SchemaName>.<ProcName>` extended property granted to the `lw_w` role in `ladle-wall-manager.sql`, which `DALSecurity.CallSecuredProcedure` (in the `Tools/Security` project) honors by switching to encrypted SA credentials. **If you add a new write SP and forget the securedsp grant, the call will silently fail or get permission-denied.**

### Async pattern

Read SPs use `Async: true`, write SPs use `Async: false` (so the UI button-disable cycle reflects the actual response). All write methods call `this.disableButtons()` at the start and rely on `that.trigger('equipment-changed')` → `saveComplete` → `enableButtons` to re-enable.

### Refresh strategy

Writes do NOT optimistically update the model. They trigger `'equipment-changed'`, which causes `that.refresh()` → `fetchLadleInfo` → `populateTable`, which re-binds the model from the server response. Server is the source of truth. This is intentional — don't introduce optimistic updates.

---

## The `.sql` file (`ladle-wall-manager.sql`)

This file is a **run-once module setup script** that gets executed manually against the `IHBoxSystem` database when the screen is first installed. It contains:

1. **Role creation**: `lw_r` (read), `lw_w` (write), with `admin` added to both.
2. **Securedsp grants**: `sp_addextendedproperty` calls naming `securedsp-<SchemaName>.<ProcName>` against the appropriate role. **Any new write SP called from the screen must have a grant added here AND executed manually on `SRD-MESDEV.IHBoxSystem` after the deploy.**
3. **Route registration**: inserts into `[FrontEnd].[Routes]` and `[FrontEnd].[Menu]` so the screen appears in the navigation.
4. **Stale embedded SPs** at lines 131–401: `LAD.GetLadleInfo`, `LAD.ChangeLadleEquipment_new`, `LAD.GetEquipmentTypes`. **These are NOT the canonical sources.** They have drifted from the SSDT versions in `mes-db/LAD/Stored Procedures/`. **Never edit them.** A future cleanup PR should delete this section. The SSDT versions are authoritative.

### Adding a new write SP — checklist

When you create a new write SP that the screen will call:
1. Create the SP in `mes-db/LAD/Stored Procedures/<Name>.sql` (formal template, see below).
2. Add the securedsp grant to this `ladle-wall-manager.sql` file.
3. Execute that single grant statement manually against `SRD-MESDEV.IHBoxSystem` after publishing — the .sql file is not auto-run.

---

## Database — stored procedures the screen calls

### Reads (all on `app` connection string, `Async: true`)

| SP | Purpose | Result sets |
|---|---|---|
| `LAD.GetLadles` | List of all ladles | 1: ladle list |
| `LAD.GetLadleInfo` | Per-ladle history + current equipment + catalog | 1: pivoted heat history (incl. `OnHold`); 2: current equipments; 3: all equipments |
| `LAD.GetEquipmentTypes` | Equipment type catalog (for name → ID lookups in JS) | 1: type list |

### Writes (all on `app` connection string, `Async: false`)

| SP | Trigger | Notes |
|---|---|---|
| `LAD.ChangeLadleEquipmentSingle` | Reset / Increment / Decrement buttons on any equipment row | When `@EquipmentTypeID = 49 AND @NewValue = 0` (Working Lining reset), bumps the campaign **and** auto-clears `OnHold` via `LAD.SetLadleOnHold` |
| `LAD.ChangeLadlePositionWeb` | EAF A / EAF B / Maintenance buttons | Calls `ChangeLadlePosition` server-side |
| `LAD.DrainLadle` | Drain button | Logs into `LAD.LadleDrains`; affects WL counter via `correctedHeatcount` |
| `LAD.SetLadleOnHold` | ON HOLD button (added in #1278) | See the ON HOLD section |

### Equipment type IDs (from `LAD.catEquipmentTypes`)

| TypeID | Name |
|---|---|
| 49 | WorkingLining (drains add to its counter via `correctedHeatcount = HeatCount + Offset + Drains`) |
| 58 | PorousPlugNorth |
| 59 | PorousPlugSouth |

The other types (PlateAssembly, UpperNozzle) are also in this catalog — `GetLadleInfo` joins to `[$(IHBoxSystem)].equip.EquipmentTypes` to materialize the names. The pivoted columns in result set 1 are `[PlateAssembly], [UpperNozzle], [WorkingLining], [PorousPlugNorth], [PorousPlugSouth]`.

### `GetLadleInfo` shape gotcha

Result set 1 is a PIVOT over `#t2`, which is built from `#t` (the heat row) plus `LAD.LadleDrains` rows inserted with an explicit column list (`HeatId='Drain'`, `TapTime`, `startTap`). **Drain rows have NULL for any column that wasn't in the explicit insert list.** When adding a new ladle-level field to result set 1, derive it from a scalar subquery against `LAD.Ladles` instead of putting it on `#t` — otherwise drain rows that happen to sort first will return NULL and the screen will see the wrong value. The current `OnHold` derivation does this correctly (line 53 — `OnHold = (select OnHold from LAD.Ladles where LadleID = @LadleId)`).

---

## Stored procedure style

The LAD schema has two coexisting generations:

- **Informal/legacy** (~60% of LAD procs): early `RETURN` on validation, `TRY/CATCH` for error handling, lowercase keywords, result-set error responses. Examples: `Reladle`, `DrainLadle`, `ForceOpenLadleEvent`, `CreateHeat`.
- **Formal** (~10%, newer): `SET NOCOUNT ON`, savepoint-based nested transactions, `@result` tracking, `THROW` for errors, `SYSTEM.LogMsg` for audit. Example: `ChangeLadleEquipmentSingle`, `SetLadleOnHold`.

The repo-level `mes-db/CLAUDE.md` says **new procedures must follow the formal template**. When adding a new write SP for this screen, follow `LAD.SetLadleOnHold` (closest sibling) or `LAD.ChangeLadleEquipmentSingle` as the pattern. Note that `SYSTEM.AuditMsg` has its parameters out of order vs. the template in `mes-db/CLAUDE.md` — the actual signature is `(@message, @moduleName, @currentUser, @checkValidation)`, so always call it with named parameters: `EXEC SYSTEM.AuditMsg @message=@msg, @moduleName='Ladles', @currentUser=@CurrentUser`.

---

## ON HOLD feature ([#1278](https://github.com/dexteel/srd-ms/issues/1278))

Added a single-button toggle in the top row that puts a ladle "ON HOLD" — a state that signals the PLC (via `LAD.L1Commands`) to restrict the ladle from being used.

### What changed

**Database (`mes-db/LAD/`)**

1. `Tables/Ladles.sql` — added `[OnHold] BIT NOT NULL CONSTRAINT [DF_Ladles_OnHold] DEFAULT ((0))`.
2. `Stored Procedures/SetLadleOnHold.sql` — **new**. Validates inputs, idempotent guard (no-op if state already matches), hardcoded `CASE @LadleID` mapping LadleID 1–16 to PLC tag suffix `_00`–`_15` (LadleID `N` → suffix zero-padded `N-1`), savepoint pattern, `LAD.L1Commands` insert (skipped with WARN log if tag is unmapped), `SYSTEM.AuditMsg` audit, `SYSTEM.LogMsg` debug.
3. `Stored Procedures/Reladle.sql` — auto-clears OnHold on **both** old and new ladle (when set) after the equipment-counter increments, before the result message.
4. `Stored Procedures/ChangeLadleEquipmentSingle.sql` — auto-clears OnHold inside the existing WL-reset branch (`IF @NewValue=0 AND @EquipmentTypeID=49`), runs inside the outer `BEGIN TRAN`/`COMMIT TRAN` and participates via savepoint.
5. `Stored Procedures/GetLadleInfo.sql` — exposes `OnHold` to the screen via a scalar subquery in the final pivot projection: `OnHold = (select OnHold from LAD.Ladles where LadleID = @LadleId)`.

**Frontend (this folder)**

6. `ladle-wall-manager.sql` — added `securedsp-LAD.SetLadleOnHold` grant with `SendCurrentUser=TRUE`.
7. `ladle-wall-manager.aspx` — new `<td class="onhold-label-column">` between Drain and the ladle-campaign cell, containing `#onHoldBtn` bound to `text: onHoldButtonLabel, classes: { 'btn-danger': onHold, 'btn-warning': not(onHold) }`.
8. `ladle-wall-manager.css` — `td.onhold-label-column` and `button#onHoldBtn` rules matching the neighboring drain/maintenance button shape.
9. `ladle-wall-manager.js` — `onHold: false` model default; `computeds.onHoldButtonLabel`; new `saveOnHold` method (Async false, triggers `equipment-changed`); `'click #onHoldBtn': this.toggleOnHold` events entry; `toggleOnHold` view method using `Modal.Views.Main` `CONTINUE-CANCEL`; one line in `populateTable` to copy `OnHold` from the fetched data into the model.
10. Drive-by typo fix: `saveEquipmentSingle` had `Type: "NT"` for `@NewValue` (line 273) — now correctly `Type: "INT"`.

### Behavior

1. Operator clicks `PUT ON HOLD` → modal confirms → `LAD.SetLadleOnHold @LadleID, @OnHold=1` → DB column flips to `1`, a `WriteBooleanTag <tag>=1` row is inserted into `LAD.L1Commands`, screen refreshes via the existing `equipment-changed → saveComplete → refresh` chain, button flips to red `RELEASE HOLD`.
2. If the operator then clicks `Reset` on Working Lining (or someone reladles the ladle), `ChangeLadleEquipmentSingle` / `Reladle` calls `EXEC LAD.SetLadleOnHold @OnHold=0` automatically, which writes `=0` to L1Commands and clears the BIT. The screen sees the new state on the next fetch.
3. The SP is **idempotent**: if you call it with the value the ladle already has, it's a no-op (no UPDATE, no L1Commands insert). This prevents L1Commands spam from auto-clear paths firing on ladles that were never on hold.

### PLC tag mapping

Hardcoded `CASE @LadleID` in `LAD.SetLadleOnHold`. LadleID 1 → `EAF_COMMON.PLC.Global.AT_Ladle_Restricted_x.AT_Ladle_Restricted_00`; LadleID 16 → `..._15`. The tag is formatted as `'WriteBooleanTag  ' + <tagName> + '=' + (1 or 0)`. **Adding a new ladle requires editing this CASE.** If `@tagName` is NULL (unmapped ladle), the SP still flips the BIT but skips the L1Commands write and emits a `SYSTEM.LogMsg WARN` row.

### Reladle scope decision

When `Reladle` runs, **both** the old and new ladle have OnHold cleared (whichever had it set). This was an explicit user choice — the literal interpretation of "when called, also disable OnHold if it was set". If operations later wants only one side cleared, the change is one `IF EXISTS` block in `Reladle.sql`.

---

## Things to watch out for

1. **`Type: "NT"` typo class.** `Core.Database.QueryParameters.Add(name, type, value)` takes a type string. Valid values include `INT`, `BIT`, `VARCHAR`, `DATETIME`, `FLOAT`, `BIGINT`. There's no validation — a typo silently produces a malformed parameter and the SP call fails server-side. The original `saveEquipmentSingle` had `"NT"` for two years before it was caught. Double-check type strings when copying patterns.
2. **Stale embedded SPs in `ladle-wall-manager.sql`** (lines 131–401). Don't edit. Mention as cleanup in any PR you write that touches this file.
3. **The screen does NOT auto-refresh on external events.** If a ladle is reladled or its OnHold is cleared by another process while the operator has the screen open, they won't see the new state until they manually re-click the same ladle button. Consistent with the rest of this screen.
4. **`selectedLadleId` is the source of truth for "which ladle is being edited"** — set in `ladleSubNavBar` when the operator clicks a ladle button. All write SPs read it from the model, not from input fields.
5. **`ladleHistoryCollection.at(0)` is the most recent row but may be a `Drain`** rather than a heat. The screen handles this by displaying `'Drain'` as the heat name. When adding new fields to that result set, source them from a scalar subquery against `LAD.Ladles` so they don't end up NULL on drain rows (see `OnHold` for the pattern).
6. **`@CurrentUser` casing inconsistency** between LAD procs (`@CurrentUser`) and the formal template (`@currentUser`). SQL Server is case-insensitive for parameter names, so it works either way — but use `@CurrentUser` in new LAD procs to match the immediate sibling style, then call them with named parameters from elsewhere.
7. **No automated tests.** All validation is manual against `SRD-MESDEV`. There is no test runner or CI for this screen — verify changes by clicking through them in the dev IIS deployment.

---

## Useful related files

- `mes-db/LAD/Tables/Ladles.sql` — table definition
- `mes-db/LAD/Tables/L1Commands.sql` — PLC command queue
- `mes-db/LAD/Tables/catEquipmentTypes.sql` — equipment type catalog
- `mes-db/LAD/Stored Procedures/GetLadleInfo.sql` — main read SP
- `mes-db/LAD/Stored Procedures/SetLadleOnHold.sql` — ON HOLD write SP (canonical reference for the formal template style in this schema)
- `mes-db/LAD/Stored Procedures/ChangeLadleEquipmentSingle.sql` — reset/increment/decrement entry point
- `mes-db/LAD/Stored Procedures/Reladle.sql` — heat-between-ladles transfer (note: also auto-clears OnHold)
- `mes-db/LAD/Stored Procedures/ForceOpenLadleEvent.sql` — closest existing reference for the L1Commands `WriteBooleanTag` pattern
- `mes-db/CLAUDE.md` — repo-level mes-db conventions, formal SP template
- `Website/IHBox/IHBoxWebsite/app/CLAUDE.md` — SPA-wide conventions
