# Denali Log Analytics — Query Guide for Web App Users

This guide is for engineers, QA analysts, and field-service staff who use the
Denali web application to investigate dialysis machine telemetry logs.

---

## 1. Three Ways to Query Data

The web app offers three query interfaces, each suited to a different depth of
investigation.

| Interface | Where to find it | Best for |
|---|---|---|
| **Analytics tab** | Top navigation → *Analytics* | Point-and-click queries with optional filters; no SQL required |
| **SQL Runner** | Top navigation → *SQL Runner* | Custom SELECT queries; full column and join control |
| **Ask Denali** | Chat icon → *Ask Denali* | Plain-English questions; the AI writes and runs the SQL |

All three interfaces query the same underlying data — a set of pre-built views
and three core tables described in sections 3 and 4 below.

---

## 2. How Data Is Organised

Every file uploaded to Denali is parsed into three curated datasets, all
queryable by **device ID** and **date** (derived from the filename):

```
DVTHD021_20251029.c.log
    │
    ├── processed_logs   — one row per firmware/sensor event
    ├── payload_details  — one row per named payload field per event
    └── alarm_details    — one row per alarm trigger or clear event
```

The **legacy views** (`v_legacy_*`) are pre-joined, pre-filtered SQL views
built on top of those three tables. The Analytics tab exposes them as named
reports. The SQL Runner and Ask Denali let you query them directly by name.

**Device ID and Date** are always the fastest filters. Every table and view is
partitioned by these two values, so adding them to any query dramatically
reduces scan time and cost.

---

## 3. Analytics Tab — Named Reports

### How to use

1. Open the **Analytics** tab.
2. Pick a report from the left panel (e.g., *Heat Disinfect Summary*).
3. Fill in any filter fields that appear (Device ID, Log Date, File Name, etc.).
4. Click **Run Query**.
5. Use the **Export** button to download results as CSV.

Every report has sensible defaults. You do not need to fill in all filters —
leave any field blank to return all matching rows.

---

### 3.1 Legacy Summary Reports

These reports produce one row per clinical cycle. They are the direct
replacement for the treatment, heat-disinfect, and water-flush tabs in the
legacy Excel workbooks.

---

#### Treatment Summary

**What it shows:** One row per HD treatment cycle detected in the log. Shows
pre-treatment start time, treatment start time, end time, total runtime, and
whether the treatment passed, failed, or was cancelled.

**Available filters:** Device ID, Log Date, File Name

**Columns:**

| Column | Type | Meaning |
|---|---|---|
| `deviceid` | text | Device identifier |
| `date` | text `yyyyMMdd` | Log date from filename |
| `source_file_name` | text | Source filename |
| `treatment_num` | integer | Sequential cycle number within the file |
| `pretx_start_time` | timestamp | Start of pre-treatment (mode 5) |
| `tx_start_time` | timestamp | Start of active treatment (mode 6); null if treatment never started |
| `end_time` | timestamp | Time treatment ended |
| `hours_run` | decimal | Total runtime from pre-treatment to end, in hours |
| `final_completion_status` | text | `PASS`, `FAIL`, or `CANCELED` |
| `interruption_alarm_id` | text | Alarm ID that interrupted the cycle, if any |

**Example — all treatments for a device on a given day:**

```sql
SELECT *
FROM diality_logs.v_legacy_treatment_summary
WHERE deviceid = 'DVTHD021'
  AND date = '20251029'
ORDER BY treatment_num;
```

**Example — all failed or cancelled treatments across all files:**

```sql
SELECT deviceid, date, source_file_name, treatment_num,
       hours_run, final_completion_status, interruption_alarm_id
FROM diality_logs.v_legacy_treatment_summary
WHERE final_completion_status IN ('FAIL', 'CANCELED')
ORDER BY date DESC, deviceid;
```

**Example — treatments interrupted by a specific alarm (alarm ID 291):**

```sql
SELECT *
FROM diality_logs.v_legacy_treatment_summary
WHERE interruption_alarm_id = '291'
ORDER BY date DESC;
```

---

#### Heat Disinfect Summary

**What it shows:** One row per heat-disinfect cycle. Shows cooling type
(active vs passive), runtime, completion status, and the last alarm active
before the cycle ended.

**Available filters:** Device ID, Log Date, File Name

**Columns:**

| Column | Type | Meaning |
|---|---|---|
| `deviceid` | text | Device identifier |
| `date` | text `yyyyMMdd` | Log date |
| `source_file_name` | text | Source filename |
| `heat_num` | integer | Sequential cycle number within the file |
| `cooling_type` | text | `Heat Disinfect (Active)` or `Heat Disinfect (Passive)` |
| `start_time` | timestamp | Cycle start |
| `end_time` | timestamp | Cycle end |
| `hours_run` | decimal | Duration in hours |
| `completion_status` | text | `PASS`, `FAIL`, or `CANCELED` |
| `interruption_alarm_id` | text | Alarm ID active at end of cycle, if any |

> **Note:** The status column is named `completion_status`. There is no bare
> `status` column in this view.

**Example — all heat disinfect cycles for a file:**

```sql
SELECT *
FROM diality_logs.v_legacy_heat_disinfect_summary
WHERE source_file_name = 'DVTHD021_20251029.c.log'
ORDER BY heat_num;
```

**Example — compare pass/fail rates by device:**

```sql
SELECT deviceid,
       COUNT(*) AS total_cycles,
       SUM(CASE WHEN completion_status = 'PASS' THEN 1 ELSE 0 END) AS passed,
       SUM(CASE WHEN completion_status = 'FAIL' THEN 1 ELSE 0 END) AS failed,
       ROUND(AVG(hours_run), 2) AS avg_hours
FROM diality_logs.v_legacy_heat_disinfect_summary
GROUP BY deviceid
ORDER BY failed DESC;
```

**Example — passive-cool cycles that failed:**

```sql
SELECT deviceid, date, heat_num, hours_run, interruption_alarm_id
FROM diality_logs.v_legacy_heat_disinfect_summary
WHERE cooling_type = 'Heat Disinfect (Passive)'
  AND completion_status = 'FAIL'
ORDER BY date DESC;
```

---

#### Water Flush Summary

**What it shows:** One row per water flush cycle. Shows start time, end time,
duration, and completion status.

**Available filters:** Device ID, Log Date, File Name

**Columns:**

| Column | Type | Meaning |
|---|---|---|
| `deviceid` | text | Device identifier |
| `date` | text `yyyyMMdd` | Log date |
| `source_file_name` | text | Source filename |
| `flush_num` | integer | Sequential cycle number within the file |
| `start_time` | timestamp | Cycle start |
| `end_time` | timestamp | Cycle end |
| `hours_run` | decimal | Duration in hours |
| `completion_status` | text | `PASS`, `FAIL`, or `CANCELED` |

**Example — all flush cycles for a device across all dates:**

```sql
SELECT deviceid, date, flush_num, start_time, end_time,
       hours_run, completion_status
FROM diality_logs.v_legacy_water_flush_summary
WHERE deviceid = 'HD000116'
ORDER BY date, flush_num;
```

**Example — long flush cycles (over 2 hours):**

```sql
SELECT *
FROM diality_logs.v_legacy_water_flush_summary
WHERE hours_run > 2
ORDER BY hours_run DESC;
```

---

### 3.2 Legacy Diagnostics Reports

---

#### Alarm Status Timeline

**What it shows:** One row per alarm trigger or clear event. Replaces the
alarm-state timeline from the legacy workbooks. Each row shows whether the
alarm was triggered (0300) or cleared (0400), along with the alarm name and
priority from the alarm reference data.

**Available filters:** Device ID, Log Date, File Name, Alarm Code, Time Start,
Time End

**Columns:**

| Column | Type | Meaning |
|---|---|---|
| `deviceid` | text | Device identifier |
| `datetime` | timestamp | Timestamp of the event |
| `date` | text `yyyyMMdd` | Log date |
| `source_file_name` | text | Source filename |
| `hex_id` | text | `0300` = alarm triggered, `0400` = alarm cleared |
| `alarm_state` | text | `TRIGGERED` or `CLEARED` |
| `alarm_top` | text | Alarm ID as text (e.g., `291`) — use this in the Alarm Code filter |
| `top_alarm_name` | text | Alarm name from reference data |
| `top_alarm_priority` | text | `ALARM_PRIORITY_LOW`, `MEDIUM`, or `HIGH` |

**Example — all alarm events for a device on a specific day:**

```sql
SELECT datetime, alarm_state, alarm_top, top_alarm_name, top_alarm_priority
FROM diality_logs.v_legacy_alarm_status_timeline
WHERE deviceid = 'DVTHD021'
  AND date = '20251029'
ORDER BY datetime;
```

**Example — only triggered events for a specific alarm ID:**

```sql
SELECT *
FROM diality_logs.v_legacy_alarm_status_timeline
WHERE alarm_top = '291'
  AND alarm_state = 'TRIGGERED'
ORDER BY datetime;
```

**Example — high-priority alarms across all devices:**

```sql
SELECT deviceid, date, datetime, alarm_top, top_alarm_name
FROM diality_logs.v_legacy_alarm_status_timeline
WHERE top_alarm_priority = 'ALARM_PRIORITY_HIGH'
  AND alarm_state = 'TRIGGERED'
ORDER BY datetime DESC
LIMIT 200;
```

**Example — alarm trigger-to-clear duration:**

```sql
SELECT
    t.deviceid,
    t.date,
    t.source_file_name,
    t.alarm_top,
    t.top_alarm_name,
    t.datetime AS triggered_at,
    MIN(c.datetime) AS cleared_at,
    DATEDIFF(second, t.datetime, MIN(c.datetime)) AS seconds_active
FROM diality_logs.v_legacy_alarm_status_timeline t
JOIN diality_logs.v_legacy_alarm_status_timeline c
  ON  c.deviceid          = t.deviceid
  AND c.source_file_name  = t.source_file_name
  AND c.alarm_top         = t.alarm_top
  AND c.alarm_state       = 'CLEARED'
  AND c.datetime          > t.datetime
WHERE t.alarm_state = 'TRIGGERED'
GROUP BY 1,2,3,4,5,6
ORDER BY t.datetime;
```

---

#### Alarm Events

**What it shows:** One row per raw 0300 (triggered) or 0400 (cleared) log
line from `processed_logs`. This is the unjoined version — useful when you
want the raw event name and payload alongside the alarm ID.

**Available filters:** Device ID, Log Date, File Name, Alarm Code, Message
Type, Time Start, Time End

**Columns:**

| Column | Type | Meaning |
|---|---|---|
| `deviceid` | text | Device identifier |
| `datetime` | timestamp | Event timestamp |
| `date` | text `yyyyMMdd` | Log date |
| `source_file_name` | text | Source filename |
| `hex_id` | text | `0300` or `0400` |
| `source` | text | `HD` or `DG` |
| `event_name` | text | Raw event label from the log line |
| `alarm_id_or_name` | text | First payload field (alarm ID as text) |
| `alarm_event_type` | text | `TRIGGERED` or `CLEARED` |

**Example — alarm events for a specific file:**

```sql
SELECT datetime, hex_id, source, event_name, alarm_id_or_name, alarm_event_type
FROM diality_logs.v_legacy_alarm_events
WHERE source_file_name = 'DVTHD021_20251029.c.log'
ORDER BY datetime;
```

**Example — count trigger vs clear events per alarm ID:**

```sql
SELECT alarm_id_or_name,
       SUM(CASE WHEN alarm_event_type = 'TRIGGERED' THEN 1 ELSE 0 END) AS triggers,
       SUM(CASE WHEN alarm_event_type = 'CLEARED'   THEN 1 ELSE 0 END) AS clears
FROM diality_logs.v_legacy_alarm_events
WHERE deviceid = 'DVTHD021'
GROUP BY alarm_id_or_name
ORDER BY triggers DESC;
```

---

### 3.3 Legacy Graph Reports

The graph reports expose time-series sensor data, one row per sensor-sample
timestamp. All graph views share the same common columns plus view-specific
measurement columns.

**Common columns in every graph view:**

| Column | Type | Meaning |
|---|---|---|
| `deviceid` | text | Device identifier |
| `datetime` | timestamp | Sample timestamp |
| `date` | text `yyyyMMdd` | Log date |
| `source_file_name` | text | Source filename |

**Available filters for all graph reports:** Device ID, Log Date, File Name,
Time Start, Time End

---

#### DG Pressure Graph

**Measurement columns:**

| Column | Unit / Meaning |
|---|---|
| `ppi` | RO pump inlet pressure |
| `ppo` | RO pump outlet pressure |
| `prd` | Drain pump inlet pressure |
| `pdr` | Drain pump outlet pressure |
| `barometric_pressure` | Barometric pressure |

**Example — pressure readings during a known time window:**

```sql
SELECT datetime, ppi, ppo, prd, pdr, barometric_pressure
FROM diality_logs.v_legacy_dg_pressure_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND "Datetime" BETWEEN '2025-10-29 08:00:00' AND '2025-10-29 10:00:00'
ORDER BY datetime;
```

---

#### DG Conductivity Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `ro_rejection_ratio` | RO rejection ratio |
| `cpi` | Inlet conductivity |
| `cpo` | Outlet conductivity |
| `cd1` | Dialysate conductivity 1 |
| `cd2` | Dialysate conductivity 2 |
| `cpi_raw` / `cpo_raw` / `cd1_raw` / `cd2_raw` | Un-calibrated raw readings |

**Example:**

```sql
SELECT datetime, cpi, cpo, cd1, cd2, ro_rejection_ratio
FROM diality_logs.v_legacy_dg_conductivity_graph
WHERE source_file_name = 'HD000116_20260331.u.log'
ORDER BY datetime
LIMIT 500;
```

---

#### DG Temperature Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `tpi` | Inlet primary heater temperature |
| `thd` | Heat-disinfect temperature |
| `tpo` | Outlet primary heater temperature |
| `td1` | Dialysate temperature sensor 1 |
| `td2` | Dialysate temperature sensor 2 |
| `tro` | Outlet redundant temperature |
| `tdi` | Inlet dialysate temperature |

**Example — temperature trend during a heat-disinfect cycle:**

```sql
SELECT datetime, tpi, thd, tpo, td1, td2
FROM diality_logs.v_legacy_dg_temperature_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND "Datetime" >= '2025-10-29 06:00:00'
  AND "Datetime" <= '2025-10-29 09:00:00'
ORDER BY datetime;
```

---

#### Load Cell Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `load_cell_a1` | Channel A sensor 1 (grams) |
| `load_cell_a2` | Channel A sensor 2 (grams) |
| `load_cell_b1` | Channel B sensor 1 (grams) |
| `load_cell_b2` | Channel B sensor 2 (grams) |

**Example:**

```sql
SELECT datetime, load_cell_a1, load_cell_a2, load_cell_b1, load_cell_b2
FROM diality_logs.v_legacy_load_cell_graph
WHERE source_file_name = 'HD000127_20260410.u.log'
ORDER BY datetime;
```

---

#### RO Pump Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `ro_target_pressure_psi` | Target RO pump pressure (PSI) |
| `ro_measured_flow_lpm` | Measured flow rate (L/min) |
| `ro_duty_cycle` | RO pump duty cycle |
| `ro_pump_state` | Pump state label |
| `ro_target_flow_lpm` | Target flow rate (L/min) |
| `ro_feedback_duty_cycle` | Feedback-controlled duty cycle |

**Example — detect anomalies where measured flow deviates from target:**

```sql
SELECT datetime,
       ro_target_flow_lpm,
       ro_measured_flow_lpm,
       ABS(ro_target_flow_lpm - ro_measured_flow_lpm) AS deviation_lpm
FROM diality_logs.v_legacy_ro_pump_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND ro_target_flow_lpm IS NOT NULL
  AND ABS(ro_target_flow_lpm - ro_measured_flow_lpm) > 0.5
ORDER BY deviation_lpm DESC
LIMIT 100;
```

---

#### Occlusion Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `arterial_pressure` | Arterial blood line pressure |
| `venous_pressure` | Venous blood line pressure |
| `bp_occlusion` | Blood pump occlusion |
| `pressure_limit_state` | Active pressure limit state |
| `art_min_limit` / `art_max_limit` | Arterial pressure limits |
| `ven_min_limit` / `ven_max_limit` | Venous pressure limits |
| `arterial_long_filtered_pressure` | Long-window filtered arterial pressure |
| `venous_long_filtered_pressure` | Long-window filtered venous pressure |
| `occlusion_long_filtered_pressure` | Long-window filtered occlusion pressure |
| `partial_occlusion_baseline` | Partial-occlusion baseline value |

**Example:**

```sql
SELECT datetime, arterial_pressure, venous_pressure, bp_occlusion
FROM diality_logs.v_legacy_occlusion_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
ORDER BY datetime
LIMIT 1000;
```

---

#### Accelerometer Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `accel_domain` | `HD` (hemodialysis side) or `DG` (dialysate generator side) |
| `x` / `y` / `z` | Instantaneous acceleration axes |
| `x_max` / `y_max` / `z_max` | Maximum acceleration axes |
| `x_tilt` / `y_tilt` / `z_tilt` | Tilt values per axis |

**Example — detect periods of elevated vibration on either unit:**

```sql
SELECT datetime, accel_domain, x_max, y_max, z_max
FROM diality_logs.v_legacy_accelerometer_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND (ABS(x_max) > 2 OR ABS(y_max) > 2 OR ABS(z_max) > 2)
ORDER BY datetime;
```

**Example — compare HD vs DG tilt trends:**

```sql
SELECT accel_domain,
       AVG(x_tilt) AS avg_x_tilt,
       AVG(y_tilt) AS avg_y_tilt,
       MAX(ABS(z_tilt)) AS max_z_tilt_abs
FROM diality_logs.v_legacy_accelerometer_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
GROUP BY accel_domain;
```

---

#### Blood Leak Graph

**Measurement columns:**

| Column | Meaning |
|---|---|
| `blood_leak_status` | Sensor status label |
| `blood_leak_state` | Current sensor state |
| `blood_leak_persistent_counter` | Persistent event counter |
| `blood_leak_serial_comm_state` | Serial communication state |
| `blood_leak_intensity` | Raw sensor intensity |
| `blood_leak_detect` | Detection value |
| `blood_leak_intensity_moving_avg` | Moving average of intensity |
| `blood_leak_time_since_zero_ms` | Milliseconds since last zero reading |

**Example — rows where intensity exceeded a threshold:**

```sql
SELECT datetime, blood_leak_status, blood_leak_intensity,
       blood_leak_intensity_moving_avg
FROM diality_logs.v_legacy_blood_leak_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND blood_leak_intensity > 100
ORDER BY datetime;
```

---

#### Switch Graph

**What it shows:** Switch state timeline for door and cap-style mechanical
inputs. HD and DG sides are distinguished by `switch_domain`.

**Measurement columns:**

| Column | Meaning |
|---|---|
| `switch_domain` | `HD` or `DG` |
| `switch_1` | Front door (HD) or concentrate cap (DG) state |
| `switch_2` | Pump-track switch (HD) or dialysate cap (DG) state |

**Example — detect all cap opens/closes on the DG side:**

```sql
SELECT datetime, switch_domain, switch_2
FROM diality_logs.v_legacy_switch_graph
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND switch_domain = 'DG'
  AND switch_2 IS NOT NULL
ORDER BY datetime;
```

---

### 3.4 Operations Reports

These reports work directly against the core `processed_logs` and
`alarm_details` tables. They are useful for fleet-level investigation,
ingestion monitoring, and cross-file comparison.

---

#### Device Investigation

**What it shows:** One row per device per file. Summarises total event rows,
first and last event time, and the number of distinct firmware message IDs
seen in the file.

**Available filters:** Device ID, Log Date, File Name, Time Start, Time End

**Columns:** `deviceid`, `source_file_name`, `date`, `first_event_time`,
`last_event_time`, `processed_row_count`, `distinct_message_count`

**Example — files with unexpectedly low message diversity:**

```sql
SELECT deviceid, source_file_name, date,
       processed_row_count, distinct_message_count
FROM (
    SELECT deviceid, source_file_name, date,
           COUNT(*) AS processed_row_count,
           COUNT(DISTINCT hex_id) AS distinct_message_count
    FROM diality_spectrum.processed_logs
    GROUP BY 1, 2, 3
) x
WHERE distinct_message_count < 10
ORDER BY distinct_message_count;
```

---

#### Cross-Device Comparison

**What it shows:** Per-device per-date row counts and message diversity.
Useful for spotting devices that are logging significantly more or fewer
events than peers on the same date.

**Available filters:** Log Date, File Name, Time Start, Time End

**Columns:** `deviceid`, `date`, `file_count`, `processed_row_count`,
`distinct_message_count`

**Example — compare all devices for a specific date:**

```sql
SELECT deviceid, date, file_count, processed_row_count, distinct_message_count
FROM (
    SELECT deviceid, date,
           COUNT(DISTINCT source_file_name) AS file_count,
           COUNT(*) AS processed_row_count,
           COUNT(DISTINCT hex_id) AS distinct_message_count
    FROM diality_spectrum.processed_logs
    WHERE date = '20260410'
    GROUP BY 1, 2
) x
ORDER BY processed_row_count DESC;
```

---

#### Log Timeline

**What it shows:** Chronological raw event feed from `processed_logs` for a
specific file or device/date combination. The most direct replacement for
scrolling the raw log file. Includes firmware message ID, source, event name,
and raw payload.

**Available filters:** Device ID, Log Date, File Name, Message Type, Time
Start, Time End

**Columns:** `deviceid`, `"Datetime"`, `date`, `source_file_name`, `source`,
`hex_id`, `event_name`, `payload`

**Example — all events for a file, chronological:**

```sql
SELECT "Datetime", source, hex_id, event_name, payload
FROM diality_spectrum.processed_logs
WHERE source_file_name = 'DVTHD021_20251029.c.log'
ORDER BY "Datetime"
LIMIT 1000;
```

**Example — look up a specific firmware message ID across all files:**

```sql
SELECT deviceid, date, "Datetime", source, event_name, payload
FROM diality_spectrum.processed_logs
WHERE hex_id = '0900'
  AND deviceid = 'DVTHD021'
ORDER BY "Datetime"
LIMIT 500;
```

**Example — search by event name keyword:**

```sql
SELECT "Datetime", hex_id, source, event_name, payload
FROM diality_spectrum.processed_logs
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND event_name ILIKE '%occlusion%'
ORDER BY "Datetime";
```

---

#### Alarm Frequency

**What it shows:** Alarm frequency summary — count of rows per alarm ID per
device per file. Use this to identify recurring alarms, compare alarm rates
across devices, or confirm that a specific alarm is absent.

**Available filters:** Device ID, Log Date, File Name, Alarm Code, Time Start,
Time End

**Columns:** `deviceid`, `date`, `source_file_name`, `alarm_id`, `alarm_name`,
`priority`, `alarm_row_count`

**Example — top 10 most frequent alarms across all files:**

```sql
SELECT alarm_id, alarm_name, priority,
       SUM(alarm_row_count) AS total_events
FROM (
    SELECT deviceid, date, source_file_name, alarm_id, alarm_name, priority,
           COUNT(*) AS alarm_row_count
    FROM diality_spectrum.alarm_details
    GROUP BY 1,2,3,4,5,6
) x
GROUP BY alarm_id, alarm_name, priority
ORDER BY total_events DESC
LIMIT 10;
```

**Example — check whether alarm 291 appeared in a specific file:**

```sql
SELECT alarm_id, alarm_name, priority, COUNT(*) AS events
FROM diality_spectrum.alarm_details
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND alarm_id = 291
GROUP BY alarm_id, alarm_name, priority;
```

---

### 3.5 Quality Reports

---

#### Parser Validation Summary

**What it shows:** Per-file row counts for `processed_logs`, `payload_details`,
and `alarm_details`. Use this to verify a file was fully processed or to
compare counts before and after a reprocess.

**Available filters:** Device ID, Log Date, File Name

**Columns:** `deviceid`, `date`, `source_file_name`, `processed_rows`,
`payload_rows`, `alarm_rows`

**Example — validate row counts for all files after a reprocess:**

```sql
SELECT deviceid, date, source_file_name,
       processed_rows, payload_rows, alarm_rows
FROM (
    SELECT p.deviceid, p.date, p.source_file_name,
           COUNT(*) AS processed_rows,
           COALESCE(y.payload_rows, 0) AS payload_rows,
           COALESCE(a.alarm_rows, 0) AS alarm_rows
    FROM (SELECT deviceid, date, source_file_name, COUNT(*) AS cnt
          FROM diality_spectrum.processed_logs GROUP BY 1,2,3) p
    LEFT JOIN (SELECT source_file_name, COUNT(*) AS payload_rows
               FROM diality_spectrum.payload_details GROUP BY 1) y
           ON y.source_file_name = p.source_file_name
    LEFT JOIN (SELECT source_file_name, COUNT(*) AS alarm_rows
               FROM diality_spectrum.alarm_details GROUP BY 1) a
           ON a.source_file_name = p.source_file_name
) x
ORDER BY date DESC, source_file_name;
```

---

#### Daily Ingestion Status

**What it shows:** Per-date summary of how many files were ingested and how
many total events they contain. Useful for monitoring the pipeline health day
by day.

**Available filters:** Device ID, Log Date, File Name

**Columns:** `date`, `file_count`, `device_count`, `processed_row_count`

---

## 4. SQL Runner — Direct Table Access

The SQL Runner accepts any `SELECT` or `WITH` query. Only read-only queries
are permitted. A `LIMIT` clause is required (maximum 5,000 rows).

### Available schemas and tables

| Schema | Object | Description |
|---|---|---|
| `diality_spectrum` | `processed_logs` | All parsed log events |
| `diality_spectrum` | `payload_details` | Named payload fields per event |
| `diality_spectrum` | `alarm_details` | Alarm trigger/clear events |
| `diality_logs` | `v_legacy_treatment_summary` | Treatment cycle summary |
| `diality_logs` | `v_legacy_heat_disinfect_summary` | Heat disinfect cycle summary |
| `diality_logs` | `v_legacy_water_flush_summary` | Water flush cycle summary |
| `diality_logs` | `v_legacy_alarm_status_timeline` | Alarm trigger/clear timeline |
| `diality_logs` | `v_legacy_alarm_events` | Raw alarm event log |
| `diality_logs` | `v_legacy_dg_pressure_graph` | DG pressure sensors |
| `diality_logs` | `v_legacy_dg_conductivity_graph` | DG conductivity sensors |
| `diality_logs` | `v_legacy_dg_temperature_graph` | DG temperature sensors |
| `diality_logs` | `v_legacy_load_cell_graph` | Load cell sensors |
| `diality_logs` | `v_legacy_ro_pump_graph` | RO pump measurements |
| `diality_logs` | `v_legacy_occlusion_graph` | Occlusion pressure sensors |
| `diality_logs` | `v_legacy_accelerometer_graph` | Accelerometer data (HD + DG) |
| `diality_logs` | `v_legacy_blood_leak_graph` | Blood leak sensor data |
| `diality_logs` | `v_legacy_switch_graph` | Switch state timeline |

### Key rules for writing SQL

1. **Always quote `"Datetime"`** — it is a Redshift reserved word.
   Use `"Datetime"` wherever it appears in `WHERE`, `ORDER BY`, or `SELECT`.

2. **Filter by `deviceid` and/or `date` first** — these are the partition
   keys. Queries that include them run much faster and cost less.

3. **Use `ILIKE` for text searches** — Redshift `ILIKE` is case-insensitive.
   Example: `event_name ILIKE '%alarm%'`.

4. **Date format is `yyyyMMdd`** — the `date` column stores values like
   `'20251029'`, not `'2025-10-29'`.

5. **`"Datetime"` format is ISO timestamp** — use
   `'2025-10-29 08:00:00'` in comparisons.

---

### Core Table Column Reference

#### `diality_spectrum.processed_logs`

| Column | Type | Notes |
|---|---|---|
| `deviceid` | text | Partition key — always filter on this when possible |
| `"Datetime"` | timestamp | Must be double-quoted |
| `date` | text `yyyyMMdd` | Partition key |
| `hex_id` | text | Normalised 4-char firmware message ID, e.g. `0900` |
| `source` | text | `HD` or `DG` |
| `id_num` | integer | Matched FW `id_dec`; null when no FW match |
| `event_name` | text | Human-readable label from the log line |
| `payload` | text | Comma-joined raw payload values |
| `source_file` | text | Full S3 path |
| `source_file_name` | text | Filename only |

#### `diality_spectrum.payload_details`

| Column | Type | Notes |
|---|---|---|
| `deviceid` | text | Partition key |
| `"Datetime"` | timestamp | Must be double-quoted |
| `date` | text `yyyyMMdd` | Partition key |
| `hex_id` | text | Parent message ID |
| `payload_index` | integer | 1-based position in payload CSV |
| `payload_key` | text | Named field from FW reference (e.g. `roPumpInletPressure`) |
| `payload_value` | text | Value at that position |
| `source_file_name` | text | Filename |

#### `diality_spectrum.alarm_details`

| Column | Type | Notes |
|---|---|---|
| `deviceid` | text | Partition key |
| `"Datetime"` | timestamp | Must be double-quoted |
| `date` | text `yyyyMMdd` | Partition key |
| `hex_id` | text | `0300` = triggered, `0400` = cleared |
| `alarm_id` | integer | Numeric alarm identifier |
| `alarm_name` | text | Alarm label |
| `priority` | text | `ALARM_PRIORITY_NONE` / `LOW` / `MEDIUM` / `HIGH` |
| `description` | text | Plain-language description |
| `trigger_conditions` | text | Conditions that activate the alarm |
| `source_file_name` | text | Filename |

---

### SQL Runner example queries

**Pivot a specific payload key across time (e.g. RO pump inlet pressure):**

```sql
SELECT "Datetime", payload_value::float AS ppi
FROM diality_spectrum.payload_details
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND payload_key = 'roPumpInletPressure'
ORDER BY "Datetime"
LIMIT 2000;
```

**Find all distinct firmware message IDs used by a device:**

```sql
SELECT DISTINCT hex_id, event_name
FROM diality_spectrum.processed_logs
WHERE deviceid = 'DVTHD021'
ORDER BY hex_id;
```

**Count alarm triggers per hour for a file:**

```sql
SELECT DATE_TRUNC('hour', "Datetime") AS hour_bucket,
       COUNT(*) AS trigger_count
FROM diality_spectrum.alarm_details
WHERE source_file_name = 'DVTHD021_20251029.c.log'
  AND hex_id = '0300'
GROUP BY 1
ORDER BY 1;
```

**Join treatment summary with alarm detail to see what alarm fired during each failed treatment:**

```sql
SELECT t.deviceid,
       t.date,
       t.treatment_num,
       t.pretx_start_time,
       t.end_time,
       t.final_completion_status,
       a.alarm_id,
       a.alarm_name,
       a.priority
FROM diality_logs.v_legacy_treatment_summary t
LEFT JOIN diality_spectrum.alarm_details a
  ON  a.deviceid         = t.deviceid
  AND a.source_file_name = t.source_file_name
  AND a.alarm_id         = t.interruption_alarm_id::int
WHERE t.final_completion_status = 'FAIL'
ORDER BY t.date DESC, t.treatment_num
LIMIT 100;
```

**Find the temperature at the moment a heat-disinfect cycle failed:**

```sql
SELECT h.heat_num,
       h.start_time,
       h.end_time,
       h.completion_status,
       t.tpi, t.thd, t.tpo
FROM diality_logs.v_legacy_heat_disinfect_summary h
JOIN diality_logs.v_legacy_dg_temperature_graph t
  ON  t.deviceid         = h.deviceid
  AND t.source_file_name = h.source_file_name
  AND t.datetime         = h.end_time
WHERE h.completion_status = 'FAIL'
  AND h.deviceid = 'DVTHD021'
ORDER BY h.start_time;
```

---

## 5. Ask Denali — AI-Assisted Queries

Ask Denali accepts plain-English questions and returns a data table. It uses
the same tables and views as the SQL Runner. Behind the scenes it generates a
SELECT query, which you can review before the results are shown.

### Tips for getting good results

- **Name the device** — "for device DVTHD021" or "on device HD000116"
- **Name the date or file** — "on 2025-10-29" or "in file DVTHD021_20251029.c.log"
- **Name the view** — "show me the treatment summary" is more reliable than
  "show me the treatments"
- **Use the exact column name** when filtering on status — say
  `completion_status = FAIL`, not just `status = FAIL`
- **Be specific about alarm IDs** — "alarm 291" rather than "the top alarm"

### Example questions

- "Show me all failed heat disinfect cycles for device DVTHD021"
- "What alarms were triggered in file DVTHD021_20251029.c.log?"
- "How many treatment cycles passed vs failed across all devices?"
- "Show me the RO pump pressure trend for file HD000116_20260331.u.log
  between 08:00 and 10:00"
- "How often does alarm 291 appear in each device?"
- "Give me the alarm trigger/clear timeline for device DVTHD021 on 2025-10-29"

---

## 6. Common Workflows

### Workflow A — Investigate a failed treatment

1. Analytics → **Treatment Summary** → set Device ID, set Log Date → Run
2. Identify the row with `final_completion_status = FAIL` and note the
   `interruption_alarm_id` and `end_time`
3. Analytics → **Alarm Status Timeline** → set Device ID, Log Date, set
   Alarm Code to the interruption alarm ID → Run
4. Find the TRIGGERED event just before `end_time`
5. SQL Runner → query `v_legacy_dg_temperature_graph` and
   `v_legacy_dg_pressure_graph` around that timestamp to see sensor state

### Workflow B — Heat disinfect failure root-cause

1. Analytics → **Heat Disinfect Summary** → filter to the device and date
2. Note the `start_time` and `end_time` of the failing cycle
3. SQL Runner:
   ```sql
   SELECT datetime, tpi, thd, tpo
   FROM diality_logs.v_legacy_dg_temperature_graph
   WHERE source_file_name = '<filename>'
     AND "Datetime" BETWEEN '<start_time>' AND '<end_time>'
   ORDER BY datetime;
   ```
4. Check whether temperature reached the required threshold

### Workflow C — Fleet-wide alarm scan

1. Analytics → **Alarm Frequency** → leave Device ID blank, set Log Date range
2. Sort by `alarm_row_count` DESC to see the highest-frequency alarms
3. For the top alarm ID, go to **Alarm Status Timeline** and filter by that
   alarm code to see which devices and dates it appeared on

### Workflow D — Treatment window with average fill mode

Identifies treatment start (HD opMode 5 → 6) and end (HD opMode 6 → 7) for
every device, then computes the average DG FILL_MODE reading across each window.
Treatment detection reads directly from `processed_logs` using strict transition
detection (prev opMode must be 5 to count as a start, prev must be 6 to count
as an end).

> **Note:** `"datetime"` must be double-quoted in Redshift — it is a reserved
> keyword. The query below is ready to paste into the SQL Runner as-is.

```sql
WITH opmode_hd AS (
    SELECT
        deviceid,
        "datetime",
        date,
        SPLIT_PART(payload, ',', 1)                           AS opmode,
        LAG(SPLIT_PART(payload, ',', 1)) OVER (
            PARTITION BY deviceid, date ORDER BY "datetime"
        )                                                      AS prev_opmode
    FROM diality_spectrum.processed_logs
    WHERE source     = 'HD'
      AND event_name = 'OpMode'
),
treatment_starts AS (
    SELECT deviceid, "datetime" AS treatment_start
    FROM opmode_hd
    WHERE opmode      = '6'
      AND prev_opmode = '5'
),
treatment_ends AS (
    SELECT deviceid, "datetime" AS treatment_end
    FROM opmode_hd
    WHERE opmode      = '7'
      AND prev_opmode = '6'
),
treatments AS (
    SELECT
        s.deviceid,
        s.treatment_start,
        MIN(e.treatment_end) AS treatment_end
    FROM treatment_starts s
    LEFT JOIN treatment_ends e
      ON  s.deviceid      = e.deviceid
      AND e.treatment_end > s.treatment_start
    GROUP BY s.deviceid, s.treatment_start
),
fill_mode_readings AS (
    SELECT
        p.deviceid,
        p."datetime",
        (((TRIM(SPLIT_PART(p.payload, ',', REGEXP_COUNT(p.payload, ',') + 1)))::float - 13735) / 13735) * 100 AS fill_mode_pct
    FROM diality_spectrum.processed_logs p
    WHERE p.hex_id     = 'A500'
      AND p.payload    LIKE '13%'
      AND p.event_name = 'General'
)
SELECT
    t.deviceid,
    t.treatment_start,
    t.treatment_end,
    DATEDIFF(minute, t.treatment_start, t.treatment_end) AS duration_minutes,
    ROUND(AVG(f.fill_mode_pct), 2)                       AS avg_fill_mode_pct,
    COUNT(f."datetime")                                   AS fill_mode_sample_count
FROM treatments t
LEFT JOIN fill_mode_readings f
  ON  f.deviceid    = t.deviceid
  AND f."datetime" >= t.treatment_start
  AND f."datetime" <= t.treatment_end
GROUP BY t.deviceid, t.treatment_start, t.treatment_end
ORDER BY t.deviceid, t.treatment_start
LIMIT 500;
```

**How it works:**

| CTE | Purpose |
|---|---|
| `opmode_hd` | Reads `processed_logs` filtered to `source='HD'` and `event_name='OpMode'`; `LAG` over `(deviceid, date)` exposes the previous opMode value |
| `treatment_starts` | Strict 5 → 6 transition only — pre-treatment handoff into active treatment |
| `treatment_ends` | Strict 6 → 7 transition only — treatment into post-treatment/rinse-back |
| `treatments` | Pairs each start to the **next** end (`MIN(end) WHERE end > start`) |
| `fill_mode_readings` | A500 `General` event last payload value normalised: `((value − 13735) / 13735) × 100` |
| Final SELECT | Averages fill-mode readings that fall inside each `[treatment_start, treatment_end]` window |

**To scope to a date range**, add to both `opmode_hd` and `fill_mode_readings`:

```sql
AND date BETWEEN '20251001' AND '20251231'
```

**To scope to a single device**, add:

```sql
AND deviceid = 'HD000127'
```

---

### Workflow E — Validate a reprocess

1. Before reprocess: note the row counts from **Parser Validation Summary**
2. After reprocess: run the same report and compare
3. SQL Runner cross-check:
   ```sql
   SELECT source_file_name,
          COUNT(*) AS processed_rows
   FROM diality_spectrum.processed_logs
   GROUP BY 1
   ORDER BY 1;
   ```

---

### Workflow F — Conductivity RR per treatment window

Computes the average Conductivity Reduction Ratio (RR) for each treatment window.
Treatment boundaries are detected the same way as Workflow D (HD opMode 5 → 6
for start, 6 → 7 for end). RR is derived from the `DG` `Conductivity` event
(hex `3100`) first payload value: `(1 − value) × 100`.

> **Note:** `"datetime"` must be double-quoted in Redshift — it is a reserved
> keyword. The query below is ready to paste into the SQL Runner as-is.

```sql
WITH opmode_hd AS (
    SELECT
        deviceid,
        "datetime",
        date,
        SPLIT_PART(payload, ',', 1)                           AS opmode,
        LAG(SPLIT_PART(payload, ',', 1)) OVER (
            PARTITION BY deviceid, date ORDER BY "datetime"
        )                                                      AS prev_opmode
    FROM diality_spectrum.processed_logs
    WHERE source     = 'HD'
      AND event_name = 'OpMode'
),
treatment_starts AS (
    SELECT deviceid, "datetime" AS treatment_start
    FROM opmode_hd
    WHERE opmode      = '6'
      AND prev_opmode = '5'
),
treatment_ends AS (
    SELECT deviceid, "datetime" AS treatment_end
    FROM opmode_hd
    WHERE opmode      = '7'
      AND prev_opmode = '6'
),
treatments AS (
    SELECT
        s.deviceid,
        s.treatment_start,
        MIN(e.treatment_end) AS treatment_end
    FROM treatment_starts s
    LEFT JOIN treatment_ends e
      ON  s.deviceid      = e.deviceid
      AND e.treatment_end > s.treatment_start
    GROUP BY s.deviceid, s.treatment_start
),
cond_rr_readings AS (
    SELECT
        p.deviceid,
        p."datetime",
        (1 - CAST(SPLIT_PART(payload, ',', 1) AS float)) * 100 AS RR
    FROM diality_spectrum.processed_logs p
    WHERE p.hex_id     = '3100'
      AND p.event_name = 'Conductivity'
      AND p.source     = 'DG'
)
SELECT
    t.deviceid,
    t.treatment_start,
    t.treatment_end,
    DATEDIFF(minute, t.treatment_start, t.treatment_end) AS duration_minutes,
    ROUND(AVG(f.RR), 2)                                  AS RR,
    COUNT(f."datetime")                                   AS RR_count
FROM treatments t
LEFT JOIN cond_rr_readings f
  ON  f.deviceid    = t.deviceid
  AND f."datetime" >= t.treatment_start
  AND f."datetime" <= t.treatment_end
GROUP BY t.deviceid, t.treatment_start, t.treatment_end
ORDER BY t.deviceid, t.treatment_start
LIMIT 500;
```

**How it works:**

| CTE | Purpose |
|---|---|
| `opmode_hd` | Reads `processed_logs` filtered to `source='HD'` and `event_name='OpMode'`; `LAG` over `(deviceid, date)` exposes the previous opMode value |
| `treatment_starts` | Strict 5 → 6 transition — pre-treatment handoff into active treatment |
| `treatment_ends` | Strict 6 → 7 transition — treatment into post-treatment/rinse-back |
| `treatments` | Pairs each start to the **next** end (`MIN(end) WHERE end > start`) |
| `cond_rr_readings` | DG `Conductivity` (hex `3100`) first payload value converted to RR %: `(1 − value) × 100` |
| Final SELECT | Averages RR readings that fall inside each `[treatment_start, treatment_end]` window |

**To scope to a date range**, add to both `opmode_hd` and `cond_rr_readings`:

```sql
AND date BETWEEN '20251001' AND '20251231'
```

**To scope to a single device**, add:

```sql
AND deviceid = 'HD000127'
```

---

### Workflow G — Inlet heater temperature flag per treatment window

Computes the average DG inlet primary heater temperature for each treatment
window, restricted to readings that fall in either of two target bands
(24–27°C or 34–37°C). Treatment boundaries are detected the same way as
Workflow D/F (HD opMode 5 → 6 for start, 6 → 7 for end). The temperature
reading is the DG `Temperatures` event (hex `2D00`) first payload value
(`inletPrimaryHeater`).

> **Note:** `"datetime"` must be double-quoted in Redshift — it is a reserved
> keyword. The query below is ready to paste into the SQL Runner as-is.

```sql
WITH opmode_hd AS (
    SELECT
        deviceid,
        "datetime",
        date,
        SPLIT_PART(payload, ',', 1)                           AS opmode,
        LAG(SPLIT_PART(payload, ',', 1)) OVER (
            PARTITION BY deviceid, date ORDER BY "datetime"
        )                                                      AS prev_opmode
    FROM diality_spectrum.processed_logs
    WHERE source     = 'HD'
      AND event_name = 'OpMode'
),
treatment_starts AS (
    SELECT deviceid, "datetime" AS treatment_start
    FROM opmode_hd
    WHERE opmode      = '6'
      AND prev_opmode = '5'
),
treatment_ends AS (
    SELECT deviceid, "datetime" AS treatment_end
    FROM opmode_hd
    WHERE opmode      = '7'
      AND prev_opmode = '6'
),
treatments AS (
    SELECT
        s.deviceid,
        s.treatment_start,
        MIN(e.treatment_end) AS treatment_end
    FROM treatment_starts s
    LEFT JOIN treatment_ends e
      ON  s.deviceid      = e.deviceid
      AND e.treatment_end > s.treatment_start
    GROUP BY s.deviceid, s.treatment_start
),
temp_readings AS (
    SELECT
        p.deviceid,
        p."datetime",
        (CAST(SPLIT_PART(payload, ',', 1) AS float)) AS inletPrimaryHeater
    FROM diality_spectrum.processed_logs p
    WHERE p.hex_id     = '2D00'
      AND p.event_name = 'Temperatures'
      AND p.source     = 'DG'
      AND (
        (CAST(SPLIT_PART(payload, ',', 1) AS float) BETWEEN 24 AND 27)
        OR
        (CAST(SPLIT_PART(payload, ',', 1) AS float) BETWEEN 34 AND 37)
      )
)
SELECT
    t.deviceid,
    t.treatment_start,
    t.treatment_end,
    DATEDIFF(minute, t.treatment_start, t.treatment_end) AS duration_minutes,
    ROUND(AVG(f.inletPrimaryHeater), 2)                       AS inletPrimaryHeater,
    COUNT(f."datetime")                                   AS TempCount
FROM treatments t
LEFT JOIN temp_readings f
  ON  f.deviceid    = t.deviceid
  AND f."datetime" >= t.treatment_start
  AND f."datetime" <= t.treatment_end
GROUP BY t.deviceid, t.treatment_start, t.treatment_end
ORDER BY t.deviceid, t.treatment_start
LIMIT 500;
```

**How it works:**

| CTE | Purpose |
|---|---|
| `opmode_hd` | Reads `processed_logs` filtered to `source='HD'` and `event_name='OpMode'`; `LAG` over `(deviceid, date)` exposes the previous opMode value |
| `treatment_starts` | Strict 5 → 6 transition — pre-treatment handoff into active treatment |
| `treatment_ends` | Strict 6 → 7 transition — treatment into post-treatment/rinse-back |
| `treatments` | Pairs each start to the **next** end (`MIN(end) WHERE end > start`) |
| `temp_readings` | DG `Temperatures` (hex `2D00`) first payload value (`inletPrimaryHeater`), restricted to the 24–27°C or 34–37°C bands |
| Final SELECT | Averages the flagged temperature readings that fall inside each `[treatment_start, treatment_end]` window, plus a count of matching samples |

**To scope to a date range**, add to both `opmode_hd` and `temp_readings`:

```sql
AND date BETWEEN '20251001' AND '20251231'
```

**To scope to a single device**, add:

```sql
AND deviceid = 'HD000127'
```

**Note on interpretation:** `TempCount` is a count of samples that fell
*inside* the two target bands during the window, not the total number of
`Temperatures` readings in the window — a low count can mean the heater
rarely entered those ranges, not that data is missing. Cross-check against
`Log Timeline` for the same window if the count looks unexpectedly low.

---

## 7. Filter Reference

| Filter name | Column it maps to | Format |
|---|---|---|
| Device ID | `deviceid` | Text, e.g. `DVTHD021` |
| Log Date | `date` | `yyyyMMdd`, e.g. `20251029` |
| File Name | `source_file_name` | Exact filename, e.g. `DVTHD021_20251029.c.log` |
| Alarm Code | `alarm_top` (timeline) or `alarm_id_or_name` (events) | Alarm ID as text, e.g. `291` |
| Message Type | `event_name` (ILIKE match) | Partial name, e.g. `Occlusion` |
| Time Start | `"Datetime" >=` | ISO timestamp, e.g. `2025-10-29 08:00:00` |
| Time End | `"Datetime" <=` | ISO timestamp, e.g. `2025-10-29 10:00:00` |

> **Cycle summary views** (Treatment, Heat Disinfect, Water Flush) do **not**
> support Time Start / Time End — those views use `start_time`/`end_time`
> columns, not `Datetime`. Use the SQL Runner if you need timestamp range
> filters on those views.

---

## 8. Quick Reference Card

| I want to… | Use this |
|---|---|
| See all treatment cycles for a device | Analytics → Treatment Summary |
| See heat disinfect pass/fail history | Analytics → Heat Disinfect Summary |
| See water flush history | Analytics → Water Flush Summary |
| See when each alarm fired and cleared | Analytics → Alarm Status Timeline |
| See raw alarm trigger/clear events | Analytics → Alarm Events |
| Trend DG pressure over time | Analytics → DG Pressure Graph |
| Trend DG conductivity or temperature | Analytics → DG Conductivity / Temperature Graph |
| See load cell, RO pump, or occlusion data | Analytics → Load Cell / RO Pump / Occlusion Graph |
| See accelerometer or blood leak data | Analytics → Accelerometer / Blood Leak Graph |
| See door/cap switch states | Analytics → Switch Graph |
| Investigate a specific device and file | Analytics → Device Investigation |
| Compare row counts across devices | Analytics → Cross-Device Comparison |
| Scroll through raw events for a file | Analytics → Log Timeline |
| Find the most common alarms | Analytics → Alarm Frequency |
| Verify file processed correctly | Analytics → Parser Validation Summary |
| See daily ingestion health | Analytics → Daily Ingestion Status |
| Write a custom JOIN or aggregation | SQL Runner |
| Ask a question in plain English | Ask Denali |
