feat: floor plan map, LQI edge coloring, zigbee path highlighting, eslint/test fixes, vite audit fix

This commit is contained in:
Pranjal Joshi
2026-06-15 16:37:41 +05:30
parent c0c42d5f46
commit 8821b05b7b
14 changed files with 745 additions and 202 deletions
+43 -7
View File
@@ -26,6 +26,7 @@ import { SettingsModal } from '@/components/modals/SettingsModal'
import { ZigbeeImportModal } from '@/components/zigbee/ZigbeeImportModal'
import { ZwaveImportModal } from '@/components/zwave/ZwaveImportModal'
import { GroupRectModal, type GroupRectFormData } from '@/components/modals/GroupRectModal'
import { FloorMapModal } from '@/components/modals/FloorMapModal'
import { TextModal, type TextFormData } from '@/components/modals/TextModal'
import { ThemeModal } from '@/components/modals/ThemeModal'
import { SearchModal } from '@/components/modals/SearchModal'
@@ -41,14 +42,14 @@ import { canvasApi, designsApi, liveviewApi } from '@/api/client'
import * as standaloneStorage from '@/utils/standaloneStorage'
import { demoNodes, demoEdges } from '@/utils/demoData'
import { useStatusPolling } from '@/hooks/useStatusPolling'
import type { NodeData, EdgeData, CustomStyleDef } from '@/types'
import type { NodeData, EdgeData, CustomStyleDef, FloorMapConfig } from '@/types'
import type { ZigbeeNode, ZigbeeEdge } from '@/components/zigbee/types'
import type { ZwaveNode, ZwaveEdge } from '@/components/zwave/types'
const STANDALONE = import.meta.env.VITE_STANDALONE === 'true'
export default function App() {
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer } = useCanvasStore()
const { loadCanvas, markSaved, markUnsaved, selectedNodeId, selectedNodeIds, addNode, updateNode, deleteNode, onConnect, updateEdge, deleteEdge, setProxmoxContainerMode, setNodeZIndex, editingGroupRectId, setEditingGroupRectId, editingTextId, setEditingTextId, nodes, edges, snapshotHistory, undo, redo, addToGroup, addToContainer, floorMap, setFloorMap } = useCanvasStore()
const canvasRef = useRef<HTMLDivElement>(null)
const { isAuthenticated } = useAuthStore()
const { activeTheme, setTheme, customStyle, setCustomStyle } = useThemeStore()
@@ -82,6 +83,7 @@ export default function App() {
const [exportModalOpen, setExportModalOpen] = useState(false)
const [zigbeeImportOpen, setZigbeeImportOpen] = useState(false)
const [zwaveImportOpen, setZwaveImportOpen] = useState(false)
const [floorMapModalOpen, setFloorMapModalOpen] = useState(false)
// Declare handleSave before the Ctrl+S effect so it is in scope.
// Returns true on success, false on failure — the design-switch effect relies
@@ -91,14 +93,16 @@ export default function App() {
const saveDesignId = designIdOverride ?? activeDesignId
if (STANDALONE) {
if (!saveDesignId) return false
standaloneStorage.saveCanvas(saveDesignId, { nodes, edges, theme_id: activeTheme, custom_style: customStyle })
standaloneStorage.saveCanvas(saveDesignId, { nodes, edges, theme_id: activeTheme, custom_style: customStyle, floorMap })
markSaved()
toast.success('Canvas saved')
return true
}
const nodesToSave = nodes.map(serializeNode)
const edgesToSave = edges.map(serializeEdge)
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport: { theme_id: activeTheme }, custom_style: customStyle, design_id: saveDesignId })
const viewport: Record<string, unknown> = { theme_id: activeTheme }
if (floorMap) viewport.floor_map = floorMap
await canvasApi.save({ nodes: nodesToSave, edges: edgesToSave, viewport, custom_style: customStyle, design_id: saveDesignId })
markSaved()
toast.success('Canvas saved')
return true
@@ -106,7 +110,7 @@ export default function App() {
toast.error('Save failed')
return false
}
}, [nodes, edges, markSaved, activeTheme, customStyle, activeDesignId])
}, [nodes, edges, markSaved, activeTheme, customStyle, activeDesignId, floorMap])
// Keep a ref so the keydown handler always calls the latest version
const handleSaveRef = useRef(handleSave)
@@ -127,6 +131,8 @@ export default function App() {
const savedTheme = res.data.viewport?.theme_id
if (savedTheme) setTheme(savedTheme)
if (res.data.custom_style) setCustomStyle(res.data.custom_style as CustomStyleDef)
const savedFloorMap = res.data.viewport?.floor_map as FloorMapConfig | undefined
if (savedFloorMap) setFloorMap(savedFloorMap)
loadCanvas(rfNodes, rfEdges)
} else {
loadCanvas(demoNodes, demoEdges)
@@ -134,7 +140,7 @@ export default function App() {
} catch {
loadCanvas(demoNodes, demoEdges)
}
}, [loadCanvas, setTheme, setCustomStyle])
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
// Standalone counterpart of loadCanvasFromApi — reads a design's canvas from
// localStorage, falling back to the demo canvas when it has never been saved.
@@ -143,11 +149,12 @@ export default function App() {
if (saved && saved.nodes.length > 0) {
if (saved.theme_id) setTheme(saved.theme_id)
if (saved.custom_style) setCustomStyle(saved.custom_style)
if (saved.floorMap) setFloorMap(saved.floorMap)
loadCanvas(saved.nodes, saved.edges)
} else {
loadCanvas(demoNodes, demoEdges)
}
}, [loadCanvas, setTheme, setCustomStyle])
}, [loadCanvas, setTheme, setCustomStyle, setFloorMap])
const loadDesignsAndCanvas = useCallback(async () => {
if (STANDALONE) {
@@ -518,6 +525,25 @@ export default function App() {
}
}, [activeDesignId])
const handleFloorMapSubmit = useCallback((config: FloorMapConfig) => {
snapshotHistory()
if (floorMap) {
// Preserve position from existing config when editing
setFloorMap({ ...floorMap, ...config })
} else {
setFloorMap(config)
}
setFloorMapModalOpen(false)
toast.success(floorMap ? 'Floor plan updated' : 'Floor plan imported')
}, [floorMap, setFloorMap, snapshotHistory])
const handleFloorMapRemove = useCallback(() => {
snapshotHistory()
setFloorMap(null)
setFloorMapModalOpen(false)
toast.success('Floor plan removed')
}, [setFloorMap, snapshotHistory])
const handleExport = useCallback(() => {
const el = canvasRef.current?.querySelector<HTMLElement>('.react-flow')
if (!el) { toast.error('Canvas not ready'); return }
@@ -697,6 +723,7 @@ export default function App() {
onScan={() => setScanConfigOpen(true)}
onZigbeeImport={() => setZigbeeImportOpen(true)}
onZwaveImport={() => setZwaveImportOpen(true)}
onFloorMap={() => setFloorMapModalOpen(true)}
onSave={handleSave}
onOpenSettings={() => setSettingsOpen(true)}
onOpenHistory={() => setScanHistoryOpen(true)}
@@ -833,6 +860,15 @@ export default function App() {
/>
)}
<FloorMapModal
key={floorMapModalOpen ? 'floor-map-open' : 'floor-map-closed'}
open={floorMapModalOpen}
onClose={() => setFloorMapModalOpen(false)}
onSubmit={handleFloorMapSubmit}
onRemove={handleFloorMapRemove}
initial={floorMap}
/>
<GroupRectModal
open={addGroupRectOpen}
onClose={() => setAddGroupRectOpen(false)}