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
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:
129
server/web/src/help/fleetHeatMap.ts
Normal file
129
server/web/src/help/fleetHeatMap.ts
Normal 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,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user