Files
AetherForge/server/web/src/components/Fleet/AccessDepthPanel.tsx
AetherForge 4204dd6b6d Add fleet phenotype cloning for sibling machines.
Publish winning tier paths per host fingerprint on hashrate success, inherit on auth before adaptive strategy, and surface clone badges in LOTL Timeline and Access Depth.
2026-06-07 02:28:00 -07:00

299 lines
11 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 } 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 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;
};
}, []);
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>}
</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>
)}
{model.phenotypeSource && (
<div className="access-depth-phenotype">
phenotype cloned from <strong>{model.phenotypeSource}</strong>
{model.phenotypeSpreadLane ? (
<>
{' '}
· spread <JoinLaneBadge lane={model.phenotypeSpreadLane} />
</>
) : null}
</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>
);
}