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