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

@@ -9,7 +9,8 @@
import { iconPositions, resetIconLayout } from '$lib/stores/icons'
import { wm, openAppWindow, toggleShowDesktop } from '$lib/stores/windows'
import { summary } from '$lib/stores/context'
import GraphBackground from '../GraphBackground.svelte'
import { getBackground } from '$lib/stores/background.svelte'
import { patternCss } from '$lib/desktop-patterns'
import DesktopIcon from './DesktopIcon.svelte'
import TaskLauncher from './TaskLauncher.svelte'
import WindowLayer from './WindowLayer.svelte'
@@ -51,13 +52,42 @@
if (e.shiftKey) wm.redo()
else wm.undo()
}
// Configurable in Settings → Appearance (see background.svelte.ts). Two
// layers, not one, because rotation and the fade mask need different
// geometry:
// - outer: exactly the viewport box. Carries the fade mask, since a
// vignette has to be centered on what's actually visible.
// - inner: oversized (200%) and centered before rotating, so turning the
// pattern doesn't pull its straight edges into view at the corners —
// a viewport-sized box rotated in place would do exactly that.
const bgActive = $derived.by(() => {
const bg = getBackground()
return bg.pattern !== 'none' || bg.fillColor !== null
})
const bgOuterStyle = $derived.by(() => {
const bg = getBackground()
if (bg.fade <= 0) return ''
const stop = Math.round(100 - bg.fade * 70)
const mask = `radial-gradient(circle at 50% 50%, black 0%, black ${stop}%, transparent 100%)`
return `mask-image:${mask};-webkit-mask-image:${mask};`
})
const bgInnerStyle = $derived.by(() => {
const bg = getBackground()
const css = patternCss(bg.pattern, bg.color, bg.scale)
return `inset:-50%;width:200%;height:200%;opacity:${bg.opacity};background-color:${bg.fillColor ?? 'transparent'};transform:rotate(${bg.rotation}deg);${css}`
})
</script>
<svelte:window onkeydown={onWindowKeydown} />
<div class="fixed inset-0 flex flex-col">
<div class="relative min-h-0 flex-1 overflow-hidden" role="presentation">
<GraphBackground />
{#if bgActive}
<div class="pointer-events-none absolute inset-0 z-0 overflow-hidden" aria-hidden="true" style={bgOuterStyle}>
<div class="absolute" style={bgInnerStyle}></div>
</div>
{/if}
<ContextMenu.Root {onOpenChange}>
<!-- The bare-desktop hit area. Placed before the icons/windows layers

View File

@@ -32,6 +32,22 @@
if (appId && !idx.has(appId)) wm.close(id)
}
})
// Keep an open app window's title in sync with its registry entry. The
// title is copied into the window at open time and then persisted, so a
// rename (e.g. "Knowledge Base" -> "Fleet") would otherwise stay stuck in
// the titlebar/taskbar of any already-open or hydrated window until it was
// closed and reopened. Mirrors the task-window title sync in windows.ts.
$effect(() => {
const idx = $appById
for (const id of $wmState.order) {
const appId = appIdFromWindowId(id)
const app = appId ? idx.get(appId) : undefined
if (app && $wmState.windows[id]?.title !== app.title) {
wm.update(id, { title: app.title })
}
}
})
</script>
<div use:dk.desktop class="absolute inset-0 z-40 pointer-events-none">