/** Fleet node groups — persisted in localStorage, shared across Roster + Crucible. */ export interface FleetGroup { id: string; name: string; color: string; agentIds: string[]; createdAt: string; } export const FLEET_GROUP_COLORS = [ '#00f5ff', '#39ff14', '#ff2da6', '#b24bf3', '#ffb020', '#ff6b35', '#00d4aa', '#3a86ff', '#f72585', '#ffd60a', '#06d6a0', '#e63946', ] as const; export const FLEET_GROUPS_STORAGE_KEY = 'aetherforge_fleet_groups'; export const FLEET_GROUPS_CHANGED_EVENT = 'aetherforge-fleet-groups-changed'; export function normalizeGroupColor(color: string): string { const c = color.trim(); if (/^#[0-9A-Fa-f]{6}$/.test(c)) return c; if (/^#[0-9A-Fa-f]{3}$/.test(c)) { const r = c[1]; const g = c[2]; const b = c[3]; return `#${r}${r}${g}${g}${b}${b}`; } return FLEET_GROUP_COLORS[0]; } export function loadFleetGroups(): FleetGroup[] { try { const raw = localStorage.getItem(FLEET_GROUPS_STORAGE_KEY); if (!raw) return []; const parsed = JSON.parse(raw) as unknown; if (!Array.isArray(parsed)) return []; return parsed .map((g) => { if (!g || typeof g !== 'object') return null; const o = g as Record; const name = typeof o.name === 'string' ? o.name.trim() : ''; if (!name) return null; const agentIds = Array.isArray(o.agentIds) ? [...new Set(o.agentIds.filter((id): id is string => typeof id === 'string' && id.length > 0))] : []; return { id: typeof o.id === 'string' && o.id ? o.id : `fg-${Date.now()}`, name, color: normalizeGroupColor(typeof o.color === 'string' ? o.color : FLEET_GROUP_COLORS[0]), agentIds, createdAt: typeof o.createdAt === 'string' ? o.createdAt : new Date().toISOString(), } satisfies FleetGroup; }) .filter((g): g is FleetGroup => g !== null); } catch { return []; } } export function saveFleetGroups(groups: FleetGroup[]): void { localStorage.setItem(FLEET_GROUPS_STORAGE_KEY, JSON.stringify(groups)); window.dispatchEvent(new Event(FLEET_GROUPS_CHANGED_EVENT)); } export function createFleetGroup(name: string, color: string, agentIds: string[]): FleetGroup { return { id: `fg-${crypto.randomUUID?.() ?? `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`}`, name: name.trim(), color: normalizeGroupColor(color), agentIds: [...new Set(agentIds)], createdAt: new Date().toISOString(), }; } /** Groups that contain this agent (preserves group list order). */ export function groupsForAgent(groups: FleetGroup[], agentId: string): FleetGroup[] { return groups.filter((g) => g.agentIds.includes(agentId)); } /** First group color for an agent (roster stripe / Crucible accent). */ export function primaryGroupForAgent(groups: FleetGroup[], agentId: string): FleetGroup | undefined { return groups.find((g) => g.agentIds.includes(agentId)); } export function notifyFleetGroupsChanged(): void { window.dispatchEvent(new Event(FLEET_GROUPS_CHANGED_EVENT)); }