# Covers Report

Operator-facing screen that tracks **ladle covers** in the caster area, the
**heats** currently sitting on each cover, and the **temperatures** each
cover's thermocouples are reporting.

Path in the app: **Caster → Covers Report** (`/!/covers-report`).

> Looking for the **technical reference** (table schemas, proc signatures,
> file map, conventions)? See [`CLAUDE.md`](./CLAUDE.md) in this same folder.

---

## What is a "cover" in this context?

A **cover** is the insulated lid placed on a steel ladle between the LMF
and the Caster to retain heat during transit. Each cover has up to two
thermocouple devices wired into a wireless gateway:

- **Sensor A** (slot 1) — primary thermocouple
- **Sensor B** (slot 2) — secondary thermocouple (optional; not every cover
  has one)

When a ladle is moved under a cover, an operator **assigns** the heat
(by name, e.g. `H123456`) to that cover on this screen. Later, when the
ladle moves out, the operator **unassigns** the heat. The system records
the start and end times so we can:

- compute **time under cover** for each heat (how long the cover retained
  heat before tap)
- compare time-under-cover against the temperature drop curve
- alert when a heat has been under cover longer than a configured
  threshold (e.g. > 24 h might mean a stuck ladle)

---

## What the screen shows

Two AG Grids on the same page:

### Top — History grid

One row per **heat × cover** assignment that started inside the chosen
date range. Columns:

| Column | Meaning |
|---|---|
| Cover | Cover number the heat was on |
| Heat | Heat name |
| Started | When the heat was assigned (local time) |
| Ended | When the heat was unassigned (blank if still on cover) |
| Under Cover | Duration; if `Ended` is blank, ticks up against `now` |

The grid is **only refreshed manually** — pick a date range at the top of
the screen and click **Refresh**.

### Bottom — Realtime grid (auto-refreshes every 30 s)

Grouped by Cover (rows that read `Cover 2`, `Cover 3`, …). Inside each
group, one row per heat currently assigned to that cover. Columns:

| Column | Meaning |
|---|---|
| Heat | Heat name currently on this cover |
| Started | When this heat was assigned |
| Under Cover | Running duration |
| S. A | Latest TempSensor1 reading (°F) |
| S. B | Latest TempSensor2 reading (°F) |
| Status | `G` (good reading in last 30 min) or `L` (lost / stale) |
| Peak | Maximum TempSensor1 since the heat was assigned |
| ✏️ | Edit / Unassign action |
| 📈 | Open per-cover temperature history chart |
| 🔔 | Edit "Out of Cover Alert" threshold (in hours) |

Click the **pencil** on a row with a heat → **Unassign** modal. Click the
pencil on a row with no heat → **Assign** modal. Click **Assign Cover**
above the grid to assign to any available cover.

---

## How assign / unassign works (end-to-end)

```
                    USER ─► Assign modal ─► Pick cover + heat (+ optional Start)
                                                    │
                                                    ▼
                                CAS.UpdateCoverHeat(@CoverId, @HeatName, @action,
                                                    @Started, @timeZoneCode,
                                                    @currentUser)
                                                    │
                                                    ▼
                                ┌───────────────────┼───────────────────┐
                                │                   │                   │
                                ▼                   ▼                   ▼
                       CAS.CoverHeats    CAS.CoverHeatsEvents     SYSTEM.AuditMsg
                       (current state)   (append-only log)        (cross-app audit)
                                │
                                ▼
            CAS.GetCoversReport @realTime=1   ◄── pulled by the realtime grid
            CAS.GetAvailableCovers            ◄── feeds the next modal open
```

---

## Where the temperature data comes from

```
   Wireless thermocouples on covers (the gateway pushes readings)
                            │
                            ▼
              Covers SQL database  (a separate SQL Server DB)
                            │
                            ▼
       SQL Agent: CAS.Job_TrackCoverTemps   (every 30 seconds)
        │  Routes its source query automatically:
        │    - If the Covers DB is on THIS instance → direct query
        │    - Otherwise → OPENQUERY([Leap_1], …)
        │  Maps device IDs → cover numbers via CAS.CoverSensorMap.
        ▼
   CAS.CoverTemps           ◄── time-series  (the "historian")
   CAS.CoverHeats           ◄── current state (placeholder rows + assignments)
                            │
                            ▼
   This screen + the Covers Temperatures screen
```

The job is documented in detail (logs, branches, observability, mapping
behaviour) in [`CLAUDE.md`](./CLAUDE.md).

---

## Operator quick-reference

**A reading isn't showing up for a cover.**
The thermocouple sends a value but you don't see it on the screen.

1. Check the most recent log:
   ```sql
   SELECT TOP 3 [Timestamp], [Message]
   FROM [SYSTEM].[Logs]
   WHERE Procedure_ = 'CAS.Job_TrackCoverTemps'
   ORDER BY [Timestamp] DESC;
   ```
   The message tells you (a) which environment branch fired, (b) how many
   rows landed, (c) how many `IsSensorLive` flips happened, and (d) how
   many devices on the source aren't yet in the map. *Unmapped devices*
   is the typical reason a new sensor is silent.

2. If the device IS in the map but no rows are arriving — the gateway is
   probably offline. Check `Covers.dbo.Reading` directly on the dev box;
   if it's not getting new rows there, the gateway side needs attention,
   not this app.

**A cover went stale ("L" status).**
The cover has had a heat assigned but the screen shows `Status = L` and
the temperatures haven't updated. The job's 30-min freshness rule flipped
`IsSensorLive` to 0 because no reading arrived in that window. Either the
device went offline or the gateway lost contact. Once readings resume,
the next tick flips it back to 1 (and the log records "1 IsSensorLive
flips").

**A heat shows the wrong start time.**
`Started` is shown in local time; it's stored as UTC. If you manually
chose a Start time when you assigned and it looks off:

- The modal asks for **local** time.
- We pass the user's timezone code (`app.models.user.get('timezoneCode')`)
  along with the modal value.
- `UpdateCoverHeat` does `IH.ToUtc(@Started, @timeZoneCode)` before
  storing.

If your account's timezone is misconfigured this is where it'll show.

---

## Files in this folder

```
covers-report/
├── README.md                ← you are here
├── CLAUDE.md                ← agent-oriented technical reference
├── covers-report.aspx       ← ASPX template (HTML scaffolding + i18n)
├── covers-report.js         ← screen logic: 2 grids, modals, refresh tick
├── covers-report.sql        ← roles, menu route, SP permissions (one-time deploy)
└── App_LocalResources/      ← .resx localization
```

---

## Related code outside this folder

| Path | Role |
|---|---|
| `Website/.../app/pages/caster/covers-temperatures/` | Sister screen — pivoted temperatures monitor |
| `Website/.../app/components/modal-covers/` | The Assign / Unassign / Edit-Alert modal |
| `Website/.../app/components/modal-temperatures-history/` | Per-cover history chart modal (📈 icon) |
| `mes-db/CAS/Tables/CoverTemps.sql` | Temperature time-series |
| `mes-db/CAS/Tables/CoverHeats.sql` | Current assignments + flags |
| `mes-db/CAS/Tables/CoverHeatsEvents.sql` | Append-only audit log |
| `mes-db/CAS/Tables/CoverSensorMap.sql` | DeviceId → (CoverNumber, SensorSlot) routing |
| `mes-db/CAS/Stored Procedures/Job_TrackCoverTemps.sql` | The Agent job |
| `mes-db/CAS/Stored Procedures/UpdateCoverHeat.sql` | Assign / Unassign / Edit-Alert |
| `mes-db/CAS/Stored Procedures/GetCoversReport.sql` | Feeds the two grids on this screen |
| `mes-db/CAS/Stored Procedures/GetAvailableCovers.sql` | Feeds the modal dropdowns |
| `mes-db/CAS/Stored Procedures/GetLastCoverTemperatures.sql` | Feeds the sister screen |
| `mes-db/CAS/Stored Procedures/GetTemperaturesByCover.sql` | Feeds the per-cover chart modal |

---

## Glossary

| Term | Meaning |
|---|---|
| **Cover** | Insulated lid placed on a ladle to retain heat |
| **Heat** | A batch of liquid steel identified by a heat number (e.g. `H123456`) |
| **Under cover** | The ladle currently has a cover on it |
| **Time under cover** | Duration between Assign and Unassign (or to "now" if still assigned) |
| **Cover number** | Internal numeric ID of a physical cover (e.g. Cover 2, Cover 3) |
| **Device ID** | The thermocouple sensor's serial number on the wireless gateway |
| **Sensor slot** | 1 = primary (TempSensor1), 2 = secondary (TempSensor2) |
| **Gateway** | The wireless receiver that collects readings from the cover thermocouples |
| **Leap_1** | The linked server name pointing at the host that runs the Covers DB in production |
| **Out of Cover Alert** | Threshold in hours; if a heat stays under cover longer than this, the row turns red on the realtime grid |
| **IsSensorLive** | DB flag; set by the job to 1 if the cover got a reading in the last 30 min, 0 otherwise |
| **isAvailable** | DB flag; set by `UpdateCoverHeat`; 0 means the cover is at its capacity limit (10 simultaneous heats) |
