diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index d2c6fd1..ebc5048 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -45,9 +45,9 @@ ### Wireless UI -- [ ] **WRUI-01**: Device detail page shows a per-station wireless table (connected clients with MAC, signal, CCQ, TX/RX rates, distance, uptime) -- [ ] **WRUI-02**: Device detail page shows per-interface RF stats (noise floor, channel width, TX power) -- [ ] **WRUI-03**: Wireless links page shows all discovered AP-CPE relationships with signal quality and link state +- [x] **WRUI-01**: Device detail page shows a per-station wireless table (connected clients with MAC, signal, CCQ, TX/RX rates, distance, uptime) +- [x] **WRUI-02**: Device detail page shows per-interface RF stats (noise floor, channel width, TX power) +- [x] **WRUI-03**: Wireless links page shows all discovered AP-CPE relationships with signal quality and link state ### Signal Trending @@ -117,9 +117,9 @@ | LINK-02 | Phase 13 | Complete | | LINK-03 | Phase 13 | Complete | | LINK-04 | Phase 13 | Complete | -| WRUI-01 | Phase 14 | Pending | -| WRUI-02 | Phase 14 | Pending | -| WRUI-03 | Phase 14 | Pending | +| WRUI-01 | Phase 14 | Complete | +| WRUI-02 | Phase 14 | Complete | +| WRUI-03 | Phase 14 | Complete | | TRND-01 | Phase 15 | Pending | | TRND-02 | Phase 15 | Pending | | ALRT-01 | Phase 15 | Pending | diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index a4ed0fa..ab41d28 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -101,7 +101,7 @@ Plans: 3. Site dashboard displays wireless link topology showing which CPEs connect to which APs with signal quality indicators 4. Device detail page shows a per-station wireless table (connected clients with MAC, signal, CCQ, TX/RX rates, distance, uptime) and per-interface RF stats 5. Operator can define sectors within a site, assign APs to sectors, and view aggregate stats per sector -**Plans:** 1/3 plans executed +**Plans:** 2/3 plans executed Plans: - [ ] 14-01-PLAN.md — Sector backend (migration, model, service, router), site_id device filter, wireless data APIs, frontend API clients @@ -128,7 +128,7 @@ Plans: | Category | Requirements | Phase | Count | |----------|-------------|-------|-------| | 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 | 1/3 | In Progress| | SECT-01, SECT-02, SECT-03 | 14 | 3 | +| Site Dashboard | DASH-02, DASH-03, DASH-04 | 14 | 2/3 | In Progress| | 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 | | Site Alerting | ALRT-01, ALRT-02 | 15 | 2 | diff --git a/.planning/STATE.md b/.planning/STATE.md index 1679b75..4fb6f09 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 14-01-PLAN.md -last_updated: "2026-03-19T11:43:25.898Z" +stopped_at: Completed 14-02-PLAN.md +last_updated: "2026-03-19T11:48:58.364Z" progress: total_phases: 5 completed_phases: 3 total_plans: 11 - completed_plans: 9 + completed_plans: 10 --- # Project State @@ -24,7 +24,7 @@ See: .planning/PROJECT.md (updated 2026-03-18) ## Current Position Phase: 14 (site-dashboard-sector-views-wireless-ui) — EXECUTING -Plan: 2 of 3 +Plan: 3 of 3 ## Performance Metrics @@ -44,6 +44,7 @@ Plan: 2 of 3 | Phase 13 P01 | 5min | 2 tasks | 4 files | | Phase 13 P03 | 3min | 2 tasks | 6 files | | Phase 14 P01 | 3min | 2 tasks | 15 files | +| Phase 14 P02 | 3min | 2 tasks | 9 files | ## Accumulated Context @@ -78,6 +79,9 @@ Decisions are logged in PROJECT.md Key Decisions table. - [Phase 14]: Sector CRUD nested under sites path (/sites/{sid}/sectors) matching REST hierarchy - [Phase 14]: Device sector assignment uses PUT /devices/{did}/sector with nullable sector_id for set/clear - [Phase 14]: Wireless registration queries join device_interfaces for MAC-to-hostname resolution +- [Phase 14]: Shared signalColor helper in separate module for reuse across wireless components +- [Phase 14]: Wireless links grouped by AP hostname with nested CPE rows for topology clarity +- [Phase 14]: Sidebar Wireless Links href is tenant-scoped for non-super_admin users ### Pending Todos @@ -91,6 +95,6 @@ None yet. ## Session Continuity -Last session: 2026-03-19T11:43:25.894Z -Stopped at: Completed 14-01-PLAN.md +Last session: 2026-03-19T11:48:58.361Z +Stopped at: Completed 14-02-PLAN.md Resume file: None diff --git a/frontend/src/components/sites/SectorFormDialog.tsx b/frontend/src/components/sites/SectorFormDialog.tsx new file mode 100644 index 0000000..40fc4f4 --- /dev/null +++ b/frontend/src/components/sites/SectorFormDialog.tsx @@ -0,0 +1,136 @@ +import { useState, useEffect } from 'react' +import { useMutation, useQueryClient } from '@tanstack/react-query' +import { sectorsApi, type SectorResponse, type SectorCreate, type SectorUpdate } from '@/lib/api' +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { Input } from '@/components/ui/input' +import { Label } from '@/components/ui/label' +import { Button } from '@/components/ui/button' + +interface SectorFormDialogProps { + open: boolean + onOpenChange: (open: boolean) => void + tenantId: string + siteId: string + sector?: SectorResponse | null +} + +export function SectorFormDialog({ open, onOpenChange, tenantId, siteId, sector }: SectorFormDialogProps) { + const queryClient = useQueryClient() + const isEdit = !!sector + + const [name, setName] = useState('') + const [azimuth, setAzimuth] = useState('') + const [description, setDescription] = useState('') + + useEffect(() => { + if (sector) { + setName(sector.name) + setAzimuth(sector.azimuth != null ? String(sector.azimuth) : '') + setDescription(sector.description ?? '') + } else { + setName('') + setAzimuth('') + setDescription('') + } + }, [sector, open]) + + const createMutation = useMutation({ + mutationFn: (data: SectorCreate) => sectorsApi.create(tenantId, siteId, data), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['sectors', tenantId, siteId] }) + onOpenChange(false) + }, + }) + + const updateMutation = useMutation({ + mutationFn: (data: SectorUpdate) => sectorsApi.update(tenantId, siteId, sector!.id, data), + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ['sectors', tenantId, siteId] }) + onOpenChange(false) + }, + }) + + const isPending = createMutation.isPending || updateMutation.isPending + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + const data = { + name: name.trim(), + azimuth: azimuth ? parseFloat(azimuth) : null, + description: description.trim() || null, + } + + if (isEdit) { + updateMutation.mutate(data) + } else { + createMutation.mutate(data) + } + } + + return ( + + + + {isEdit ? 'Edit Sector' : 'Add Sector'} + + {isEdit ? 'Update sector details.' : 'Create a new sector to organize APs by direction.'} + + + +
+
+ + setName(e.target.value)} + placeholder="North Sector" + required + /> +
+ +
+ + setAzimuth(e.target.value)} + placeholder="0-360" + /> +
+ +
+ +