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:
parent
1fa76bf665
commit
2f3363cbce
11 changed files with 304 additions and 102 deletions
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue