Files
AetherForge/server/web/src/components/Fleet/AgentListItem.tsx

160 lines
5.6 KiB
TypeScript

import AgentRemoteActions from './AgentRemoteActions';
import { formatHashrate, formatUptime } from '../../help/fleetFilters';
import { lotlTierLabel } from '../../help/warRoomTelemetry';
import type { Agent } from '../../types';
import type { SeqCommandResult } from '../../context/WebSocketContext';
import type { FleetGroup } from '../../help/fleetGroups';
import LatencyBadge from './LatencyBadge';
function formatRelTime(iso: string): string {
const diff = Date.now() - new Date(iso).getTime();
const mins = Math.floor(diff / 60000);
if (mins < 2) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hrs = Math.floor(mins / 60);
if (hrs < 24) return `${hrs}h ago`;
return `${Math.floor(hrs / 24)}d ago`;
}
interface Props {
agent: Agent;
selected: boolean;
expanded: boolean;
selectable?: boolean;
checked?: boolean;
onToggleExpand: () => void;
onSelect: () => void;
onCheck?: (checked: boolean) => void;
commandResults?: SeqCommandResult[];
memberGroups?: FleetGroup[];
}
export default function AgentListItem({
agent,
selected,
expanded,
selectable,
checked,
onToggleExpand,
onSelect,
onCheck,
commandResults,
memberGroups = [],
}: Props) {
const online = agent.status === 'online';
const primaryGroup = memberGroups[0];
const handleRowClick = (e: React.MouseEvent) => {
const target = e.target as HTMLElement;
if (target.closest('input[type="checkbox"]') || target.closest('button') || target.closest('.agent-remote')) {
return;
}
onSelect();
onToggleExpand();
};
return (
<div
className={`neon-card agent-list-item compact-row ${selected ? 'selected' : ''} ${expanded ? 'expanded' : ''}`}
style={
primaryGroup
? ({
borderLeftWidth: '3px',
borderLeftStyle: 'solid',
borderLeftColor: primaryGroup.color,
boxShadow: selected
? `inset 0 0 20px ${primaryGroup.color}22, 0 0 16px ${primaryGroup.color}33`
: undefined,
} as React.CSSProperties)
: undefined
}
onClick={handleRowClick}
>
<div className="agent-list-header">
<div className="agent-list-name">
{selectable && (
<input
type="checkbox"
className="checkbox agent-list-select"
checked={!!checked}
onChange={(e) => {
e.stopPropagation();
onCheck?.(e.target.checked);
}}
onClick={(e) => e.stopPropagation()}
/>
)}
<span className={`status-dot ${agent.status}`} />
<span>{agent.name}</span>
{agent.platform && (
<span className="agent-tag-chip platform-badge" title={agent.os_version || agent.platform}>
{agent.platform}{agent.arch ? `/${agent.arch}` : ''}
</span>
)}
{agent.campaign && (
<span className="agent-tag-chip" title="Spread campaign (?c=)">
c:{agent.campaign}
</span>
)}
{lotlTierLabel(agent.lotl_tier) && (
<span className="agent-tag-chip war-room-lotl-badge" title="LOTL tier">
{lotlTierLabel(agent.lotl_tier)}
</span>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.4rem' }}>
<span className={`status-badge ${agent.status}`}>{agent.status}</span>
<LatencyBadge ms={agent.status === 'online' ? agent.latency_ms : undefined} />
</div>
</div>
{(memberGroups.length > 0 || (agent.tags?.length ?? 0) > 0) && (
<div className="agent-list-tags">
{memberGroups.map((g) => (
<span
key={g.id}
className="agent-tag-chip fleet-group-tag"
style={{ background: `${g.color}22`, color: g.color, borderColor: `${g.color}55` }}
>
{g.name}
</span>
))}
{agent.tags?.map((t) => (
<span key={t} className="agent-tag-chip">{t}</span>
))}
</div>
)}
<div className="agent-list-details">
<span>{formatHashrate(agent.hashrate_15m)}</span>
<span>{agent.ip || '—'}</span>
{agent.status !== 'online' && agent.last_seen && (
<span className="form-hint" title={new Date(agent.last_seen).toLocaleString()}>
last seen {formatRelTime(agent.last_seen)}
</span>
)}
{!expanded && agent.status === 'online' && <span className="form-hint">click for details</span>}
</div>
{!expanded && agent.notes?.trim() && (
<p className="agent-list-notes-preview">{agent.notes.trim().slice(0, 80)}{agent.notes.length > 80 ? '…' : ''}</p>
)}
{expanded && (
<div className="agent-list-expand" onClick={(e) => e.stopPropagation()}>
<div className="agent-list-meta">
<span>Shares: {agent.shares_good}/{agent.shares_total}</span>
<span>{agent.cpu_cores} cores · {agent.memory_gb} GB</span>
<span>Uptime: {formatUptime(agent.uptime_seconds)}</span>
<span>v{agent.version || '?'}</span>
{agent.build_id && <span className="mono" title="Forge build">build:{agent.build_id.slice(0, 8)}</span>}
{agent.campaign && <span className="agent-tag-chip" title="Spread campaign">c:{agent.campaign}</span>}
</div>
{agent.notes?.trim() && <p className="form-hint">{agent.notes}</p>}
<AgentRemoteActions agent={agent} compact online={online} commandResults={commandResults} />
</div>
)}
</div>
);
}