Private
Public Access
1
0

feat: M6 maintenance windows + M7 WebSocket relay (real-time job status)

M6 - Maintenance Windows:
- routes/maintenance_windows.rs: full CRUD API
- migrations/004_maintenance_windows.sql
- frontend/MaintenanceWindowsPage.tsx
- HostDetailPage.tsx: maintenance window config panel

M7 - WebSocket Relay:
- pm-web: POST /api/v1/ws/ticket (JWT-auth, single-use, 60s TTL)
- pm-web: WS /api/v1/ws/jobs?ticket=... (PgListener -> browser push)
- pm-web: DashMap<String,WsTicket> in AppState, 30s cleanup task
- pm-worker: ws_relay.rs subscribes to agent WS, updates patch_job_hosts,
  fires pg_notify(job_update) for real-time fan-out
- frontend: useJobWebSocket hook with auto-reconnect + exponential backoff
- frontend: JobsPage live updates with WS status indicator
- types: JobWsEvent interface
- api/client: wsApi.createTicket()

All tasks marked complete in tasks/todo.md
cargo build: zero errors, zero warnings
This commit is contained in:
2026-04-23 17:42:51 +00:00
parent 6f9c6dc881
commit a5d52ffab0
21 changed files with 2833 additions and 36 deletions

View File

@ -11,6 +11,7 @@ import UsersPage from './pages/UsersPage'
import DashboardPage from './pages/DashboardPage'
import PatchDeploymentPage from './pages/PatchDeploymentPage'
import JobsPage from './pages/JobsPage'
import MaintenanceWindowsPage from './pages/MaintenanceWindowsPage'
// Placeholder pages — implemented in later milestones
const PlaceholderPage = ({ title }: { title: string }) => (
@ -44,10 +45,14 @@ function App() {
<Route path="/groups" element={<RequireAuth><GroupsPage /></RequireAuth>} />
<Route path="/users" element={<RequireAuth><UsersPage /></RequireAuth>} />
{/* Protected — later milestones */}
{/* Protected — M5 */}
<Route path="/jobs" element={<RequireAuth><JobsPage /></RequireAuth>} />
<Route path="/deployment" element={<RequireAuth><PatchDeploymentPage /></RequireAuth>} />
<Route path="/maintenance" element={<RequireAuth><PlaceholderPage title="Maintenance Windows" /></RequireAuth>} />
{/* Protected — M6 */}
<Route path="/maintenance" element={<RequireAuth><MaintenanceWindowsPage /></RequireAuth>} />
{/* Placeholder — later milestones */}
<Route path="/reports" element={<RequireAuth><PlaceholderPage title="Reports" /></RequireAuth>} />
<Route path="/certificates" element={<RequireAuth><PlaceholderPage title="Certificates" /></RequireAuth>} />
<Route path="/settings" element={<RequireAuth><PlaceholderPage title="Settings" /></RequireAuth>} />