diff --git a/scripts/sql/p1_historian.sql b/scripts/sql/p1_historian.sql
index 4168050..da79543 100644
--- a/scripts/sql/p1_historian.sql
+++ b/scripts/sql/p1_historian.sql
@@ -43,3 +43,10 @@ CREATE TABLE IF NOT EXISTS hc900.live_kpi (
value double precision, unit text, state text, excluded_min int, status text,
updated_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (column_id, kpi, window_start));
+
+-- P1d: 실시간 알람 (edge-trigger: 진입 active, 해제 resolved)
+CREATE TABLE IF NOT EXISTS hc900.kpi_alert (
+ column_id text NOT NULL, rule text NOT NULL, severity text, active boolean NOT NULL DEFAULT true,
+ message text, value double precision,
+ opened_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), resolved_at timestamptz,
+ PRIMARY KEY (column_id, rule));
diff --git a/src/Hc900Crawler/Controllers/ReportController.cs b/src/Hc900Crawler/Controllers/ReportController.cs
index d35ab3f..d75316d 100644
--- a/src/Hc900Crawler/Controllers/ReportController.cs
+++ b/src/Hc900Crawler/Controllers/ReportController.cs
@@ -53,6 +53,32 @@ public class ReportController : ControllerBase
return Ok(new { Count = items.Count, Items = items });
}
+ /// 활성 알람(kpi_alert) — cleaning/drawdown 진입·폐합 이탈. active=false면 해제 포함.
+ [HttpGet("alerts")]
+ public async Task Alerts(bool activeOnly = true, CancellationToken ct = default)
+ {
+ var conn = _db.Database.GetDbConnection();
+ if (conn.State != ConnectionState.Open) await conn.OpenAsync(ct);
+ await using var cmd = conn.CreateCommand();
+ cmd.CommandText = @"SELECT column_id, rule, severity, active, message, value, opened_at, updated_at, resolved_at
+ FROM hc900.kpi_alert" + (activeOnly ? " WHERE active" : "") +
+ " ORDER BY active DESC, opened_at DESC";
+ var items = new List