Private
Public Access
1
0

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

* 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:
Draco-Lunaris-Echo
2026-06-05 16:17:17 -05:00
committed by GitHub
parent 5aec9e629c
commit ea8337b944
12 changed files with 345 additions and 78 deletions

View File

@ -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>

View File

@ -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 }}>

View File

@ -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">

View File

@ -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 {