Add scout constellation mode for APK venue persona packs.
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user