Files
AetherForge/server/web/src/components/Fleet/AccessDepthPanel.tsx
AetherForge c3a9cda7d5
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Fix Vitest suite and wire cloud/AWS dashboard API helpers.
Adds missing client methods, VPC seeder badges, hospice strain UI, and uiHelp drift keys so server/web builds and all 849 Vitest tests pass.
2026-06-07 11:03:35 -07:00

443 lines
17 KiB
TypeScript

import { useEffect, useMemo, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import { api } from '../../api/client';
import {
atlasSkipDisplayLabel,
buildAccessDepthModel,
parseAccessDepthServerPolicy,
type AccessDepthDiagnostics,
} from '../../help/accessDepth';
import {
clearanceLabel,
clearancePermissions,
formatClearanceElevation,
} from '../../help/clearance';
import { useWebSocket } from '../../hooks/useWebSocket';
import type { Agent, StrainCard, StrainHospiceRecord } from '../../types';
import { HelpTip } from '../HelpTip';
import JoinLaneBadge from './JoinLaneBadge';
import LotlTierBadge from './LotlTierBadge';
import './AccessDepthPanel.css';
import './LotlVisuals.css';
import './ReconVisuals.css';
interface Props {
agent: Agent;
diagnostics?: AccessDepthDiagnostics;
}
function OnionList({ rows, empty }: { rows: ReturnType<typeof buildAccessDepthModel>['miningOnion']; empty: string }) {
if (rows.length === 0) {
return <div className="access-depth-empty">{empty}</div>;
}
return (
<ol className="access-depth-onion-list">
{rows.map((row) => (
<li key={`${row.index}-${row.tier}`} className={`access-depth-onion-item access-depth-onion-item--${row.status}`}>
<span className="access-depth-onion-idx">{row.index}.</span>
<span className="access-depth-onion-label">{row.label}</span>
{row.status === 'active' && <span className="access-depth-tag access-depth-tag--active">active</span>}
{row.status === 'skipped' && <span className="access-depth-tag access-depth-tag--skip">skipped</span>}
{row.status === 'skipped_by_atlas' && (
<span className="access-depth-tag access-depth-tag--skip">atlas skip</span>
)}
{row.status === 'done' && <span className="access-depth-tag access-depth-tag--ok">ok</span>}
{row.status === 'failed' && <span className="access-depth-tag access-depth-tag--fail">fail</span>}
{row.status === 'pending' && <span className="access-depth-tag access-depth-tag--pending">pending</span>}
</li>
))}
</ol>
);
}
function AttemptMiniList({
rows,
empty,
}: {
rows: ReturnType<typeof buildAccessDepthModel>['succeeded'];
empty: string;
}) {
if (rows.length === 0) return <div className="access-depth-empty">{empty}</div>;
return (
<ul className="access-depth-attempt-list">
{rows.map((row, i) => (
<li key={`${row.tier}-${i}`} className="access-depth-attempt-row">
<span className={`lotl-attempt-icon ${row.ok ? 'ok' : 'fail'}`}>{row.ok ? '✓' : '✗'}</span>
<span className="access-depth-attempt-tier">{row.label}</span>
{row.phase && <span className="access-depth-phase">{row.phase}</span>}
{!row.ok && row.error && <span className="lotl-attempt-err">{row.error}</span>}
</li>
))}
</ul>
);
}
export default function AccessDepthPanel({ agent, diagnostics }: Props) {
const { latestMessage } = useWebSocket();
const [policyLoaded, setPolicyLoaded] = useState(false);
const [serverPolicy, setServerPolicy] = useState(parseAccessDepthServerPolicy({}));
const [elevationFlash, setElevationFlash] = useState<string | null>(null);
const [strainCards, setStrainCards] = useState<StrainCard[]>([]);
const [hospiceStrains, setHospiceStrains] = useState<Set<string>>(new Set());
const [strainPlayBusy, setStrainPlayBusy] = useState<string | null>(null);
const flashTimerRef = useRef<number | null>(null);
const clearanceLevel = agent.clearance_level ?? 1;
useEffect(() => {
if (!latestMessage || latestMessage.type !== 'clearance_elevated') return;
const p = latestMessage.payload as {
agent_id?: string;
to_level?: number;
source?: string;
reason?: string;
};
if (p.agent_id !== agent.id || typeof p.to_level !== 'number') return;
const text = formatClearanceElevation({
to_level: p.to_level,
source: p.source,
reason: p.reason,
});
setElevationFlash(text);
if (flashTimerRef.current != null) window.clearTimeout(flashTimerRef.current);
flashTimerRef.current = window.setTimeout(() => setElevationFlash(null), 6000);
}, [latestMessage, agent.id]);
useEffect(() => {
return () => {
if (flashTimerRef.current != null) window.clearTimeout(flashTimerRef.current);
};
}, []);
useEffect(() => {
let cancelled = false;
api
.getConfig()
.then((cfg) => {
if (!cancelled) {
setServerPolicy(parseAccessDepthServerPolicy(cfg));
setPolicyLoaded(true);
}
})
.catch(() => {
if (!cancelled) setPolicyLoaded(true);
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
let cancelled = false;
api
.listStrainCards(agent.id)
.then((cards) => {
if (!cancelled) setStrainCards(cards ?? []);
})
.catch(() => {
if (!cancelled) setStrainCards([]);
});
return () => {
cancelled = true;
};
}, [agent.id]);
useEffect(() => {
let cancelled = false;
api
.listStrainHospice()
.then((rows: StrainHospiceRecord[]) => {
if (!cancelled) {
setHospiceStrains(new Set(rows.map((r: StrainHospiceRecord) => r.strain_id.trim().toLowerCase())));
}
})
.catch(() => {
if (!cancelled) setHospiceStrains(new Set());
});
return () => {
cancelled = true;
};
}, []);
useEffect(() => {
if (!latestMessage) return;
if (latestMessage.type === 'strain_card' || latestMessage.type === 'strain_card_played') {
const p = latestMessage.payload as { card?: StrainCard; agent_id?: string };
if (p.card && (p.card.source_agent_id === agent.id || p.card.root_agent_id === agent.id || p.agent_id === agent.id)) {
setStrainCards((prev) => {
const next = prev.filter((c) => c.id !== p.card!.id);
return [p.card!, ...next];
});
}
}
}, [latestMessage, agent.id]);
const strainInHospice = (strain?: string) => {
const id = strain?.trim().toLowerCase().replace(/^#/, '') ?? '';
return id !== '' && hospiceStrains.has(id);
};
const playStrainCard = async (card: StrainCard) => {
if (strainPlayBusy || strainInHospice(card.spread_strain)) return;
setStrainPlayBusy(card.id);
try {
await api.playStrainCard({ agent_id: agent.id, card_id: card.id });
} finally {
setStrainPlayBusy(null);
}
};
const model = useMemo(
() => buildAccessDepthModel(agent, diagnostics, serverPolicy),
[agent, diagnostics, serverPolicy],
);
return (
<section className="access-depth-panel lotl-attempts-block" aria-label="Access depth">
<div className="access-depth-header">
<span className="lotl-attempts-title">
ACCESS DEPTH <HelpTip field="crucible_access_depth" />
</span>
<span
className="access-depth-clearance-badge"
title={clearancePermissions(clearanceLevel)}
aria-label={`Clearance ${clearanceLabel(clearanceLevel)}: ${clearancePermissions(clearanceLevel)}`}
>
{clearanceLabel(clearanceLevel)}
</span>
{elevationFlash && (
<span className="access-depth-clearance-flash" role="status">
{elevationFlash}
</span>
)}
{!policyLoaded && <span className="access-depth-muted">loading policy</span>}
<Link to="/oath" className="access-depth-oath-link" title="Immutable operator accountability ledger">
Oath
</Link>
</div>
<div className="access-depth-grid">
<div className="access-depth-section">
<div className="access-depth-section-title">OS &amp; posture</div>
<div className="access-depth-os-line">{model.osLine}</div>
{model.probes.length > 0 && (
<div className="access-depth-probes">
{model.probes.map((p) => (
<span key={p.key} className={`access-depth-probe ${p.ok ? 'ok' : 'no'}`}>
{p.label}
</span>
))}
</div>
)}
{model.spreadCaps.length > 0 && (
<div className="access-depth-meta">
spread: {model.spreadCaps.join(', ')}
</div>
)}
{model.privilegeHints.length > 0 && (
<div className="access-depth-meta">
{model.privilegeHints.join(' · ')}
</div>
)}
</div>
<div className="access-depth-section">
<div className="access-depth-section-title">Active</div>
{model.activeTier ? (
<LotlTierBadge tier={model.activeTier} attempts={agent.lotl_attempts} variant="inline" />
) : (
<div className="access-depth-empty">No active mining tier</div>
)}
{model.joinLane ? (
<div className="access-depth-join">
join lane <JoinLaneBadge lane={model.joinLane} />
</div>
) : (
<div className="access-depth-muted">No join lane yet</div>
)}
{strainInHospice(agent.spread_strain) && (
<div className="access-depth-hospice-note access-depth-muted">
strain in hospice museum read-only lineage
</div>
)}
{(agent.parent_agent_id || agent.spread_generation || agent.spread_strain) && (
<div className="access-depth-lineage" data-strain={agent.spread_strain?.replace(/^#/, '') ?? ''}>
lineage gen {agent.spread_generation ?? 0}
{agent.spread_strain ? (
<span
className="access-depth-strain-swatch"
style={{ backgroundColor: agent.spread_strain }}
title={`strain ${agent.spread_strain}`}
aria-hidden
/>
) : null}
{agent.parent_agent_id ? (
<span className="access-depth-muted"> · parent {agent.parent_agent_id.slice(0, 8)}</span>
) : null}
</div>
)}
{strainCards.length > 0 && (
<div className="access-depth-strain-cards">
{strainCards.slice(0, 2).map((card) => (
<div
key={card.id}
className="access-depth-strain-card"
data-strain={card.spread_strain?.replace(/^#/, '') ?? ''}
>
<div className="access-depth-strain-card-head">
{card.spread_strain ? (
<span
className="access-depth-strain-swatch"
style={{ backgroundColor: card.spread_strain }}
aria-hidden
/>
) : null}
<span className="access-depth-strain-card-title">
strain · {card.persona}
{strainInHospice(card.spread_strain) ? (
<span className="access-depth-tag access-depth-tag--skip"> hospice</span>
) : null}
</span>
<button
type="button"
className="access-depth-strain-play"
disabled={
agent.status !== 'online' ||
strainPlayBusy === card.id ||
strainInHospice(card.spread_strain)
}
onClick={() => playStrainCard(card)}
title={
strainInHospice(card.spread_strain)
? 'Strain retired to hospice'
: `Play ${card.source_agent_name} lineage preset`
}
>
{strainPlayBusy === card.id ? '…' : 'play'}
</button>
</div>
<div className="access-depth-strain-card-meta">
{card.wins.length}W · {card.losses.length}L · {card.subnets.length} subnets
{card.erasure_recovery_rate > 0
? ` · erasure ${Math.round(card.erasure_recovery_rate * 100)}%`
: ''}
</div>
</div>
))}
</div>
)}
{model.phenotypeSource && (
<div className="access-depth-phenotype">
phenotype cloned from <strong>{model.phenotypeSource}</strong>
{model.phenotypeSpreadLane ? (
<>
{' '}
· spread <JoinLaneBadge lane={model.phenotypeSpreadLane} />
</>
) : null}
</div>
)}
{serverPolicy.graft_enabled && (agent.graft_tier || agent.graft_source_strain) && (
<div className="access-depth-graft-note access-depth-muted">
genealogy graft pending · tier {agent.graft_tier}
{agent.graft_source_strain ? ` · strain ${agent.graft_source_strain}` : ''}
{' '}(applies on next spread)
</div>
)}
</div>
<div className="access-depth-section">
<div className="access-depth-section-title">Succeeded</div>
<AttemptMiniList rows={model.succeeded} empty="No successful tier attempts" />
</div>
<div className="access-depth-section">
<div className="access-depth-section-title">Failed / in progress</div>
<AttemptMiniList rows={model.failed} empty="No failed attempts" />
{model.inProgressLabel && (
<div className="access-depth-in-progress">
trying <strong>{model.inProgressLabel}</strong>
</div>
)}
{model.pendingLabels.length > 0 && (
<div className="access-depth-pending">
pending: {model.pendingLabels.slice(0, 6).join(' → ')}
{model.pendingLabels.length > 6 ? ` +${model.pendingLabels.length - 6}` : ''}
</div>
)}
</div>
</div>
{model.atlasSkips.length > 0 && (
<div className="access-depth-section access-depth-atlas-block">
<div className="access-depth-section-title">Atlas skips</div>
<ul className="access-depth-atlas-list">
{model.atlasSkips.map((skip) => (
<li key={`${skip.tier}-${skip.condition}`} className="access-depth-atlas-row">
{atlasSkipDisplayLabel(skip)}
</li>
))}
</ul>
</div>
)}
{model.strategyReasoning.length > 0 && (
<div className="access-depth-section access-depth-strategy-block">
<div className="access-depth-section-title">
Strategy
{model.adaptiveActive && (
<span className="access-depth-tag access-depth-tag--active access-depth-adaptive-badge">Adaptive</span>
)}
</div>
<ul className="access-depth-strategy-list">
{model.strategyReasoning.map((row, i) => (
<li key={`${row.action}-${i}`} className="access-depth-strategy-row">
<span className="access-depth-strategy-fact">{row.fact}</span>
<span className="access-depth-strategy-inference">{row.inference}</span>
<span className="access-depth-strategy-action">{row.action}</span>
</li>
))}
</ul>
{typeof model.adaptiveConfidence === 'number' && (
<div className="access-depth-meta">confidence {Math.round(model.adaptiveConfidence * 100)}%</div>
)}
</div>
)}
<div className="access-depth-section access-depth-onion-block">
<div className="access-depth-section-title">
Effective onion order
<span className="access-depth-source">({model.miningOrderSource})</span>
{model.adaptiveActive && (
<span className="access-depth-tag access-depth-tag--active access-depth-adaptive-badge">AI path</span>
)}
</div>
<div className="access-depth-onion-columns">
<div>
<div className="access-depth-onion-subtitle">Mining tiers</div>
<OnionList rows={model.miningOnion} empty="No mining tier chain" />
</div>
<div>
<div className="access-depth-onion-subtitle">
Spread contingency{' '}
<Link to="/settings" className="access-depth-calibrate-link">
Calibrate
</Link>
</div>
<OnionList rows={model.spreadOnion} empty="Default spread order" />
</div>
</div>
{model.tripleOnionSummary && (
<div className="access-depth-triple">
Triple onion: {model.tripleOnionSummary}
</div>
)}
<p className="access-depth-hint">
<HelpTip field="crucible_access_depth_calibrate" label="?" />{' '}
Calibrate <Link to="/settings">lotl_onion_tiers</Link> changes spread order on next agent reconnect.
</p>
</div>
</section>
);
}