fix: repair live view + settings in standalone multi-canvas mode

Two gaps surfaced after adding per-design localStorage storage:

- Live view (/view) still read the legacy bare `homelable_canvas` key, which
  no longer exists once canvases are keyed per design — the read-only tab
  rendered empty. Now passes the active design id (?design=<id>) and reads that
  design's canvas, falling back to the first design.
- The Settings modal was gated out entirely in standalone, leaving the Settings
  button dead even though canvas prefs (snap, hide-IP) work without a backend.
  Mount it in standalone; only the backend status-check section stays hidden.

ha-relevant: no
This commit is contained in:
Pouzor
2026-06-30 00:05:13 +02:00
parent ca171089c2
commit c356a65a5f
3 changed files with 58 additions and 18 deletions
@@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest'
import { render, screen, waitFor } from '@testing-library/react'
import { useCanvasStore } from '@/stores/canvasStore'
import { useThemeStore } from '@/stores/themeStore'
import * as standaloneStorage from '@/utils/standaloneStorage'
// ── Mock heavy dependencies ────────────────────────────────────────────────
@@ -213,9 +214,13 @@ describe('LiveView (non-standalone)', () => {
// ── Standalone mode ────────────────────────────────────────────────────────
// Captures props from the re-imported (resetModules) ReactFlow so standalone
// tests can assert which nodes were rendered without reaching into the fresh
// canvas-store module instance.
let standaloneRfProps: Record<string, unknown> = {}
const XYFLOW_MOCK = {
ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
ReactFlow: () => <div data-testid="react-flow" />,
ReactFlow: (props: Record<string, unknown>) => { standaloneRfProps = props; return <div data-testid="react-flow" /> },
Background: () => null,
Controls: () => null,
BackgroundVariant: { Dots: 'dots' },
@@ -235,16 +240,45 @@ describe('LiveView (standalone — localStorage)', () => {
vi.unstubAllEnvs()
})
it('loads canvas from localStorage without calling the API', async () => {
const stored = {
it('loads the active design canvas from localStorage without calling the API', async () => {
const design = standaloneStorage.createDesign('Main')
standaloneStorage.saveCanvas(design.id, {
nodes: [{
id: 'ls-node', type: 'router',
position: { x: 10, y: 20 },
data: { label: 'Router', type: 'router', status: 'unknown', services: [] },
}],
edges: [],
}
localStorage.setItem('homelable_canvas', JSON.stringify(stored))
})
vi.stubEnv('VITE_STANDALONE', 'true')
vi.resetModules()
const mockLoad = vi.fn()
vi.doMock('@xyflow/react', () => XYFLOW_MOCK)
vi.doMock('@xyflow/react/dist/style.css', () => ({}))
vi.doMock('@/api/client', () => ({ liveviewApi: { load: mockLoad } }))
const { default: LiveViewStandalone } = await import('../LiveView')
setSearch(`?design=${design.id}`)
render(<LiveViewStandalone />)
await waitFor(() => {
expect(screen.getByTestId('react-flow')).toBeDefined()
})
expect((standaloneRfProps.nodes as { id: string }[]).map((n) => n.id)).toContain('ls-node')
expect(mockLoad).not.toHaveBeenCalled()
})
it('falls back to the first design when no ?design= param is given', async () => {
const design = standaloneStorage.createDesign('Only')
standaloneStorage.saveCanvas(design.id, {
nodes: [{
id: 'fb-node', type: 'server',
position: { x: 0, y: 0 },
data: { label: 'Srv', type: 'server', status: 'unknown', services: [] },
}],
edges: [],
})
vi.stubEnv('VITE_STANDALONE', 'true')
vi.resetModules()
@@ -260,6 +294,7 @@ describe('LiveView (standalone — localStorage)', () => {
await waitFor(() => {
expect(screen.getByTestId('react-flow')).toBeDefined()
})
expect((standaloneRfProps.nodes as { id: string }[]).map((n) => n.id)).toContain('fb-node')
expect(mockLoad).not.toHaveBeenCalled()
})