The only writing surface was `device_inventory.notes`, a single TEXT column rendered as plain preformatted text. No structure, no links, no search, and nowhere to write anything that is not about one device. This adds markdown documents, a navigable tree and a Documentation section in the left rail — the first app-level view that is not a canvas. Model — three tables. `documents` (kind: device / node / design / page / folder), `document_revisions` capped at 50 per document, and a `documents_fts` FTS5 index. A document attaches to the *inventory row*, not to a node, so one device reads the same on every canvas. Every link is SET NULL and the title is denormalized: deleting a device or a canvas must never destroy what the user wrote. Foreign keys are off in this SQLite setup, so `doc_links.unlink_documents` clears those columns by hand in the nodes, scan and designs delete paths. FTS5 is optional. The SQLite shipped in the LXC and Docker images may not carry it — `doc_search` probes once at boot and falls back to LIKE, and the search response names the engine it used so the UI can drop snippet highlighting. A device document is scaffolded once, from the scan facts: identity table, hardware, services (one section per service), network, operations, troubleshooting, dependencies, changelog. Then it belongs to the user and nothing rewrites it. The old `notes` are appended verbatim under `## Notes` behind a provenance comment, and `device_inventory.notes` is left untouched — HACS, the YAML export, MCP and canvas search still read it. Drift is surfaced, never corrected: `facts_snapshot` powers a banner, and `GET /documents/blocks` re-renders one generated block from current facts for the editor's slash commands. `review_every` plus `reviewed_at` produce a "needs attention" bucket, because homelab docs die of staleness. The Devices tree is pivoted client-side from the already-loaded inventory — zone, group, type, physical/virtual, subnet, rack, vendor, discovery source, status, tag, A-Z — so re-pivoting costs no request. The Library beside it is real folders the user makes. The Canvas pivot is scoped to the loaded canvas: `NodeData` carries no `design_id`, so naming every canvas a device appears on would be a guess. Saving is explicit, per the product rule. The dirty body is mirrored to localStorage and offered back on reopen; a draft whose base no longer matches the document is discarded rather than replayed. react-markdown is loaded behind React.lazy, so the canvas path does not pay for it: the main bundle is unchanged and the section brings its own 199 kB chunk. Raw HTML is not rendered, which removes the XSS surface and means no sanitizer. Export/import, a print view, an aggregated open-tasks view and images inside a document are deliberately not in this lot. MCP tools are lot 2. ha-relevant: yes
73 lines
2.0 KiB
JSON
73 lines
2.0 KiB
JSON
{
|
|
"name": "frontend",
|
|
"private": true,
|
|
"version": "3.3.5",
|
|
"type": "module",
|
|
"scripts": {
|
|
"dev": "vite",
|
|
"build": "tsc -b && vite build",
|
|
"lint": "eslint .",
|
|
"typecheck": "tsc -b",
|
|
"test": "vitest run",
|
|
"test:watch": "vitest",
|
|
"test:coverage": "vitest run --coverage",
|
|
"preview": "vite preview"
|
|
},
|
|
"dependencies": {
|
|
"@base-ui/react": "^1.2.0",
|
|
"@dagrejs/dagre": "^2.0.4",
|
|
"@fontsource-variable/geist": "^5.2.8",
|
|
"@fontsource-variable/inter": "^5.2.8",
|
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
|
"@radix-ui/react-tooltip": "^1.2.8",
|
|
"@types/js-yaml": "^4.0.9",
|
|
"@xyflow/react": "^12.10.1",
|
|
"axios": "^1.15.2",
|
|
"class-variance-authority": "^0.7.1",
|
|
"clsx": "^2.1.1",
|
|
"dagre": "^0.8.5",
|
|
"html-to-image": "^1.11.13",
|
|
"js-yaml": "^4.3.1",
|
|
"next-themes": "^0.4.6",
|
|
"react": "^19.2.0",
|
|
"react-dom": "^19.2.0",
|
|
"react-markdown": "^10.1.0",
|
|
"rehype-slug": "^6.0.0",
|
|
"remark-gfm": "^4.0.1",
|
|
"sonner": "^2.0.7",
|
|
"tailwind-merge": "^3.5.0",
|
|
"tw-animate-css": "^1.4.0",
|
|
"zustand": "^5.0.11"
|
|
},
|
|
"overrides": {
|
|
"hono": "^4.12.21",
|
|
"esbuild": "^0.28.1",
|
|
"form-data": "^4.0.6"
|
|
},
|
|
"devDependencies": {
|
|
"@eslint/js": "^9.39.1",
|
|
"@tailwindcss/vite": "^4.2.1",
|
|
"@testing-library/jest-dom": "^6.9.1",
|
|
"@testing-library/react": "^16.3.2",
|
|
"@testing-library/user-event": "^14.6.1",
|
|
"@types/dagre": "^0.7.54",
|
|
"@types/node": "^24.10.1",
|
|
"@types/react": "^19.2.7",
|
|
"@types/react-dom": "^19.2.3",
|
|
"@vitejs/plugin-react": "^5.1.1",
|
|
"@vitest/coverage-v8": "^4.0.18",
|
|
"eslint": "^9.39.0",
|
|
"eslint-plugin-react-hooks": "^7.0.1",
|
|
"eslint-plugin-react-refresh": "^0.4.24",
|
|
"globals": "^16.5.0",
|
|
"jsdom": "^28.1.0",
|
|
"lucide-react": "^1.7.0",
|
|
"shadcn": "^4.0.0",
|
|
"tailwindcss": "^4.2.1",
|
|
"typescript": "~5.9.3",
|
|
"typescript-eslint": "^8.48.0",
|
|
"vite": "^7.3.5",
|
|
"vitest": "^4.0.18"
|
|
}
|
|
}
|