feat(web): replace 3D graph with fleet map, add desktop background patterns, rename Knowledge Base to Fleet
Some checks failed
ci / build-test (push) Has been cancelled
ci / docker-build (push) Has been cancelled
ci / web (push) Has been cancelled
Desktop App / Build Linux (amd64) (push) Has been cancelled
Desktop App / Attach to Release (push) Has been cancelled

- FleetMap: service-centric host -> container -> service graph replacing
  the WebGL 3D force graph, with health coloring, hover-to-trace blast
  radius, and click-to-open
- Desktop background: configurable CSS pattern picker in Settings ->
  Appearance (8 patterns, color/fill/opacity/fade/size/rotation),
  replacing the hardcoded ambient graph background
- Fix missing data-orientation/data-disabled Tailwind custom variants so
  the shadcn Slider's track actually renders
- Rename "Knowledge Base" app to "Fleet"; scope its table to the same
  fleet entities as the graph (compute-entity descendants + service)
  instead of all entities
- Remove dead code: EntityGraph, GraphBackground, categories.ts,
  MultiSelectFilter (all superseded by the above)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-25 20:04:53 +02:00
parent c151a66627
commit 4e4e2c169c
16 changed files with 1754 additions and 996 deletions

View File

@@ -0,0 +1,118 @@
import { PATTERNS, type BackgroundPatternId } from '$lib/desktop-patterns'
export interface BackgroundConfig {
pattern: BackgroundPatternId
color: string // pattern foreground color, hex
fillColor: string | null // fill behind the pattern (its "gaps"); null = transparent, i.e. the app's own theme background shows through
opacity: number // 0..1
fade: number // 0..1 — 0 disables the vignette mask entirely
scale: number // pattern tile-size multiplier; 1 = natural size
rotation: number // degrees, 0..359
}
const STORAGE_KEY = 'oikos-desktop-bg'
const VALID_IDS = new Set(PATTERNS.map((p) => p.id))
// Matches the previous hardcoded desktop background (bubbles, brand blue,
// tuned low so it reads as texture rather than noise) so this feature ships
// as "now configurable" rather than a visual regression on first load.
const DEFAULTS: BackgroundConfig = {
pattern: 'bubbles',
color: '#444cf7',
fillColor: null,
opacity: 0.08,
fade: 0,
scale: 1,
rotation: 0
}
function clamp01(v: unknown, fallback: number): number {
const n = typeof v === 'number' ? v : Number(v)
if (!Number.isFinite(n)) return fallback
return Math.min(1, Math.max(0, n))
}
function clampScale(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v)
if (!Number.isFinite(n)) return DEFAULTS.scale
return Math.min(3, Math.max(0.4, n))
}
function clampRotation(v: unknown): number {
const n = typeof v === 'number' ? v : Number(v)
if (!Number.isFinite(n)) return DEFAULTS.rotation
return ((n % 360) + 360) % 360
}
function isHexColor(c: unknown): c is string {
return typeof c === 'string' && /^#[0-9a-fA-F]{6}$/.test(c)
}
function storedConfig(): BackgroundConfig {
if (typeof localStorage === 'undefined') return DEFAULTS
const raw = localStorage.getItem(STORAGE_KEY)
if (!raw) return DEFAULTS
try {
const parsed = JSON.parse(raw)
return {
pattern: VALID_IDS.has(parsed.pattern) ? parsed.pattern : DEFAULTS.pattern,
color: isHexColor(parsed.color) ? parsed.color : DEFAULTS.color,
fillColor: parsed.fillColor === null ? null : isHexColor(parsed.fillColor) ? parsed.fillColor : DEFAULTS.fillColor,
opacity: clamp01(parsed.opacity, DEFAULTS.opacity),
fade: clamp01(parsed.fade, DEFAULTS.fade),
scale: clampScale(parsed.scale),
rotation: clampRotation(parsed.rotation)
}
} catch {
return DEFAULTS
}
}
let config: BackgroundConfig = $state(storedConfig())
function persist(): void {
if (typeof localStorage !== 'undefined') localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
}
export function getBackground(): BackgroundConfig {
return config
}
export function setBackgroundPattern(pattern: BackgroundPatternId): void {
config = { ...config, pattern }
persist()
}
export function setPatternColor(color: string): void {
if (!isHexColor(color)) return
config = { ...config, color }
persist()
}
// null clears back to "auto" (transparent — the app's theme background
// shows through the pattern's gaps).
export function setFillColor(color: string | null): void {
if (color !== null && !isHexColor(color)) return
config = { ...config, fillColor: color }
persist()
}
export function setBackgroundOpacity(opacity: number): void {
config = { ...config, opacity: clamp01(opacity, DEFAULTS.opacity) }
persist()
}
export function setBackgroundFade(fade: number): void {
config = { ...config, fade: clamp01(fade, DEFAULTS.fade) }
persist()
}
export function setBackgroundScale(scale: number): void {
config = { ...config, scale: clampScale(scale) }
persist()
}
export function setBackgroundRotation(rotation: number): void {
config = { ...config, rotation: clampRotation(rotation) }
persist()
}