A device drawn on three canvases was three independent copies of the same facts. Point every node at the Device Inventory row it draws, and let that row own what the device *is* — the node keeps only how it is drawn. - nodes.device_id -> device_inventory.id, ON DELETE SET NULL. NULL for canvas furniture (group / groupRect / text), which describes nothing physical. Deleting a node never deletes the row. - services/inventory_sync holds the shared rules: matching by ieee > ip > mac (per token, so 10.0.0.4 never matches 10.0.0.40), a property union on key, a service union on (port, protocol, name), and the backfill that links every pre-existing node. - The backfill is non-destructive by construction: it writes device_id and fills the row, and deletes nothing. Nodes are visited oldest-edit-first, so where two canvases disagree on a scalar the most recently edited wins, while properties and services stay unioned — nothing any canvas recorded is lost. A second boot finds nothing to do. - The wire shape does not change: GET /canvas hydrates the device fields from the row, and a save routes them back to it. Editing a node's IP on one canvas now shows on every other canvas holding that device. - approve / bulk-approve set device_id instead of owning a copy, and a new canvas node joins (or mints) its row. Rows minted this way are tagged with a new `canvas` discovery source and get their own inventory filter. - DetailPanel offers "Open in inventory"; standalone, which has no inventory, is not offered it. test_racks' "reports what the canvas node knows" seeded a second inventory row for a host that already had one — a state a node create can no longer produce. Its seeding order is swapped so the node links to the row; every assertion is unchanged. ha-relevant: yes
React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- @vitejs/plugin-react uses Babel (or oxc when used in rolldown-vite) for Fast Refresh
- @vitejs/plugin-react-swc uses SWC for Fast Refresh
React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.
Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])