Private
Public Access
1
0

M5: Patch Deployment & Job Management

Backend:
- migrations/003_jobs_scheduling.sql: retry_next_at/last_error columns,
  pg_notify trigger for immediate job dispatch, retry index
- pm-agent-client: ApplyPatchesRequest/Response, AgentJobStatus,
  RollbackResponse types; apply_patches/job_status/rollback_job
  client methods + generic POST helper
- pm-core/models: JobStatus, JobKind, PatchJob, PatchJobHost,
  CreateJobRequest, PatchJobSummary
- pm-web/routes/jobs.rs: POST/GET /api/v1/jobs, GET /jobs/:id,
  POST /jobs/:id/cancel, POST /jobs/:id/rollback
- pm-worker/job_executor.rs: NOTIFY listener, periodic scanner,
  execute_host_job, poll_running_jobs, handle_host_failure (3-retry
  exponential backoff 1m/5m/30m), sync_job_status, retry_pending_jobs
- pm-worker/main.rs: spawn job_executor

Frontend:
- types/index.ts: PatchInfo, PatchJobHost, PatchJob, PatchJobSummary,
  CreateJobRequest interfaces
- api/client.ts: jobsApi (list/get/create/cancel/rollback),
  patchesApi (getHostPatches)
- pages/PatchDeploymentPage.tsx: 3-step MUI Stepper
  (host select → configure → result)
- pages/JobsPage.tsx: job list table, expandable per-host detail,
  cancel/rollback actions with confirm dialog, load-more pagination
- App.tsx: /jobs and /deployment routes wired to real pages

cargo check: 0 errors | vite build: 0 errors
This commit is contained in:
2026-04-23 17:08:43 +00:00
parent a6eb762962
commit 6f9c6dc881
30 changed files with 8465 additions and 44 deletions

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'
import { useEffect, useState, useCallback } from 'react'
import {
Box, Button, Chip, CircularProgress, Container, IconButton,
Paper, Table, TableBody, TableCell, TableContainer, TableHead,
@ -7,6 +7,7 @@ import {
import { Add as AddIcon, Refresh as RefreshIcon, Delete as DeleteIcon } from '@mui/icons-material'
import { useNavigate } from 'react-router-dom'
import { apiClient } from '../api/client'
import { hostsApi } from '../api/client'
import type { Host, HostHealthStatus } from '../types'
const statusColor = (s: HostHealthStatus) =>
@ -18,8 +19,9 @@ export default function HostsPage() {
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState('')
const [refreshing, setRefreshing] = useState<string | null>(null)
const load = async () => {
const load = useCallback(async () => {
setLoading(true)
try {
const res = await apiClient.get('/hosts', { params: { limit: 100 } })
@ -27,6 +29,17 @@ export default function HostsPage() {
setTotal(res.data.total)
} catch { /* handled by interceptor */ }
finally { setLoading(false) }
}, [])
const handleRefresh = async (e: React.MouseEvent, hostId: string) => {
e.stopPropagation()
setRefreshing(hostId)
try {
await hostsApi.refresh(hostId)
setTimeout(() => { load(); setRefreshing(null) }, 2000)
} catch {
setRefreshing(null)
}
}
useEffect(() => { load() }, [])
@ -72,6 +85,15 @@ export default function HostsPage() {
</TableCell>
<TableCell>{h.agent_version ?? '—'}</TableCell>
<TableCell onClick={e => e.stopPropagation()}>
<Tooltip title="Request refresh">
<IconButton size="small" color="primary"
disabled={refreshing === h.id}
onClick={(e) => handleRefresh(e, h.id)}>
{refreshing === h.id
? <CircularProgress size={16} />
: <RefreshIcon fontSize="small" />}
</IconButton>
</Tooltip>
<Tooltip title="Delete"><IconButton size="small" color="error">
<DeleteIcon fontSize="small" />
</IconButton></Tooltip>