feat: add CRL health status schema and UI (PR 3 of 6)
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
All checks were successful
CI Pipeline / Rust Format Check (push) Successful in 5s
CI Pipeline / Clippy Lints (push) Successful in 52s
CI Pipeline / Rust Unit Tests (push) Successful in 1m8s
CI Pipeline / Security Audit (push) Successful in 5s
CI Pipeline / Frontend Lint & Type Check (push) Successful in 15s
CI Pipeline / Build .deb & Release (push) Has been skipped
* feat: add CRL health status schema and UI (PR 3 of 6) * fix(lint): strict equality for crl_age_seconds --------- Co-authored-by: Draco Lunaris <331325+Draco-Lunaris@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5aec9e629c
commit
ea8337b944
@ -22,6 +22,7 @@ import {
|
||||
RestartAlt,
|
||||
Refresh as RefreshIcon,
|
||||
Security as SecurityIcon,
|
||||
VerifiedUser as VerifiedUserIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { fleetApi, certsApi } from '../api/client'
|
||||
import type { FleetStatus } from '../types'
|
||||
@ -237,6 +238,57 @@ export default function DashboardPage() {
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
{/* ── Row 4: CRL Status ── */}
|
||||
<Card variant="outlined" sx={{ mt: 3 }}>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<VerifiedUserIcon color="primary" />
|
||||
<Typography variant="subtitle1" fontWeight={600}>
|
||||
CRL Status
|
||||
</Typography>
|
||||
</Box>
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 6, sm: 3 }}>
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: '#2e7d32' }}>
|
||||
{status.crl_valid}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Valid</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 3 }}>
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: '#ed6c02' }}>
|
||||
{status.crl_expired}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Expired</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 3 }}>
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: '#ed6c02' }}>
|
||||
{status.crl_missing}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Missing</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 6, sm: 3 }}>
|
||||
<Box textAlign="center">
|
||||
<Typography variant="h5" fontWeight={700} sx={{ color: '#d32f2f' }}>
|
||||
{status.crl_invalid}
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary">Invalid</Typography>
|
||||
</Box>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{status.crl_not_reporting > 0 && (
|
||||
<Typography variant="caption" color="text.secondary" sx={{ display: 'block', mt: 1 }}>
|
||||
{status.crl_not_reporting} host{status.crl_not_reporting !== 1 ? 's' : ''} not reporting CRL status
|
||||
</Typography>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
)}
|
||||
</Container>
|
||||
|
||||
@ -46,6 +46,9 @@ import {
|
||||
Schedule as ScheduleIcon,
|
||||
VpnKey as VpnKeyIcon,
|
||||
ContentCopy as CopyIcon,
|
||||
VerifiedUser as VerifiedUserIcon,
|
||||
Security as SecurityIcon,
|
||||
WarningAmber as WarningAmberIcon,
|
||||
} from '@mui/icons-material'
|
||||
import { apiClient, hostsApi, maintenanceWindowsApi, healthChecksApi, certsApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
@ -1035,6 +1038,53 @@ export default function HostDetailPage() {
|
||||
</Grid>
|
||||
</Paper>
|
||||
|
||||
{/* ── CRL Status ─────────────────────────────────────────────────── */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 2 }}>
|
||||
<VerifiedUserIcon color="primary" />
|
||||
<Typography variant="h6" fontWeight={600}>CRL Status</Typography>
|
||||
</Box>
|
||||
<Divider sx={{ mb: 2 }} />
|
||||
{host?.crl_status === undefined || host?.crl_status === null ? (
|
||||
<Alert severity="info">
|
||||
CRL status not available (agent version does not support CRL)
|
||||
</Alert>
|
||||
) : (
|
||||
<Grid container spacing={2}>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">Status</Typography>
|
||||
{host.crl_status === 'valid' ? (
|
||||
<Chip icon={<VerifiedUserIcon />} label="Valid" color="success" size="small" />
|
||||
) : host.crl_status === 'expired' ? (
|
||||
<Chip icon={<WarningAmberIcon />} label="Expired" color="warning" size="small" />
|
||||
) : host.crl_status === 'missing' ? (
|
||||
<Chip icon={<WarningAmberIcon />} label="Missing" color="warning" size="small" />
|
||||
) : host.crl_status === 'invalid' ? (
|
||||
<Chip icon={<SecurityIcon />} label="Invalid" color="error" size="small" />
|
||||
) : (
|
||||
<Typography variant="body2">{String(host.crl_status)}</Typography>
|
||||
)}
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">CRL Age</Typography>
|
||||
<Typography variant="body2">
|
||||
{host.crl_age_seconds !== null
|
||||
? (() => { const s = Number(host.crl_age_seconds); return s < 3600 ? `${Math.round(s / 60)} minutes ago` : s < 86400 ? `${Math.round(s / 3600)} hours ago` : `${Math.round(s / 86400)} days ago`; })()
|
||||
: '—'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid size={{ xs: 12, sm: 4 }}>
|
||||
<Typography variant="caption" color="text.secondary" display="block">Next Update</Typography>
|
||||
<Typography variant="body2">
|
||||
{host.crl_next_update
|
||||
? new Date(host.crl_next_update as string).toLocaleString()
|
||||
: '—'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
)}
|
||||
</Paper>
|
||||
|
||||
{/* ── Maintenance Windows ──────────────────────────────────────────── */}
|
||||
<Paper sx={{ p: 3, mb: 3 }}>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', mb: 2 }}>
|
||||
|
||||
@ -5,7 +5,7 @@ import {
|
||||
Table, TableBody, TableCell, TableContainer, TableHead, TableRow,
|
||||
TablePagination, TextField, Toolbar, Tooltip, Typography,
|
||||
} from '@mui/material'
|
||||
import { Add as AddIcon, Refresh as RefreshIcon, Delete as DeleteIcon, CheckCircle as CheckCircleIcon, Cancel as CancelIcon, Remove as RemoveIcon, Pending as PendingIcon, GppMaybe as GppMaybeIcon, CheckCircleOutline as CheckCircleOutlineIcon, WarningAmber as WarningAmberIcon } from '@mui/icons-material'
|
||||
import { Add as AddIcon, Refresh as RefreshIcon, Delete as DeleteIcon, CheckCircle as CheckCircleIcon, Cancel as CancelIcon, Remove as RemoveIcon, Pending as PendingIcon, GppMaybe as GppMaybeIcon, CheckCircleOutline as CheckCircleOutlineIcon, WarningAmber as WarningAmberIcon, VerifiedUser as VerifiedUserIcon, Security as SecurityIcon } from '@mui/icons-material'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { apiClient, hostsApi, enrollmentApi } from '../api/client'
|
||||
import { useAuthStore } from '../store/authStore'
|
||||
@ -182,6 +182,7 @@ export default function HostsPage() {
|
||||
<TableCell>OS</TableCell>
|
||||
<TableCell>Health</TableCell>
|
||||
<TableCell>Checks</TableCell>
|
||||
<TableCell>CRL</TableCell>
|
||||
<TableCell>Agent</TableCell>
|
||||
{canWrite && <TableCell>Actions</TableCell>}
|
||||
</TableRow>
|
||||
@ -201,6 +202,7 @@ export default function HostsPage() {
|
||||
<TableCell>{(req.os_details['name'] as string) ?? 'Unknown'}</TableCell>
|
||||
<TableCell><Chip size="small" label="pending" color="warning" /></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell></TableCell>
|
||||
<TableCell>—</TableCell>
|
||||
{canWrite && <TableCell onClick={e => e.stopPropagation()}>
|
||||
<Tooltip title="Approve">
|
||||
@ -240,6 +242,19 @@ export default function HostsPage() {
|
||||
<Tooltip title="No checks configured"><RemoveIcon color="disabled" fontSize="small" /></Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{h.crl_status === 'valid' ? (
|
||||
<Tooltip title="CRL valid"><VerifiedUserIcon color="success" fontSize="small" /></Tooltip>
|
||||
) : h.crl_status === 'expired' ? (
|
||||
<Tooltip title="CRL expired"><WarningAmberIcon color="warning" fontSize="small" /></Tooltip>
|
||||
) : h.crl_status === 'missing' ? (
|
||||
<Tooltip title="CRL missing"><WarningAmberIcon color="warning" fontSize="small" /></Tooltip>
|
||||
) : h.crl_status === 'invalid' ? (
|
||||
<Tooltip title="CRL invalid — security event"><SecurityIcon color="error" fontSize="small" /></Tooltip>
|
||||
) : (
|
||||
<Tooltip title="CRL status not available (agent version does not support CRL)"><RemoveIcon color="disabled" fontSize="small" /></Tooltip>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{h.agent_version ?? '—'}</TableCell>
|
||||
{canWrite && <TableCell onClick={e => e.stopPropagation()}>
|
||||
<Tooltip title="Request refresh">
|
||||
|
||||
@ -27,6 +27,9 @@ export interface Host {
|
||||
patches_missing: number
|
||||
registered_at: string
|
||||
health_check_status?: 'all_healthy' | 'some_unhealthy' | 'none'
|
||||
crl_status?: 'valid' | 'expired' | 'missing' | 'invalid'
|
||||
crl_age_seconds?: number
|
||||
crl_next_update?: string
|
||||
}
|
||||
|
||||
export interface CreateHostRequest {
|
||||
@ -98,6 +101,11 @@ export interface FleetStatus {
|
||||
total_pending_patches: number
|
||||
hosts_requiring_reboot: number
|
||||
compliance_pct: number
|
||||
crl_valid: number
|
||||
crl_expired: number
|
||||
crl_missing: number
|
||||
crl_invalid: number
|
||||
crl_not_reporting: number
|
||||
}
|
||||
|
||||
export interface PatchInfo {
|
||||
|
||||
Reference in New Issue
Block a user