Add fleet groups, agent screenshots, deploy guards, and Crucible polish.
This commit is contained in:
97
server/web/src/help/fleetGroups.ts
Normal file
97
server/web/src/help/fleetGroups.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/** 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<string, unknown>;
|
||||
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));
|
||||
}
|
||||
Reference in New Issue
Block a user