160 lines
5.6 KiB
TypeScript
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>
|
|
);
|
|
}
|