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

@ -1,7 +1,12 @@
import axios, { type AxiosError } from 'axios'
import type { InternalAxiosRequestConfig } from 'axios'
import { useAuthStore } from '../store/authStore'
import type { FleetStatus, CreateJobRequest } from '../types'
import type {
FleetStatus,
CreateJobRequest,
CreateMaintenanceWindowRequest,
UpdateMaintenanceWindowRequest,
} from '../types'
const BASE_URL = '/api/v1'
@ -123,3 +128,22 @@ export const patchesApi = {
// The backend reads from host_patch_data table (cached from agent poll)
getHostPatches: (hostId: string) => apiClient.get(`/hosts/${hostId}/patches`),
}
// ── Maintenance Windows API ───────────────────────────────────────────────────
export const maintenanceWindowsApi = {
list: (hostId: string) =>
apiClient.get(`/hosts/${hostId}/maintenance-windows`),
create: (hostId: string, body: CreateMaintenanceWindowRequest) =>
apiClient.post(`/hosts/${hostId}/maintenance-windows`, body),
update: (hostId: string, windowId: string, body: UpdateMaintenanceWindowRequest) =>
apiClient.put(`/hosts/${hostId}/maintenance-windows/${windowId}`, body),
remove: (hostId: string, windowId: string) =>
apiClient.delete(`/hosts/${hostId}/maintenance-windows/${windowId}`),
}
// ── WebSocket API (M7) ────────────────────────────────────────────────────────
export const wsApi = {
/** POST /api/v1/ws/ticket — obtain a single-use WS auth ticket (60 s expiry). */
createTicket: (): Promise<{ ticket: string }> =>
apiClient.post<{ ticket: string }>('/ws/ticket').then((r) => r.data),
}