diff --git a/.gitignore b/.gitignore index e2cbe7a..d448195 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ Thumbs.db # User-provided Docker data data/network.json +data/.backups # Vite vite.config.js.timestamp-* diff --git a/.prettierignore b/.prettierignore index 6562bcb..2df23c2 100644 --- a/.prettierignore +++ b/.prettierignore @@ -4,3 +4,4 @@ pnpm-lock.yaml yarn.lock bun.lock bun.lockb +src/lib/config/vendorIconManifest.ts diff --git a/Dockerfile b/Dockerfile index 6dca253..e3cd5b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,24 +15,28 @@ COPY . . ENV DEPLOY_TARGET=docker RUN pnpm run build:docker -FROM nginxinc/nginx-unprivileged:1.27-alpine AS runner +FROM node:20-alpine AS runner -USER root +WORKDIR /app +ENV NODE_ENV=production +ENV HOST=0.0.0.0 +ENV PORT=3000 -RUN apk add --no-cache libcap && \ - setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx +RUN addgroup -S appuser && adduser -S -G appuser appuser -COPY nginx/default.conf /etc/nginx/conf.d/default.conf -COPY --from=builder /app/build /usr/share/nginx/html +COPY --from=builder /app/build ./build +COPY --from=builder /app/server.mjs ./server.mjs +COPY --from=builder /app/src/lib/shared ./src/lib/shared +COPY --from=builder /app/data ./data -RUN mkdir -p /usr/share/nginx/html/data && \ - chown -R 101:101 /usr/share/nginx/html /var/cache/nginx /var/run /etc/nginx/conf.d +RUN chown -R appuser:appuser /app/build /app/server.mjs /app/data /app \ + && chmod -R u+rwX /app /app/data -USER 101 +EXPOSE 3000 -EXPOSE 80 +USER appuser HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ - CMD wget -qO- http://127.0.0.1/ > /dev/null || exit 1 + CMD wget -qO- http://127.0.0.1:3000/ > /dev/null || exit 1 -CMD ["nginx", "-g", "daemon off;"] +CMD ["node", "server.mjs"] diff --git a/README.md b/README.md index fa36b88..4f13259 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,9 @@ ✅ **Fully self-hostable via Docker** ✅ **Docker-first deployment target** (Netlify optional for demo hosting) ✅ **Interactive network visualisation** -✅ **Simple file-based configuration** +✅ **Single-page modal editor with live updates** +✅ **Debounced autosave to JSON (self-hosted)** +✅ **Local vendored icon catalog (offline runtime)** ✅ **Lightweight Svelte** Use it to **document your home lab, office network, or cloud infrastructure** with an easy-to-use web interface. @@ -45,22 +47,26 @@ docker compose up --build Or pull and run directly: ```bash -docker run -d -p 8080:80 -v "$(pwd)/data:/usr/share/nginx/html/data:ro" jcreek23/open-network-diagram +docker run -d -p 8080:3000 \ + -e NETWORK_DATA_FILE=/app/data/network.json \ + -e NETWORK_BACKUP_DIR=/app/data/.backups \ + -v "$(pwd)/data:/app/data" \ + jcreek23/open-network-diagram ``` -- **`-p 8080:80`** → Maps the app to `http://localhost:8080` -- **`-v .../data:/usr/share/nginx/html/data:ro`** → Uses your local `data/network.json` +- **`-p 8080:3000`** → Maps the app to `http://localhost:8080` +- **`-v .../data:/app/data`** → Uses your local writable `data/network.json` ### **3️⃣ Open the Web UI** Visit **`http://localhost:8080`** to view your network diagram. -### **4️⃣ Modify Your Network (JSON-Based)** +### **4️⃣ Modify Your Network (Modal UI + JSON Persistence)** -- Docker runtime reads **`/data/network.json`** from the mounted volume. -- In this repo, your editable file is **`data/network.json`** (gitignored). -- Netlify demo builds use committed sample data at **`static/data/network.json`**. -- After editing JSON, click **Reload Default JSON** in the UI. +- Edit machines/devices/VMs/ports directly in the modal UI. +- Diagram updates immediately as you edit. +- In self-hosted Docker/local mode, changes autosave to mounted **`data/network.json`**. +- Netlify/demo is intentionally read-only; edits are in-memory only. --- @@ -92,7 +98,8 @@ pnpm run dev ```bash pnpm run build # default (Docker/static target) pnpm run build:docker # explicit Docker/static target -pnpm run build:netlify # explicit Netlify target +pnpm run build:netlify # Netlify target (read-only mode) +pnpm run icons:manifest # regenerate local vendor icon manifest ``` --- @@ -102,9 +109,13 @@ pnpm run build:netlify # explicit Netlify target ```text open-network-diagram/ ├── src/ # Svelte app source +├── src/lib/config/vendorIconManifest.ts # Generated local icon catalog ├── static/data/network.json # Demo dataset (Netlify/demo) +├── static/icons/vendor/ # Vendored icon assets (runtime-local) ├── data/network.json.example # User data template (Docker) +├── third_party/ # Third-party license/provenance notes ├── Dockerfile # Docker build/runtime +├── server.mjs # Node runtime server (static + /api/network-data) ├── docker-compose.yml # Local Docker run with mounted data ├── netlify.toml # Netlify build config ├── .github/workflows/ # CI workflows (PR build + automated release/publish) @@ -124,9 +135,32 @@ docker build -t open-network-diagram . ### **Run Locally** ```bash -docker run --rm -p 8080:80 -v "$(pwd)/data:/usr/share/nginx/html/data:ro" open-network-diagram +docker run --rm -p 8080:3000 \ + -e NETWORK_DATA_FILE=/app/data/network.json \ + -e NETWORK_BACKUP_DIR=/app/data/.backups \ + -v "$(pwd)/data:/app/data" \ + open-network-diagram ``` +### **Write API Environment Variables** + +- **`NETWORK_READ_ONLY`** (default: `false`) + Set to `true` to disable `PUT /api/network-data` and force read-only mode. +- **`NETWORK_DATA_FILE`** (default: `data/network.json`) + JSON file path to read/write. +- **`NETWORK_BACKUP_DIR`** (default: sibling `.backups`) + Backup directory for rolling save backups (last 5 retained). + +### **Local Icon Catalog (No Runtime Network Dependency)** + +- Icons are vendored locally under **`static/icons/vendor/homarr/`**. +- The searchable catalog is generated into **`src/lib/config/vendorIconManifest.ts`**. +- Third-party provenance and licensing are documented in: + - **`third_party/homarr-dashboard-icons/SOURCE.txt`** + - **`third_party/homarr-dashboard-icons/LICENSE`** + - **`third_party/homarr-dashboard-icons/NOTICE.txt`** +- Runtime icon search/rendering does not call external APIs. + ### **CI/CD (GitHub Actions + Netlify)** - GitHub Actions workflow (`.github/workflows/docker.yml`) builds Docker on PRs (validation only). @@ -163,16 +197,6 @@ Define your network using **`network.json`**: --- -## **🔜 Roadmap** - -✅ **Initial version with JSON-based diagrams** -⏳ **Drag-and-drop editing in the UI** -⏳ **Custom icons for different devices** -⏳ **Export diagrams as PNG/SVG/Graphviz** -⏳ **Dark mode & UI themes** - ---- - ## **🤝 Contributing** We welcome contributions! To contribute: diff --git a/docker-compose.yml b/docker-compose.yml index d4f567c..280ab4b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,7 +5,10 @@ services: dockerfile: Dockerfile image: open-network-diagram:local ports: - - "8080:80" + - "8080:3000" volumes: - - ./data:/usr/share/nginx/html/data:ro + - ./data:/app/data + environment: + NETWORK_DATA_FILE: "/app/data/network.json" + NETWORK_BACKUP_DIR: "/app/data/.backups" restart: unless-stopped diff --git a/eslint.config.js b/eslint.config.js index 3a02127..c25a8c6 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -43,6 +43,10 @@ export default tseslint.config( }, settings: { 'import/resolver': { + typescript: { + alwaysTryTypes: true, + project: ['./tsconfig.json'] + }, node: { extensions: ['.js', '.mjs', '.cjs', '.ts', '.d.ts', '.svelte', '.json'] } @@ -57,17 +61,10 @@ export default tseslint.config( 'no-inner-declarations': 'off', 'no-unused-vars': 'off', 'import/no-extraneous-dependencies': 'off', + 'import/no-unresolved': 'error', + 'import/prefer-default-export': 'off', '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }], - 'import/extensions': [ - 'error', - 'ignorePackages', - { - js: 'never', - mjs: 'never', - cjs: 'never', - ts: 'never' - } - ] + 'import/extensions': 'off' } }, { diff --git a/package.json b/package.json index ec7bcf1..4fbd7d5 100644 --- a/package.json +++ b/package.json @@ -7,11 +7,13 @@ "dev": "vite dev", "build": "vite build", "build:docker": "DEPLOY_TARGET=docker vite build", - "build:netlify": "DEPLOY_TARGET=netlify vite build", + "build:netlify": "NETWORK_READ_ONLY=true DEPLOY_TARGET=netlify vite build", + "start": "node server.mjs", "preview": "vite preview", "prepare": "svelte-kit sync || echo ''", "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "icons:manifest": "node scripts/generate-vendor-icon-manifest.mjs", "format": "prettier --write .", "lint": "prettier --check . && eslint ." }, @@ -27,9 +29,11 @@ "@tailwindcss/typography": "^0.5.15", "@tailwindcss/vite": "^4.0.0", "@types/cytoscape-dagre": "^2.3.3", + "@types/node": "^22.13.10", "eslint": "^8.57.1", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-prettier": "^10.1.8", + "eslint-import-resolver-typescript": "^4.4.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-svelte": "^3.0.0", "globals": "^16.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b834361..5f1cb2b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,40 +26,46 @@ importers: version: 8.57.1 '@sveltejs/adapter-auto': specifier: ^4.0.0 - version: 4.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))) + version: 4.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))) '@sveltejs/adapter-netlify': specifier: ^5.0.0 - version: 5.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))) + version: 5.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))) '@sveltejs/adapter-static': specifier: ^3.0.10 - version: 3.0.10(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))) + version: 3.0.10(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))) '@sveltejs/kit': specifier: ^2.16.0 - version: 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + version: 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.0 - version: 5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + version: 5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) '@tailwindcss/typography': specifier: ^0.5.15 version: 0.5.16(tailwindcss@4.1.3) '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.1.3(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + version: 4.1.3(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) '@types/cytoscape-dagre': specifier: ^2.3.3 version: 2.3.3 + '@types/node': + specifier: ^22.13.10 + version: 22.19.11 eslint: specifier: ^8.57.1 version: 8.57.1 eslint-config-airbnb-base: specifier: ^15.0.0 - version: 15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1) + version: 15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-config-prettier: specifier: ^10.1.8 version: 10.1.8(eslint@8.57.1) + eslint-import-resolver-typescript: + specifier: ^4.4.4 + version: 4.4.4(eslint-plugin-import@2.32.0)(eslint@8.57.1) eslint-plugin-import: specifier: ^2.32.0 - version: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) + version: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) eslint-plugin-svelte: specifier: ^3.0.0 version: 3.5.1(eslint@8.57.1)(svelte@5.25.8) @@ -80,7 +86,7 @@ importers: version: 5.25.8 svelte-check: specifier: ^4.0.0 - version: 4.1.5(svelte@5.25.8)(typescript@5.9.3) + version: 4.1.5(picomatch@4.0.3)(svelte@5.25.8)(typescript@5.9.3) tailwindcss: specifier: ^4.0.0 version: 4.1.3 @@ -92,7 +98,7 @@ importers: version: 8.29.1(eslint@8.57.1)(typescript@5.9.3) vite: specifier: ^6.2.5 - version: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2) + version: 6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2) packages: @@ -100,6 +106,15 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} + '@emnapi/core@1.8.1': + resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + + '@emnapi/runtime@1.8.1': + resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + + '@emnapi/wasi-threads@1.1.0': + resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@esbuild/aix-ppc64@0.24.2': resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==} engines: {node: '>=18'} @@ -465,6 +480,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@napi-rs/wasm-runtime@0.2.12': + resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -710,6 +728,9 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -725,6 +746,9 @@ packages: '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} + '@types/node@22.19.11': + resolution: {integrity: sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==} + '@typescript-eslint/eslint-plugin@8.29.1': resolution: {integrity: sha512-ba0rr4Wfvg23vERs3eB+P3lfj2E+2g3lhWcCVukUuhtcdUx5lSIFZlGFEBHKr+3zizDa/TvZTptdNHVZWAkSBg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -775,6 +799,101 @@ packages: '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==} + cpu: [arm] + os: [android] + + '@unrs/resolver-binding-android-arm64@1.11.1': + resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==} + cpu: [arm64] + os: [android] + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==} + cpu: [arm64] + os: [darwin] + + '@unrs/resolver-binding-darwin-x64@1.11.1': + resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==} + cpu: [x64] + os: [darwin] + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==} + cpu: [x64] + os: [freebsd] + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==} + cpu: [arm] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==} + cpu: [arm64] + os: [linux] + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==} + cpu: [ppc64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==} + cpu: [riscv64] + os: [linux] + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==} + cpu: [s390x] + os: [linux] + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==} + cpu: [x64] + os: [linux] + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==} + cpu: [arm64] + os: [win32] + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==} + cpu: [ia32] + os: [win32] + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==} + cpu: [x64] + os: [win32] + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -947,6 +1066,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -1040,9 +1168,31 @@ packages: peerDependencies: eslint: '>=7.0.0' + eslint-import-context@0.1.9: + resolution: {integrity: sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + peerDependencies: + unrs-resolver: ^1.0.0 + peerDependenciesMeta: + unrs-resolver: + optional: true + eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} + eslint-import-resolver-typescript@4.4.4: + resolution: {integrity: sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==} + engines: {node: ^16.17.0 || >=18.6.0} + peerDependencies: + eslint: '*' + eslint-plugin-import: '*' + eslint-plugin-import-x: '*' + peerDependenciesMeta: + eslint-plugin-import: + optional: true + eslint-plugin-import-x: + optional: true + eslint-module-utils@2.12.1: resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==} engines: {node: '>=4'} @@ -1160,6 +1310,15 @@ packages: picomatch: optional: true + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + file-entry-cache@6.0.1: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} @@ -1217,6 +1376,9 @@ packages: resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -1327,6 +1489,9 @@ packages: resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} engines: {node: '>= 0.4'} + is-bun-module@2.0.0: + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} + is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -1587,6 +1752,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + napi-postinstall@0.3.4: + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + hasBin: true + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -1663,6 +1833,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -1796,6 +1970,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -1890,6 +2067,10 @@ packages: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + stable-hash-x@0.2.0: + resolution: {integrity: sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==} + engines: {node: '>=12.0.0'} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -1957,6 +2138,10 @@ packages: text-table@0.2.0: resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -1974,6 +2159,9 @@ packages: tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2014,6 +2202,12 @@ packages: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + unrs-resolver@1.11.1: + resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -2114,6 +2308,22 @@ snapshots: '@jridgewell/gen-mapping': 0.3.8 '@jridgewell/trace-mapping': 0.3.25 + '@emnapi/core@1.8.1': + dependencies: + '@emnapi/wasi-threads': 1.1.0 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.8.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.1.0': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.24.2': optional: true @@ -2336,6 +2546,13 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@napi-rs/wasm-runtime@0.2.12': + dependencies: + '@emnapi/core': 1.8.1 + '@emnapi/runtime': 1.8.1 + '@tybys/wasm-util': 0.10.1 + optional: true + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -2416,25 +2633,25 @@ snapshots: dependencies: acorn: 8.14.1 - '@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))': + '@sveltejs/adapter-auto@4.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))': dependencies: - '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) import-meta-resolve: 4.1.0 - '@sveltejs/adapter-netlify@5.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))': + '@sveltejs/adapter-netlify@5.0.0(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))': dependencies: '@iarna/toml': 2.2.5 - '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) esbuild: 0.24.2 set-cookie-parser: 2.7.1 - '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))': + '@sveltejs/adapter-static@3.0.10(@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))': dependencies: - '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + '@sveltejs/kit': 2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) - '@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))': + '@sveltejs/kit@2.20.4(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) '@types/cookie': 0.6.0 cookie: 0.6.0 devalue: 5.1.1 @@ -2447,27 +2664,27 @@ snapshots: set-cookie-parser: 2.7.1 sirv: 3.0.1 svelte: 5.25.8 - vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2) - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + '@sveltejs/vite-plugin-svelte': 5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) debug: 4.4.0 svelte: 5.25.8 - vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))': + '@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.0.3(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)))(svelte@5.25.8)(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) debug: 4.4.0 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.17 svelte: 5.25.8 - vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2) - vitefu: 1.0.6(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)) + vite: 6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2) + vitefu: 1.0.6(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)) transitivePeerDependencies: - supports-color @@ -2533,12 +2750,17 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.3 - '@tailwindcss/vite@4.1.3(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2))': + '@tailwindcss/vite@4.1.3(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2))': dependencies: '@tailwindcss/node': 4.1.3 '@tailwindcss/oxide': 4.1.3 tailwindcss: 4.1.3 - vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2) + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true '@types/cookie@0.6.0': {} @@ -2552,6 +2774,10 @@ snapshots: '@types/json5@0.0.29': {} + '@types/node@22.19.11': + dependencies: + undici-types: 6.21.0 + '@typescript-eslint/eslint-plugin@8.29.1(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.1 @@ -2631,6 +2857,65 @@ snapshots: '@ungap/structured-clone@1.3.0': {} + '@unrs/resolver-binding-android-arm-eabi@1.11.1': + optional: true + + '@unrs/resolver-binding-android-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-arm64@1.11.1': + optional: true + + '@unrs/resolver-binding-darwin-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-freebsd-x64@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-gnueabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm-musleabihf@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-arm64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-ppc64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-riscv64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-s390x-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-gnu@1.11.1': + optional: true + + '@unrs/resolver-binding-linux-x64-musl@1.11.1': + optional: true + + '@unrs/resolver-binding-wasm32-wasi@1.11.1': + dependencies: + '@napi-rs/wasm-runtime': 0.2.12 + optional: true + + '@unrs/resolver-binding-win32-arm64-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-ia32-msvc@1.11.1': + optional: true + + '@unrs/resolver-binding-win32-x64-msvc@1.11.1': + optional: true + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -2815,6 +3100,10 @@ snapshots: dependencies: ms: 2.1.3 + debug@4.4.3: + dependencies: + ms: 2.1.3 + deep-is@0.1.4: {} deepmerge@4.3.1: {} @@ -2994,11 +3283,11 @@ snapshots: escape-string-regexp@4.0.0: {} - eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1))(eslint@8.57.1): + eslint-config-airbnb-base@15.0.0(eslint-plugin-import@2.32.0)(eslint@8.57.1): dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.1 - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) object.assign: 4.1.7 object.entries: 1.1.9 semver: 6.3.1 @@ -3007,6 +3296,13 @@ snapshots: dependencies: eslint: 8.57.1 + eslint-import-context@0.1.9(unrs-resolver@1.11.1): + dependencies: + get-tsconfig: 4.13.6 + stable-hash-x: 0.2.0 + optionalDependencies: + unrs-resolver: 1.11.1 + eslint-import-resolver-node@0.3.9: dependencies: debug: 3.2.7 @@ -3015,17 +3311,33 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1): + eslint-import-resolver-typescript@4.4.4(eslint-plugin-import@2.32.0)(eslint@8.57.1): + dependencies: + debug: 4.4.3 + eslint: 8.57.1 + eslint-import-context: 0.1.9(unrs-resolver@1.11.1) + get-tsconfig: 4.13.6 + is-bun-module: 2.0.0 + stable-hash-x: 0.2.0 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) + transitivePeerDependencies: + - supports-color + + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.29.1(eslint@8.57.1)(typescript@5.9.3) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 + eslint-import-resolver-typescript: 4.4.4(eslint-plugin-import@2.32.0)(eslint@8.57.1) transitivePeerDependencies: - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint@8.57.1): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -3036,7 +3348,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint@8.57.1) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.29.1(eslint@8.57.1)(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@4.4.4)(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -3176,7 +3488,13 @@ snapshots: dependencies: reusify: 1.1.0 - fdir@6.4.3: {} + fdir@6.4.3(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 file-entry-cache@6.0.1: dependencies: @@ -3247,6 +3565,10 @@ snapshots: es-errors: 1.3.0 get-intrinsic: 1.3.0 + get-tsconfig@4.13.6: + dependencies: + resolve-pkg-maps: 1.0.0 + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -3356,6 +3678,10 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-bun-module@2.0.0: + dependencies: + semver: 7.7.1 + is-callable@1.2.7: {} is-core-module@2.16.1: @@ -3572,6 +3898,8 @@ snapshots: nanoid@3.3.11: {} + napi-postinstall@0.3.4: {} + natural-compare@1.4.0: {} object-inspect@1.13.4: {} @@ -3657,6 +3985,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.3: {} + possible-typed-array-names@1.1.0: {} postcss-load-config@3.1.4(postcss@8.5.3): @@ -3733,6 +4063,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -3868,6 +4200,8 @@ snapshots: source-map-js@1.2.1: {} + stable-hash-x@0.2.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -3910,11 +4244,11 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte-check@4.1.5(svelte@5.25.8)(typescript@5.9.3): + svelte-check@4.1.5(picomatch@4.0.3)(svelte@5.25.8)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.25 chokidar: 4.0.3 - fdir: 6.4.3 + fdir: 6.4.3(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 svelte: 5.25.8 @@ -3956,6 +4290,11 @@ snapshots: text-table@0.2.0: {} + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -3973,6 +4312,9 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 + tslib@2.8.1: + optional: true + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -4031,25 +4373,52 @@ snapshots: has-symbols: 1.1.0 which-boxed-primitive: 1.1.1 + undici-types@6.21.0: {} + + unrs-resolver@1.11.1: + dependencies: + napi-postinstall: 0.3.4 + optionalDependencies: + '@unrs/resolver-binding-android-arm-eabi': 1.11.1 + '@unrs/resolver-binding-android-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-arm64': 1.11.1 + '@unrs/resolver-binding-darwin-x64': 1.11.1 + '@unrs/resolver-binding-freebsd-x64': 1.11.1 + '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1 + '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-arm64-musl': 1.11.1 + '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1 + '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-gnu': 1.11.1 + '@unrs/resolver-binding-linux-x64-musl': 1.11.1 + '@unrs/resolver-binding-wasm32-wasi': 1.11.1 + '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1 + '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1 + '@unrs/resolver-binding-win32-x64-msvc': 1.11.1 + uri-js@4.4.1: dependencies: punycode: 2.3.1 util-deprecate@1.0.2: {} - vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2): + vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2): dependencies: esbuild: 0.25.2 postcss: 8.5.3 rollup: 4.39.0 optionalDependencies: + '@types/node': 22.19.11 fsevents: 2.3.3 jiti: 2.4.2 lightningcss: 1.29.2 - vitefu@1.0.6(vite@6.2.5(jiti@2.4.2)(lightningcss@1.29.2)): + vitefu@1.0.6(vite@6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2)): optionalDependencies: - vite: 6.2.5(jiti@2.4.2)(lightningcss@1.29.2) + vite: 6.2.5(@types/node@22.19.11)(jiti@2.4.2)(lightningcss@1.29.2) which-boxed-primitive@1.1.1: dependencies: diff --git a/scripts/generate-vendor-icon-manifest.mjs b/scripts/generate-vendor-icon-manifest.mjs new file mode 100644 index 0000000..4f31db9 --- /dev/null +++ b/scripts/generate-vendor-icon-manifest.mjs @@ -0,0 +1,192 @@ +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const currentFilePath = fileURLToPath(import.meta.url); +const currentDirPath = path.dirname(currentFilePath); +const projectRoot = path.resolve(currentDirPath, '..'); + +const vendorRoot = path.join(projectRoot, 'static/icons/vendor/homarr'); +const outputPath = path.join(projectRoot, 'src/lib/config/vendorIconManifest.ts'); +const prettierIgnorePath = path.join(projectRoot, '.prettierignore'); + +const allowedExtensions = new Set(['.svg', '.png', '.webp']); +const extensionPriority = { + '.svg': 0, + '.png': 1, + '.webp': 2 +}; + +/** @param {string} input */ +function toPosixPath(input) { + return input.split(path.sep).join('/'); +} + +/** @param {string} filename */ +function toSlug(filename) { + return filename + .toLowerCase() + .replace(/[^a-z0-9._-]/g, '-') + .replace(/[._]+/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + +/** @param {string} slug */ +function toLabel(slug) { + return slug + .split('-') + .filter(Boolean) + .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) + .join(' '); +} + +/** @param {string} dir */ +async function walk(dir) { + const entries = await fs.readdir(dir, { withFileTypes: true }); + const files = entries + .filter((entry) => entry.isFile()) + .map((entry) => path.join(dir, entry.name)); + const directories = entries + .filter((entry) => entry.isDirectory()) + .map((entry) => path.join(dir, entry.name)); + const nestedFileLists = await Promise.all(directories.map((childDirectory) => walk(childDirectory))); + + for (const nestedFiles of nestedFileLists) { + files.push(...nestedFiles); + } + return files; +} + +function compareIconRecords(a, b) { + const labelCompare = a.label.localeCompare(b.label); + if (labelCompare !== 0) { + return labelCompare; + } + return a.key.localeCompare(b.key); +} + +/** @param {string} filePath @param {string} entry */ +async function ensureIgnoreEntry(filePath, entry) { + const normalizedEntry = toPosixPath(entry).replace(/^\.?\//, ''); + let contents = ''; + try { + contents = await fs.readFile(filePath, 'utf8'); + } catch (error) { + if (!(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT') { + throw error; + } + } + + const eol = contents.includes('\r\n') ? '\r\n' : '\n'; + const existingEntries = contents + .split(/\r?\n/) + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => toPosixPath(line).replace(/^\.?\//, '')); + if (existingEntries.includes(normalizedEntry)) { + return; + } + + const suffix = contents.length > 0 && !contents.endsWith('\n') && !contents.endsWith('\r\n') ? eol : ''; + const nextContents = `${contents}${suffix}${normalizedEntry}${eol}`; + await fs.writeFile(filePath, nextContents, 'utf8'); +} + +async function main() { + const vendorExists = await fs + .access(vendorRoot) + .then(() => true) + .catch(() => false); + + if (!vendorExists) { + throw new Error(`Vendor icon directory not found: ${vendorRoot}`); + } + + const allFiles = await walk(vendorRoot); + const dedupedBySlug = new Map(); + + for (const absoluteFilePath of allFiles) { + const extension = path.extname(absoluteFilePath).toLowerCase(); + if (!allowedExtensions.has(extension)) { + continue; + } + + const relativePath = toPosixPath(path.relative(vendorRoot, absoluteFilePath)); + const basename = path.basename(relativePath, extension); + const slug = toSlug(basename); + if (!slug) { + continue; + } + + const key = `homarr:${slug}`; + const candidate = { + key, + label: toLabel(slug), + path: `/icons/vendor/homarr/${relativePath}`, + source: 'homarr-dashboard-icons', + extension, + priority: extensionPriority[extension] ?? 99, + relativePath + }; + + const existing = dedupedBySlug.get(slug); + if (!existing) { + dedupedBySlug.set(slug, candidate); + continue; + } + + if (candidate.priority < existing.priority) { + dedupedBySlug.set(slug, candidate); + continue; + } + + if (candidate.priority === existing.priority && candidate.relativePath < existing.relativePath) { + dedupedBySlug.set(slug, candidate); + } + } + + const records = Array.from(dedupedBySlug.values()) + .sort(compareIconRecords) + .map(({ key, label, path: iconPath, source }) => ({ key, label, path: iconPath, source })); + + const header = [ + '/* eslint-disable */', + '// prettier-ignore', + '// Auto-generated by scripts/generate-vendor-icon-manifest.mjs', + '// Do not edit manually.', + '', + 'export interface VendorIconDefinition {', + ' key: string;', + ' label: string;', + ' path: string;', + " source: 'homarr-dashboard-icons';", + '}', + '', + 'export const VENDOR_ICON_DEFINITIONS: VendorIconDefinition[] = [' + ].join('\n'); + + const body = records + .map((record) => { + const key = JSON.stringify(record.key); + const label = JSON.stringify(record.label); + const iconPath = JSON.stringify(record.path); + return ` { key: ${key}, label: ${label}, path: ${iconPath}, source: 'homarr-dashboard-icons' },`; + }) + .join('\n'); + + const footer = ['];', ''].join('\n'); + const output = `${header}\n${body}\n${footer}`; + const outputIgnoreEntry = toPosixPath(path.relative(projectRoot, outputPath)); + + await fs.mkdir(path.dirname(outputPath), { recursive: true }); + await ensureIgnoreEntry(prettierIgnorePath, outputIgnoreEntry); + await fs.writeFile(outputPath, output, 'utf8'); + +} + +main().catch((error) => { + const message = error instanceof Error ? error.stack ?? error.message : String(error); + process.stderr.write(`${message}\n`); + process.exitCode = 1; +}); diff --git a/scripts/vendor-icons/README.md b/scripts/vendor-icons/README.md new file mode 100644 index 0000000..e5ba425 --- /dev/null +++ b/scripts/vendor-icons/README.md @@ -0,0 +1,17 @@ +# Vendor Icon Refresh + +This project vendors icon assets from `homarr-labs/dashboard-icons` into: + +- `static/icons/vendor/homarr/` + +After refreshing the vendored files, regenerate the searchable manifest: + +```bash +pnpm run icons:manifest +``` + +Then verify: + +- `src/lib/config/vendorIconManifest.ts` changed as expected +- `third_party/homarr-dashboard-icons/SOURCE.txt` has the new upstream commit/date +- `third_party/homarr-dashboard-icons/LICENSE` still matches upstream diff --git a/server.mjs b/server.mjs new file mode 100644 index 0000000..512b4fe --- /dev/null +++ b/server.mjs @@ -0,0 +1,223 @@ +import { readFile, stat } from 'node:fs/promises'; +import { createServer } from 'node:http'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { validateNetworkData } from './src/lib/shared/networkSchemaCore.mjs'; +import { + checkWritableState, + isWriteEnabled, + readNetworkFile, + writeNetworkFile +} from './src/lib/shared/networkPersistenceCore.mjs'; + +const currentFilePath = fileURLToPath(import.meta.url); +const currentDirPath = path.dirname(currentFilePath); +const buildDir = path.resolve(currentDirPath, 'build'); + +const HOST = process.env.HOST ?? '0.0.0.0'; +const PORT = Number(process.env.PORT ?? 3000); +const MAX_BODY_BYTES = 1024 * 1024; + +const contentTypeByExtension = { + '.html': 'text/html; charset=utf-8', + '.js': 'text/javascript; charset=utf-8', + '.css': 'text/css; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.png': 'image/png', + '.jpg': 'image/jpeg', + '.jpeg': 'image/jpeg', + '.svg': 'image/svg+xml; charset=utf-8', + '.ico': 'image/x-icon', + '.webp': 'image/webp', + '.txt': 'text/plain; charset=utf-8' +}; + +function sendJson(response, status, payload, extraHeaders = {}) { + const body = JSON.stringify(payload); + response.writeHead(status, { + 'Content-Type': 'application/json; charset=utf-8', + 'Content-Length': Buffer.byteLength(body), + 'Cache-Control': 'no-store', + ...extraHeaders + }); + response.end(body); +} + +function badRequest(response, message, details) { + sendJson(response, 400, { + error: message, + ...(details ? { details } : {}) + }); +} + +async function serveApi(request, response) { + if (request.method === 'GET') { + try { + const payload = await readNetworkFile(); + const validation = validateNetworkData(payload.data); + if (!validation.valid) { + sendJson(response, 500, { + error: 'Stored network data is invalid', + details: validation.errors + }); + return; + } + + const canWrite = await checkWritableState(); + + sendJson(response, 200, { + data: validation.data, + writable: canWrite, + source: payload.source, + updatedAt: payload.updatedAt + }); + } catch (error) { + sendJson(response, 500, { + error: error instanceof Error ? error.message : 'Failed to read network data' + }); + } + return; + } + + if (request.method === 'PUT') { + if (!isWriteEnabled()) { + sendJson(response, 403, { + error: 'Write API disabled. Unset NETWORK_READ_ONLY to enable persistence.' + }); + return; + } + + let raw = ''; + let accumulatedSize = 0; + for await (const chunk of request) { + const chunkSize = Buffer.isBuffer(chunk) ? chunk.length : Buffer.byteLength(chunk); + accumulatedSize += chunkSize; + if (accumulatedSize > MAX_BODY_BYTES) { + raw = ''; + accumulatedSize = 0; + sendJson( + response, + 413, + { error: `Payload too large. Max body size is ${MAX_BODY_BYTES} bytes.` }, + { Connection: 'close' } + ); + request.destroy(); + return; + } + raw += typeof chunk === 'string' ? chunk : chunk.toString('utf8'); + } + + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + badRequest(response, 'Request body must be valid JSON'); + return; + } + + const validation = validateNetworkData(parsed); + if (!validation.valid) { + badRequest(response, 'Network data validation failed', validation.errors); + return; + } + + try { + const metadata = await writeNetworkFile(validation.data); + sendJson(response, 200, { + data: validation.data, + writable: true, + source: metadata.source, + updatedAt: metadata.updatedAt + }); + } catch (error) { + sendJson(response, 500, { + error: error instanceof Error ? error.message : 'Failed to persist network data' + }); + } + return; + } + + sendJson(response, 405, { error: 'Method not allowed' }); +} + +function safeRelativePath(requestPath) { + const decoded = decodeURIComponent(requestPath); + const normalized = path.posix.normalize(decoded); + if (normalized.startsWith('..')) { + return null; + } + return normalized; +} + +async function serveStatic(requestPath, response) { + const relativePath = safeRelativePath(requestPath); + if (relativePath === null) { + response.writeHead(400); + response.end('Bad request'); + return; + } + + const requestedFilePath = path.join( + buildDir, + relativePath === '/' ? 'index.html' : relativePath.slice(1) + ); + + const sendFile = async (filePath) => { + const ext = path.extname(filePath).toLowerCase(); + const contentType = contentTypeByExtension[ext] ?? 'application/octet-stream'; + const content = await readFile(filePath); + response.writeHead(200, { + 'Content-Type': contentType, + 'Cache-Control': ext === '.html' ? 'no-store' : 'public, max-age=31536000, immutable' + }); + response.end(content); + }; + + try { + const fileInfo = await stat(requestedFilePath); + if (fileInfo.isDirectory()) { + await sendFile(path.join(requestedFilePath, 'index.html')); + return; + } + await sendFile(requestedFilePath); + return; + } catch { + // continue with SPA fallback + } + + try { + await sendFile(path.join(buildDir, 'index.html')); + } catch { + response.writeHead(404); + response.end('Not found'); + } +} + +const server = createServer(async (request, response) => { + try { + const requestUrl = new URL(request.url ?? '/', `http://${request.headers.host ?? 'localhost'}`); + if (requestUrl.pathname === '/api/network-data') { + await serveApi(request, response); + return; + } + await serveStatic(requestUrl.pathname, response); + } catch (error) { + // eslint-disable-next-line no-console + console.error('[OpenNetworkDiagram] request handling failed', error); + if (response.headersSent || response.writableEnded) { + response.destroy(); + return; + } + const body = JSON.stringify({ error: 'Internal server error' }); + response.statusCode = 500; + response.setHeader('Content-Type', 'application/json; charset=utf-8'); + response.setHeader('Content-Length', Buffer.byteLength(body)); + response.setHeader('Cache-Control', 'no-store'); + response.end(body); + } +}); + +server.listen(PORT, HOST, () => { + // eslint-disable-next-line no-console + console.log(`[OpenNetworkDiagram] listening on http://${HOST}:${PORT}`); +}); diff --git a/src/app.css b/src/app.css index 1c4d2a8..a6f2d93 100644 --- a/src/app.css +++ b/src/app.css @@ -1,2 +1,30 @@ @import 'tailwindcss'; @plugin '@tailwindcss/typography'; + +:root { + --app-bg: #e2e8f0; + --panel-bg: #f8fafc; + --panel-border: #cbd5e1; + --panel-contrast: #0f172a; + --muted-text: #334155; + --chip-bg: #e2e8f0; + --chip-text: #0f172a; + --modal-overlay: rgba(15, 23, 42, 0.58); +} + +:root[data-theme='dark'] { + --app-bg: #0b1220; + --panel-bg: #111827; + --panel-border: #334155; + --panel-contrast: #e2e8f0; + --muted-text: #94a3b8; + --chip-bg: #1e293b; + --chip-text: #e2e8f0; + --modal-overlay: rgba(2, 6, 23, 0.78); +} + +html, +body { + background: var(--app-bg); + color: var(--panel-contrast); +} diff --git a/src/lib/components/Modal.svelte b/src/lib/components/Modal.svelte index 187178f..fc9ce35 100644 --- a/src/lib/components/Modal.svelte +++ b/src/lib/components/Modal.svelte @@ -52,7 +52,7 @@ align-items: center; justify-content: center; padding: 1rem; - background: rgba(15, 23, 42, 0.58); + background: var(--modal-overlay); z-index: 1000; } @@ -61,8 +61,8 @@ max-height: 90vh; display: flex; flex-direction: column; - background: #f8fafc; - border: 1px solid #cbd5e1; + background: var(--panel-bg); + border: 1px solid var(--panel-border); border-radius: 12px; box-shadow: 0 24px 48px rgba(15, 23, 42, 0.24); } @@ -73,19 +73,19 @@ justify-content: space-between; gap: 0.5rem; padding: 0.9rem 1rem; - border-bottom: 1px solid #e2e8f0; + border-bottom: 1px solid var(--panel-border); } .modal-header h2 { margin: 0; font-size: 1.05rem; - color: #0f172a; + color: var(--panel-contrast); } .close-button { border: 1px solid transparent; background: transparent; - color: #475569; + color: var(--muted-text); font-size: 1.7rem; line-height: 1; width: 2rem; @@ -95,8 +95,8 @@ } .close-button:hover { - background: #e2e8f0; - color: #0f172a; + background: var(--chip-bg); + color: var(--panel-contrast); } .modal-body { diff --git a/src/lib/components/NetworkDiagram.svelte b/src/lib/components/NetworkDiagram.svelte index 82bbd40..6c863b3 100644 --- a/src/lib/components/NetworkDiagram.svelte +++ b/src/lib/components/NetworkDiagram.svelte @@ -1,44 +1,101 @@ -
-
-
+
+
+
+ +
+ {#if mapControlsCollapsed} + + {:else} +
+ + + + + + + + + +
+ {/if} +
-
- - - - - - -
Source: {dataSourceLabel}
+
+ {saveStateLabel()} +
+ + {#if hasLoadedInitialData && !writable} +
{readOnlyNotice}
+ {/if} + {#if isLoadingData}
Loading JSON...
{/if} + {#if loadError} -
{loadError}
+
{loadError}
+ {/if} + {#if saveError} +
{saveError}
{/if} {#if tooltip.visible} @@ -1200,93 +1695,725 @@ {/if} + + {#if validationIssues.length > 0} +
+ Validation issues ({validationIssues.length}) +
    + {#each validationIssues as issue (`${issue.path}:${issue.message}`)} +
  • {issue.path}: {issue.message}
  • + {/each} +
+
+ {/if}
- {#if selectedDetails?.type === 'machine'} -