diff --git a/src/app.css b/src/app.css index 1c4d2a8..a6f2d93 100644 --- a/src/app.css +++ b/src/app.css @@ -1,2 +1,30 @@ @import 'tailwindcss'; @plugin '@tailwindcss/typography'; + +:root { + --app-bg: #e2e8f0; + --panel-bg: #f8fafc; + --panel-border: #cbd5e1; + --panel-contrast: #0f172a; + --muted-text: #334155; + --chip-bg: #e2e8f0; + --chip-text: #0f172a; + --modal-overlay: rgba(15, 23, 42, 0.58); +} + +:root[data-theme='dark'] { + --app-bg: #0b1220; + --panel-bg: #111827; + --panel-border: #334155; + --panel-contrast: #e2e8f0; + --muted-text: #94a3b8; + --chip-bg: #1e293b; + --chip-text: #e2e8f0; + --modal-overlay: rgba(2, 6, 23, 0.78); +} + +html, +body { + background: var(--app-bg); + color: var(--panel-contrast); +} diff --git a/src/lib/config/iconRegistry.ts b/src/lib/config/iconRegistry.ts new file mode 100644 index 0000000..f139e5e --- /dev/null +++ b/src/lib/config/iconRegistry.ts @@ -0,0 +1,27 @@ +export interface IconDefinition { + key: string; + label: string; + path: string; +} + +const ICONS: IconDefinition[] = [ + { key: 'server', label: 'Server', path: '/icons/server.svg' }, + { key: 'router', label: 'Router', path: '/icons/router.svg' }, + { key: 'switch', label: 'Switch', path: '/icons/switch.svg' }, + { key: 'storage', label: 'Storage', path: '/icons/storage.svg' }, + { key: 'desktop', label: 'Desktop', path: '/icons/desktop.svg' }, + { key: 'cloud', label: 'Cloud', path: '/icons/cloud.svg' } +]; + +const ICON_BY_KEY = new Map(ICONS.map((icon) => [icon.key, icon])); + +export function listIconDefinitions(): IconDefinition[] { + return ICONS; +} + +export function resolveIconPath(iconKey?: string): string | undefined { + if (!iconKey) { + return undefined; + } + return ICON_BY_KEY.get(iconKey)?.path; +} diff --git a/src/lib/graph/transformNetworkData.ts b/src/lib/graph/transformNetworkData.ts index c3781b7..67afc16 100644 --- a/src/lib/graph/transformNetworkData.ts +++ b/src/lib/graph/transformNetworkData.ts @@ -1,4 +1,5 @@ import type { ConnectedPortRef, NetworkData, Port } from '../types'; +import { resolveIconPath } from '../config/iconRegistry'; import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types'; interface EndpointOwner { @@ -8,8 +9,12 @@ interface EndpointOwner { ports: Map; } -function toOwnerKey(kind: EndpointOwner['kind'], name: string): string { - return `${kind}:${name}`; +function normalizeOwnerName(name: string): string { + return name.trim().toLowerCase(); +} + +function toOwnerLookupKey(kind: EndpointOwner['kind'], name: string): string { + return `${kind}:${normalizeOwnerName(name)}`; } function toMachineNodeId(name: string): string { @@ -75,7 +80,7 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra vmCount: number; } > = {}; - const ownersByKey = new Map(); + const ownersByLookup = new Map(); const seenWarnings = new Set(); const warn = (message: string) => { @@ -86,14 +91,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra }; const addOwner = (owner: EndpointOwner) => { - const ownerKey = toOwnerKey(owner.kind, owner.name); - if (ownersByKey.has(ownerKey)) { + const ownerLookupKey = toOwnerLookupKey(owner.kind, owner.name); + if (ownersByLookup.has(ownerLookupKey)) { warn( `Duplicate ${owner.kind} name "${owner.name}" found. First occurrence will be used for link resolution.` ); return; } - ownersByKey.set(ownerKey, owner); + ownersByLookup.set(ownerLookupKey, owner); }; let hostingEdgeCounter = 0; @@ -110,24 +115,27 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra nodes.push({ data: { - id: machineNodeId, - label: machine.machineName, - rawName: machine.machineName, - kind: 'machine', + id: machineNodeId, + label: machine.machineName, + rawName: machine.machineName, + kind: 'machine', + iconKey: machine.iconKey, + iconUrl: resolveIconPath(machine.iconKey), nodeWidth: 200, nodeHeight: 110, details: { type: 'machine', - name: machine.machineName, - ip: machine.ipAddress, - role: machine.role, - os: machine.operatingSystem, - cpu: machine.hardware?.cpu ?? 'Unknown', - ram: machine.hardware?.ram ?? 'Unknown', - gpu: machine.hardware?.gpu ?? undefined, - ports: machinePortsList, - vmCount: machineVms.length, - vms: machineVms + name: machine.machineName, + iconKey: machine.iconKey, + ip: machine.ipAddress, + role: machine.role, + os: machine.operatingSystem, + cpu: machine.hardware?.cpu ?? 'Unknown', + ram: machine.hardware?.ram ?? 'Unknown', + gpu: machine.hardware?.gpu ?? undefined, + ports: machinePortsList, + vmCount: machineVms.length, + vms: machineVms } } }); @@ -148,11 +156,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra rawName: vm.name, kind: 'vm', hostMachineId: machineNodeId, + iconKey: vm.iconKey, + iconUrl: resolveIconPath(vm.iconKey), nodeWidth: 140, nodeHeight: 66, details: { type: 'vm', name: vm.name, + iconKey: vm.iconKey, ip: vm.ipAddress, role: vm.role, hostName: machine.machineName @@ -199,11 +210,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra label: device.name, rawName: device.name, kind: 'device', + iconKey: device.iconKey, + iconUrl: resolveIconPath(device.iconKey), nodeWidth: 176, nodeHeight: 116, details: { type: 'device', name: device.name, + iconKey: device.iconKey, ip: device.ipAddress, deviceType: device.type, notes: device.notes, @@ -228,15 +242,17 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra const seenPhysicalConnections = new Set(); let physicalEdgeCounter = 0; - for (const owner of ownersByKey.values()) { + for (const owner of ownersByLookup.values()) { for (const port of owner.ports.values()) { const connectedTo = parseConnectedTo(port.connectedTo); if (!connectedTo) { continue; } - const machineTargetOwner = ownersByKey.get(toOwnerKey('machine', connectedTo.device)); - const deviceTargetOwner = ownersByKey.get(toOwnerKey('device', connectedTo.device)); + const machineTargetOwner = ownersByLookup.get( + toOwnerLookupKey('machine', connectedTo.device) + ); + const deviceTargetOwner = ownersByLookup.get(toOwnerLookupKey('device', connectedTo.device)); if (machineTargetOwner && deviceTargetOwner) { warn( `Link from "${owner.name}:${port.portName}" references ambiguous device "${connectedTo.device}" (matches both machine and device names).` diff --git a/src/lib/graph/types.ts b/src/lib/graph/types.ts index e80324e..c2a5504 100644 --- a/src/lib/graph/types.ts +++ b/src/lib/graph/types.ts @@ -13,6 +13,7 @@ export interface PortDetails { export interface MachineDetails { type: 'machine'; name: string; + iconKey?: string; ip: string; role: string; os: string; @@ -31,6 +32,7 @@ export interface MachineDetails { export interface DeviceDetails { type: 'device'; name: string; + iconKey?: string; ip: string; deviceType: string; notes?: string; @@ -40,6 +42,7 @@ export interface DeviceDetails { export interface VmDetails { type: 'vm'; name: string; + iconKey?: string; ip: string; role: string; hostName: string; @@ -55,6 +58,8 @@ export interface GraphNodeData { hostMachineId?: string; nodeWidth?: number; nodeHeight?: number; + iconUrl?: string; + iconKey?: string; details?: GraphNodeDetails; } diff --git a/src/lib/stores/theme.ts b/src/lib/stores/theme.ts new file mode 100644 index 0000000..251aa6d --- /dev/null +++ b/src/lib/stores/theme.ts @@ -0,0 +1,55 @@ +import { writable } from 'svelte/store'; + +export type ThemeMode = 'light' | 'dark'; + +const STORAGE_KEY = 'ond-theme'; + +function getInitialTheme(): ThemeMode { + if (typeof window === 'undefined') { + return 'light'; + } + const stored = window.localStorage.getItem(STORAGE_KEY); + if (stored === 'light' || stored === 'dark') { + return stored; + } + return 'light'; +} + +function applyTheme(theme: ThemeMode) { + if (typeof document === 'undefined') { + return; + } + document.documentElement.setAttribute('data-theme', theme); +} + +function createThemeStore() { + const store = writable('light'); + + return { + subscribe: store.subscribe, + initialize() { + const theme = getInitialTheme(); + store.set(theme); + applyTheme(theme); + }, + toggle() { + store.update((current) => { + const next = current === 'dark' ? 'light' : 'dark'; + if (typeof window !== 'undefined') { + window.localStorage.setItem(STORAGE_KEY, next); + } + applyTheme(next); + return next; + }); + }, + set(theme: ThemeMode) { + if (typeof window !== 'undefined') { + window.localStorage.setItem(STORAGE_KEY, theme); + } + applyTheme(theme); + store.set(theme); + } + }; +} + +export const themeMode = createThemeStore(); diff --git a/src/lib/types.ts b/src/lib/types.ts index 2fb4293..939d2fc 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -21,6 +21,7 @@ export interface VM { name: string; role: string; ipAddress: string; + iconKey?: string; } export interface Software { @@ -32,6 +33,7 @@ export interface Machine { ipAddress: string; role: string; operatingSystem: string; + iconKey?: string; software: Software; hardware: Hardware; ports?: Port[]; @@ -41,6 +43,7 @@ export interface NetworkDevice { name: string; ipAddress: string; type: string; + iconKey?: string; notes?: string; ports?: Port[]; } diff --git a/static/icons/cloud.svg b/static/icons/cloud.svg new file mode 100644 index 0000000..5183aa5 --- /dev/null +++ b/static/icons/cloud.svg @@ -0,0 +1,3 @@ + + + diff --git a/static/icons/desktop.svg b/static/icons/desktop.svg new file mode 100644 index 0000000..b00337a --- /dev/null +++ b/static/icons/desktop.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/static/icons/router.svg b/static/icons/router.svg new file mode 100644 index 0000000..d422120 --- /dev/null +++ b/static/icons/router.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/static/icons/server.svg b/static/icons/server.svg new file mode 100644 index 0000000..4fff3dc --- /dev/null +++ b/static/icons/server.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/icons/storage.svg b/static/icons/storage.svg new file mode 100644 index 0000000..e84e3c6 --- /dev/null +++ b/static/icons/storage.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/static/icons/switch.svg b/static/icons/switch.svg new file mode 100644 index 0000000..313c47c --- /dev/null +++ b/static/icons/switch.svg @@ -0,0 +1,11 @@ + + + + + + + + + + +