Add scout constellation mode for APK venue persona packs.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Cluster 3+ scout_report hits on the same SSID within 10 minutes; server infers airport/campus/retail venue class and pushes persona spread_policy. Emberwake weather-map merges active scout biomes. Includes agent, server API, and Vitest coverage.
This commit is contained in:
AetherForge
2026-06-07 09:18:58 -07:00
parent 8b14582975
commit bbab38f8e1
60 changed files with 2610 additions and 43 deletions

View File

@@ -13,7 +13,7 @@ import {
formatClearanceElevation,
} from '../../help/clearance';
import { useWebSocket } from '../../hooks/useWebSocket';
import type { Agent } from '../../types';
import type { Agent, StrainCard } from '../../types';
import { HelpTip } from '../HelpTip';
import JoinLaneBadge from './JoinLaneBadge';
import LotlTierBadge from './LotlTierBadge';
@@ -77,6 +77,8 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
const [policyLoaded, setPolicyLoaded] = useState(false);
const [serverPolicy, setServerPolicy] = useState(parseAccessDepthServerPolicy({}));
const [elevationFlash, setElevationFlash] = useState<string | null>(null);
const [strainCards, setStrainCards] = useState<StrainCard[]>([]);
const [strainPlayBusy, setStrainPlayBusy] = useState<string | null>(null);
const flashTimerRef = useRef<number | null>(null);
const clearanceLevel = agent.clearance_level ?? 1;
@@ -124,6 +126,44 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
};
}, []);
useEffect(() => {
let cancelled = false;
api
.listStrainCards(agent.id)
.then((cards) => {
if (!cancelled) setStrainCards(cards ?? []);
})
.catch(() => {
if (!cancelled) setStrainCards([]);
});
return () => {
cancelled = true;
};
}, [agent.id]);
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 playStrainCard = async (card: StrainCard) => {
if (strainPlayBusy) 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],
@@ -205,6 +245,45 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
) : 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}
</span>
<button
type="button"
className="access-depth-strain-play"
disabled={agent.status !== 'online' || strainPlayBusy === card.id}
onClick={() => playStrainCard(card)}
title={`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>
@@ -216,6 +295,13 @@ export default function AccessDepthPanel({ agent, diagnostics }: Props) {
) : 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">