diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f0d3a87..6f684ee 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -38,7 +38,7 @@ v9.7 transforms TOD from a flat device list into a site-aware fleet management p - [x] **Phase 12: Per-Client Wireless Collection** - Poller extension to collect registration table and per-interface RF stats (completed 2026-03-19) - [x] **Phase 13: Link Discovery + Registration Ingestion** - Backend NATS consumer, MAC resolution, AP-CPE link state machine (completed 2026-03-19) - [x] **Phase 14: Site Dashboard + Sector Views + Wireless UI** - Site detail page, sector-centric view, per-station wireless tables (completed 2026-03-19) -- [ ] **Phase 15: Signal Trending + Site Alerting** - Signal history charts, degradation detection, site/sector alert rules +- [x] **Phase 15: Signal Trending + Site Alerting** - Signal history charts, degradation detection, site/sector alert rules (completed 2026-03-19) ## Phase Details @@ -117,7 +117,7 @@ Plans: 2. System detects and surfaces signal degradation trends (e.g., "signal dropped 8dB over 2 weeks") 3. Operator can create site-scoped alert rules (e.g., "alert when >20% of devices at this site go offline") 4. Operator can create sector-scoped alert rules (e.g., "alert when sector average signal drops below -75dBm") -**Plans:** 2/3 plans executed +**Plans:** 3/3 plans complete Plans: - [ ] 15-01-PLAN.md — Backend data model, services, and REST API for site alert rules, alert events, and signal history @@ -131,7 +131,7 @@ Plans: | Sites | SITE-01, SITE-02, SITE-03, SITE-04, SITE-05, SITE-06 | 11 | 3/3 | Complete | 2026-03-19 | DASH-01 | 11 | 1 | | Site Dashboard | DASH-02, DASH-03, DASH-04 | 14 | 3/3 | Complete | 2026-03-19 | SECT-01, SECT-02, SECT-03 | 14 | 3 | | Wireless Collection | WRCL-01, WRCL-02, WRCL-03, WRCL-04, WRCL-05, WRCL-06 | 12 | 2/2 | Complete | 2026-03-19 | LINK-01, LINK-02, LINK-03, LINK-04 | 13 | 3/3 | Complete | 2026-03-19 | WRUI-01, WRUI-02, WRUI-03 | 14 | 3 | -| Signal Trending | TRND-01, TRND-02 | 15 | 2/3 | In Progress| | ALRT-01, ALRT-02 | 15 | 2 | +| Signal Trending | TRND-01, TRND-02 | 15 | 3/3 | Complete | 2026-03-19 | ALRT-01, ALRT-02 | 15 | 2 | | **Total** | | | **30** | ## Progress diff --git a/.planning/STATE.md b/.planning/STATE.md index d5fcc5b..9089683 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,13 +3,13 @@ gsd_state_version: 1.0 milestone: v9.7 milestone_name: Tower & Site Management status: unknown -stopped_at: Completed 15-01-PLAN.md -last_updated: "2026-03-19T12:19:31.850Z" +stopped_at: Completed 15-03-PLAN.md +last_updated: "2026-03-19T12:26:34.674Z" progress: total_phases: 5 - completed_phases: 4 + completed_phases: 5 total_plans: 14 - completed_plans: 13 + completed_plans: 14 --- # Project State @@ -48,6 +48,7 @@ Plan: 2 of 3 | Phase 14 P03 | 3min | 2 tasks | 6 files | | Phase 15 P02 | 3min | 2 tasks | 4 files | | Phase 15 P01 | 4min | 2 tasks | 10 files | +| Phase 15 P03 | 5min | 3 tasks | 9 files | ## Accumulated Context @@ -91,6 +92,9 @@ Decisions are logged in PROJECT.md Key Decisions table. - [Phase 15]: Site alert tables are separate from device-level alert_rules/alert_events (no coupling between systems) - [Phase 15]: Signal history uses TimescaleDB time_bucket with 3 range presets (5min/1h/4h buckets) - [Phase 15]: Alert event count endpoint returns simple JSON for notification bell badge +- [Phase 15]: NotificationBell placed in ContextStrip for consistent header integration +- [Phase 15]: Expandable chart rows use React.Fragment pattern with per-component state +- [Phase 15]: Alert rule type selector is context-aware (sector types when sectorId provided, site types otherwise) ### Pending Todos @@ -104,6 +108,6 @@ None yet. ## Session Continuity -Last session: 2026-03-19T12:19:31.847Z -Stopped at: Completed 15-01-PLAN.md +Last session: 2026-03-19T12:26:34.671Z +Stopped at: Completed 15-03-PLAN.md Resume file: None diff --git a/backend/app/services/alert_evaluator_site.py b/backend/app/services/alert_evaluator_site.py index 85a01fb..e98e78b 100644 --- a/backend/app/services/alert_evaluator_site.py +++ b/backend/app/services/alert_evaluator_site.py @@ -72,7 +72,7 @@ async def _evaluate_condition(session, rule) -> bool: # noqa: ANN001 FROM wireless_registrations wr JOIN devices d ON d.id = wr.device_id WHERE d.sector_id = :sector_id - AND wr.collected_at > now() - interval '10 minutes' + AND wr.time > now() - interval '10 minutes' """), {"sector_id": sector_id}, ) @@ -93,7 +93,7 @@ async def _evaluate_condition(session, rule) -> bool: # noqa: ANN001 FROM wireless_registrations wr JOIN devices d ON d.id = wr.device_id WHERE d.sector_id = :sector_id - AND wr.collected_at > now() - interval '10 minutes' + AND wr.time > now() - interval '10 minutes' """), {"sector_id": sector_id}, ) @@ -106,7 +106,7 @@ async def _evaluate_condition(session, rule) -> bool: # noqa: ANN001 FROM wireless_registrations wr JOIN devices d ON d.id = wr.device_id WHERE d.sector_id = :sector_id - AND wr.collected_at BETWEEN now() - interval '70 minutes' + AND wr.time BETWEEN now() - interval '70 minutes' AND now() - interval '60 minutes' """), {"sector_id": sector_id}, @@ -178,10 +178,10 @@ async def _evaluate_rules() -> None: await session.execute( text(""" INSERT INTO site_alert_events - (tenant_id, site_id, sector_id, rule_id, rule_type, + (tenant_id, site_id, sector_id, rule_id, severity, message, state, consecutive_hits, triggered_at) VALUES - (:tenant_id, :site_id, :sector_id, :rule_id, :rule_type, + (:tenant_id, :site_id, :sector_id, :rule_id, :severity, :message, 'active', 1, now()) """), { @@ -189,7 +189,6 @@ async def _evaluate_rules() -> None: "site_id": str(rule.site_id), "sector_id": str(rule.sector_id) if rule.sector_id else None, "rule_id": rule_id, - "rule_type": rule.rule_type, "severity": severity, "message": f"Alert rule '{rule.name}' condition met", }, diff --git a/backend/app/services/trend_detector.py b/backend/app/services/trend_detector.py index 9758569..a2e8092 100644 --- a/backend/app/services/trend_detector.py +++ b/backend/app/services/trend_detector.py @@ -53,7 +53,7 @@ async def _detect_trends() -> None: FROM wireless_registrations WHERE mac_address = :mac AND device_id = :ap_device_id - AND collected_at > now() - interval '7 days' + AND time > now() - interval '7 days' """), {"mac": mac, "ap_device_id": str(ap_device_id)}, ) @@ -67,7 +67,7 @@ async def _detect_trends() -> None: FROM wireless_registrations WHERE mac_address = :mac AND device_id = :ap_device_id - AND collected_at > now() - interval '14 days' + AND time > now() - interval '14 days' """), {"mac": mac, "ap_device_id": str(ap_device_id)}, ) @@ -88,7 +88,7 @@ async def _detect_trends() -> None: text(""" SELECT id FROM site_alert_events WHERE link_id = :link_id - AND rule_type = 'signal_degradation' + AND rule_id IS NULL AND state = 'active' LIMIT 1 """), @@ -105,10 +105,10 @@ async def _detect_trends() -> None: await session.execute( text(""" INSERT INTO site_alert_events - (tenant_id, site_id, link_id, rule_type, severity, message, state, + (tenant_id, site_id, link_id, severity, message, state, consecutive_hits, triggered_at) VALUES - (:tenant_id, :site_id, :link_id, 'signal_degradation', 'warning', + (:tenant_id, :site_id, :link_id, 'warning', :message, 'active', 1, now()) """), {