Watch
1
0
Fork
You've already forked RedFlag
0

feat: post-upgrade attestation — new binary proves the swap took

Old binary drops a marker (command_id, from/to) once the swap is
committed, on both the helper path and the legacy path. New binary
checks it at startup: running >= target just clears the marker
(check-in confirm still owns success); short of target means the swap
failed or rolled back, so it files a failed update_agent report under
the original command_id and the server clears is_updating right away
instead of sitting out the stuck-update timeout. Marker survives
failed reports for retry, drops on 409 or after 24h.
This commit is contained in:
Fimeg 2026-06-10 15:13:47 -04:00
commit 2f3363cbce
11 changed files with 304 additions and 102 deletions

View file

@ -5,7 +5,7 @@ import { cn } from '@/lib/utils';
* MetricItem a single system-info metric cell.
*
* Renders a label, value, optional icon, optional sub-text, and optional
* progress bar. Designed to be an independent grid item inside SystemInfoGrid.
* progress bar. Designed to be an independent grid or flex item.
*
* Usage:
* <MetricItem label="CPU" value="Intel i7" icon={Cpu} sub="4 cores" />

View file

@ -8,7 +8,7 @@ import { cn } from '@/lib/utils';
* Renders a compact table (Name, PID, CPU%, Mem%) when process data is
* available, or a fallback message with the process count.
*
* Designed as an independent grid item inside SystemInfoGrid.
* Designed as an independent grid or flex item.
*/
interface ProcessInfo {
name: string;

View file

@ -6,8 +6,9 @@ import { cn } from '@/lib/utils';
* ScreenshotCard screenshot thumbnail with sunshine overlay and capture state.
*
* Renders the agent's screenshot (or capture prompt), live/stream badges,
* and timestamp. Designed as a grid item inside SystemInfoGrid with
* col-start-1 row-span-2 to anchor top-left.
* and timestamp. The image box keeps a 16:9 ratio at its natural size but
* grows past it to absorb extra height pass `className="grow"` from a
* flex-col parent to make the card fill leftover vertical space.
*
* Handlers are passed in as props this component doesn't own mutations.
*/
@ -49,10 +50,10 @@ const ScreenshotCard: React.FC<ScreenshotCardProps> = ({
const canClick = !isCapturing && !isPolling;
return (
<div className={cn('min-w-0', className)}>
<div className={cn('flex min-w-0 flex-col', className)}>
<div
className={cn(
'relative aspect-video w-full overflow-hidden rounded border',
'relative aspect-video w-full grow overflow-hidden rounded border',
'bg-gradient-to-br from-slate-800 to-slate-900 border-slate-700',
'flex flex-col items-center justify-center gap-1 text-slate-400',
canClick && 'cursor-pointer hover:from-slate-700 hover:to-slate-800 transition-colors'

View file

@ -1,36 +0,0 @@
import React from 'react';
import { cn } from '@/lib/utils';
/**
* SystemInfoGrid CSS Grid container for system information primitives.
*
* Items auto-place into a 2-column grid that collapses to 1 on narrow
* viewports. The screenshot (or any anchor item) gets col-start-1
* row-span-2 via its own className prop.
*
* Usage:
* <SystemInfoGrid>
* <ScreenshotCard className="col-start-1 row-span-2" ... />
* <MetricItem label="CPU" ... />
* <MetricItem label="Memory" ... />
* <ProcessTable ... />
* </SystemInfoGrid>
*/
interface SystemInfoGridProps {
children: React.ReactNode;
className?: string;
}
const SystemInfoGrid: React.FC<SystemInfoGridProps> = ({ children, className }) => (
<div
className={cn('grid gap-6', className)}
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gridAutoRows: 'min-content',
}}
>
{children}
</div>
);
export default SystemInfoGrid;

View file

@ -6,3 +6,6 @@ export { default as PageState, PageSkeleton } from './PageState';
export { default as Modal } from './Modal';
export { default as Pagination } from './Pagination';
export { default as StatCard, StatCardGroup } from './StatCard';
export { default as ScreenshotCard } from './ScreenshotCard';
export { default as MetricItem } from './MetricItem';
export { default as ProcessTable } from './ProcessTable';

View file

@ -23,11 +23,14 @@ import {
MonitorPlay,
Upload,
} from 'lucide-react';
import { SearchInput, FilterDropdown, PageState } from '@/components/primitives';
import SystemInfoGrid from '@/components/primitives/SystemInfoGrid';
import ScreenshotCard from '@/components/primitives/ScreenshotCard';
import MetricItem from '@/components/primitives/MetricItem';
import ProcessTable from '@/components/primitives/ProcessTable';
import {
SearchInput,
FilterDropdown,
PageState,
ScreenshotCard,
MetricItem,
ProcessTable,
} from '@/components/primitives';
import { useDebounce } from '@/hooks/useDebounce';
import { useAgents, useAgent, useScanMultipleAgents, useUnregisterAgent } from '@/hooks/useAgents';
import { useActiveCommands, useCancelCommand, useCaptureScreenshot, useCommand } from '@/hooks/useCommands';
@ -825,7 +828,7 @@ const Agents: React.FC = () => {
)}
</div>
{/* System info — independent primitives in a CSS Grid */}
{/* System info — screenshot pane + metrics grid */}
<div className="card">
<h2 className="text-lg font-medium text-gray-900 mb-4">System Information</h2>
@ -837,51 +840,51 @@ const Agents: React.FC = () => {
const topProcesses = selectedAgent.metadata?.top_processes;
return (
<SystemInfoGrid>
<ScreenshotCard
className="col-start-1 row-span-2"
image={screenshotImage}
isCapturing={captureScreenshotMutation.isPending}
isPolling={screenshotCommand && !['completed', 'failed', 'timed_out', 'cancelled'].includes(screenshotCommand.status)}
sunshine={sunshine ? { ...sunshine, state: resolveState(sunshine) } : undefined}
screenshotStatus={screenshotCommand}
onCapture={() => handleCaptureScreenshot(selectedAgent.id)}
formatRelativeTime={formatRelativeTime}
/>
<MetricItem label="Platform" value={osInfo.platform} />
<MetricItem
label="Distribution"
value={osInfo.distribution}
sub={osInfo.version ? `(${osInfo.version})` : undefined}
/>
<MetricItem
label="Architecture"
value={selectedAgent.os_architecture || selectedAgent.architecture}
/>
<MetricItem label="CPU" value={meta.cpuModel} icon={Cpu} sub={`${meta.cpuCores} cores`} />
{meta.memoryTotal > 0 && (
<MetricItem label="Memory" value={formatBytes(meta.memoryTotal)} icon={MemoryStick} />
)}
{meta.diskTotal > 0 && (
<MetricItem
label={`Disk (${meta.diskMount})`}
value={`${formatBytes(meta.diskUsed)} / ${formatBytes(meta.diskTotal)}`}
icon={HardDrive}
progress={{ used: meta.diskUsed, total: meta.diskTotal }}
<div className="flex flex-col gap-6 md:flex-row">
{/* Left: screenshot grows to keep the panes level, platform identity below */}
<div className="flex flex-col gap-3 md:w-[45%] md:shrink-0">
<ScreenshotCard
className="grow"
image={screenshotImage}
isCapturing={captureScreenshotMutation.isPending}
isPolling={screenshotCommand && !['completed', 'failed', 'timed_out', 'cancelled'].includes(screenshotCommand.status)}
sunshine={sunshine ? { ...sunshine, state: resolveState(sunshine) } : undefined}
screenshotStatus={screenshotCommand}
onCapture={() => handleCaptureScreenshot(selectedAgent.id)}
formatRelativeTime={formatRelativeTime}
/>
)}
{meta.processes !== 'Unknown' && (
<MetricItem label="Running Processes" value={meta.processes} icon={GitBranch} />
)}
{meta.uptime !== 'Unknown' && (
<MetricItem label="Uptime" value={meta.uptime} icon={Clock} />
)}
<ProcessTable
processes={topProcesses}
processCount={meta.processes}
onSeeMore={() => navigate(`/updates?agent=${selectedAgent.id}`)}
/>
</SystemInfoGrid>
<MetricItem label="Platform" value={osInfo.platform} />
<MetricItem label="Distribution" value={osInfo.distribution} sub={osInfo.version ? `(${osInfo.version})` : undefined} />
<MetricItem label="Architecture" value={selectedAgent.os_architecture || selectedAgent.architecture} />
</div>
{/* Right: hardware metrics, ProcessTable spanning full width below */}
<div className="grid flex-1 grid-cols-1 content-start gap-3 min-w-0 sm:grid-cols-2">
<MetricItem label="CPU" value={meta.cpuModel} icon={Cpu} sub={`${meta.cpuCores} cores`} />
{meta.memoryTotal > 0 && (
<MetricItem label="Memory" value={formatBytes(meta.memoryTotal)} icon={MemoryStick} />
)}
{meta.diskTotal > 0 && (
<MetricItem
label={`Disk (${meta.diskMount})`}
value={`${formatBytes(meta.diskUsed)} / ${formatBytes(meta.diskTotal)}`}
icon={HardDrive}
progress={{ used: meta.diskUsed, total: meta.diskTotal }}
/>
)}
{meta.processes !== 'Unknown' && (
<MetricItem label="Running Processes" value={meta.processes} icon={GitBranch} />
)}
{meta.uptime !== 'Unknown' && (
<MetricItem label="Uptime" value={meta.uptime} icon={Clock} />
)}
<ProcessTable
className="col-span-full"
processes={topProcesses}
processCount={meta.processes}
onSeeMore={() => navigate(`/updates?agent=${selectedAgent.id}`)}
/>
</div>
</div>
);
})()}
</div>