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,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;
}