Merge pull request #8 from jcreek/5-add-ui-to-edit-config
5 add UI to edit config
@@ -20,6 +20,7 @@ Thumbs.db
|
||||
|
||||
# User-provided Docker data
|
||||
data/network.json
|
||||
data/.backups
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
|
||||
@@ -4,3 +4,4 @@ pnpm-lock.yaml
|
||||
yarn.lock
|
||||
bun.lock
|
||||
bun.lockb
|
||||
src/lib/config/vendorIconManifest.ts
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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
|
||||
@@ -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}`);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { VENDOR_ICON_DEFINITIONS } from './vendorIconManifest';
|
||||
|
||||
export interface IconDefinition {
|
||||
key: string;
|
||||
label: string;
|
||||
path: string;
|
||||
source?: 'built-in' | 'homarr-dashboard-icons';
|
||||
}
|
||||
|
||||
const BUILTIN_ICONS: IconDefinition[] = [
|
||||
{ key: 'server', label: 'Server', path: '/icons/server.svg', source: 'built-in' },
|
||||
{ key: 'router', label: 'Router', path: '/icons/router.svg', source: 'built-in' },
|
||||
{ key: 'switch', label: 'Switch', path: '/icons/switch.svg', source: 'built-in' },
|
||||
{ key: 'storage', label: 'Storage', path: '/icons/storage.svg', source: 'built-in' },
|
||||
{ key: 'desktop', label: 'Desktop', path: '/icons/desktop.svg', source: 'built-in' },
|
||||
{ key: 'cloud', label: 'Cloud', path: '/icons/cloud.svg', source: 'built-in' }
|
||||
];
|
||||
|
||||
const ICONS: IconDefinition[] = [...BUILTIN_ICONS, ...VENDOR_ICON_DEFINITIONS];
|
||||
|
||||
const ICON_BY_KEY = new Map(ICONS.map((icon) => [icon.key, icon]));
|
||||
|
||||
export function listIconDefinitions(): IconDefinition[] {
|
||||
return ICONS;
|
||||
}
|
||||
|
||||
export function resolveIconPath(iconKey?: string): string | undefined {
|
||||
if (!iconKey) {
|
||||
return undefined;
|
||||
}
|
||||
return ICON_BY_KEY.get(iconKey)?.path;
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
import type { Machine, NetworkData, NetworkDevice, Port, VM } from '../types';
|
||||
|
||||
export type OwnerKind = 'machine' | 'device';
|
||||
|
||||
interface OwnerRef {
|
||||
kind: OwnerKind;
|
||||
name: string;
|
||||
ports: Port[];
|
||||
}
|
||||
|
||||
function deepClone<T>(value: T): T {
|
||||
if (typeof structuredClone === 'function') {
|
||||
return structuredClone(value);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function equalsIgnoreCase(a: string, b: string): boolean {
|
||||
return a.localeCompare(b, undefined, { sensitivity: 'accent' }) === 0;
|
||||
}
|
||||
|
||||
function normalizePorts(ports: Port[] | undefined): Port[] {
|
||||
return Array.isArray(ports) ? ports : [];
|
||||
}
|
||||
|
||||
function normalizeVms(vms: VM[] | undefined): VM[] {
|
||||
return Array.isArray(vms) ? vms : [];
|
||||
}
|
||||
|
||||
function allOwners(data: NetworkData): OwnerRef[] {
|
||||
const machineOwners = data.machines.map((machine) => {
|
||||
const ports = normalizePorts(machine.ports);
|
||||
return {
|
||||
kind: 'machine' as const,
|
||||
name: machine.machineName,
|
||||
ports
|
||||
};
|
||||
});
|
||||
const deviceOwners = data.devices.map((device) => {
|
||||
const ports = normalizePorts(device.ports);
|
||||
return {
|
||||
kind: 'device' as const,
|
||||
name: device.name,
|
||||
ports
|
||||
};
|
||||
});
|
||||
return [...machineOwners, ...deviceOwners];
|
||||
}
|
||||
|
||||
export function cloneNetworkData(data: NetworkData): NetworkData {
|
||||
const cloned = deepClone(data);
|
||||
for (const machine of cloned.machines) {
|
||||
machine.ports = normalizePorts(machine.ports);
|
||||
machine.software = {
|
||||
vms: normalizeVms(machine.software?.vms)
|
||||
};
|
||||
}
|
||||
for (const device of cloned.devices) {
|
||||
device.ports = normalizePorts(device.ports);
|
||||
}
|
||||
return cloned;
|
||||
}
|
||||
|
||||
function findOwnerByKindAndName(data: NetworkData, kind: OwnerKind, name: string): OwnerRef | undefined {
|
||||
if (kind === 'machine') {
|
||||
const machine = data.machines.find((item) => equalsIgnoreCase(item.machineName, name));
|
||||
if (!machine) {
|
||||
return undefined;
|
||||
}
|
||||
machine.ports = normalizePorts(machine.ports);
|
||||
return {
|
||||
kind,
|
||||
name: machine.machineName,
|
||||
ports: machine.ports
|
||||
};
|
||||
}
|
||||
|
||||
const device = data.devices.find((item) => equalsIgnoreCase(item.name, name));
|
||||
if (!device) {
|
||||
return undefined;
|
||||
}
|
||||
device.ports = normalizePorts(device.ports);
|
||||
return {
|
||||
kind,
|
||||
name: device.name,
|
||||
ports: device.ports
|
||||
};
|
||||
}
|
||||
|
||||
function findOwnerByName(data: NetworkData, name: string): OwnerRef | undefined {
|
||||
const owners = allOwners(data);
|
||||
return owners.find((owner) => equalsIgnoreCase(owner.name, name));
|
||||
}
|
||||
|
||||
function findPort(owner: OwnerRef, portName: string): Port | undefined {
|
||||
return owner.ports.find((port) => equalsIgnoreCase(port.portName, portName));
|
||||
}
|
||||
|
||||
function ensureReciprocalConnections(data: NetworkData) {
|
||||
const owners = allOwners(data);
|
||||
|
||||
for (const owner of owners) {
|
||||
for (const port of owner.ports) {
|
||||
if (!port.connectedTo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetOwner = owners.find((candidate) =>
|
||||
equalsIgnoreCase(candidate.name, port.connectedTo!.device)
|
||||
);
|
||||
if (!targetOwner) {
|
||||
delete port.connectedTo;
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetPort = findPort(targetOwner, port.connectedTo.port);
|
||||
if (!targetPort) {
|
||||
delete port.connectedTo;
|
||||
continue;
|
||||
}
|
||||
|
||||
const expectedReciprocal = {
|
||||
device: owner.name,
|
||||
port: port.portName
|
||||
};
|
||||
if (!targetPort.connectedTo) {
|
||||
targetPort.connectedTo = expectedReciprocal;
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasMatchingReciprocal =
|
||||
equalsIgnoreCase(targetPort.connectedTo.device, owner.name) &&
|
||||
equalsIgnoreCase(targetPort.connectedTo.port, port.portName);
|
||||
if (hasMatchingReciprocal) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clearInboundReferences(data: NetworkData, predicate: (connection: Port['connectedTo']) => boolean) {
|
||||
for (const owner of allOwners(data)) {
|
||||
for (const port of owner.ports) {
|
||||
if (!port.connectedTo) {
|
||||
continue;
|
||||
}
|
||||
if (predicate(port.connectedTo)) {
|
||||
delete port.connectedTo;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function withReconciledConnections(data: NetworkData): NetworkData {
|
||||
const cloned = cloneNetworkData(data);
|
||||
ensureReciprocalConnections(cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function createEmptyMachine(): Machine {
|
||||
return {
|
||||
machineName: 'New Machine',
|
||||
ipAddress: '192.168.1.100',
|
||||
role: 'Role',
|
||||
operatingSystem: 'OS',
|
||||
software: {
|
||||
vms: []
|
||||
},
|
||||
hardware: {
|
||||
cpu: 'CPU',
|
||||
ram: '8GB',
|
||||
networkPorts: 1
|
||||
},
|
||||
ports: []
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyDevice(): NetworkDevice {
|
||||
return {
|
||||
name: 'New Device',
|
||||
ipAddress: '192.168.1.200',
|
||||
type: 'Device',
|
||||
notes: '',
|
||||
ports: []
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyVm(): VM {
|
||||
return {
|
||||
name: 'New VM',
|
||||
role: 'Role',
|
||||
ipAddress: '192.168.1.201'
|
||||
};
|
||||
}
|
||||
|
||||
export function createEmptyPort(prefix = 'eth'): Port {
|
||||
return {
|
||||
portName: `${prefix}0`,
|
||||
speedGbps: 1
|
||||
};
|
||||
}
|
||||
|
||||
export function renameOwner(
|
||||
data: NetworkData,
|
||||
kind: OwnerKind,
|
||||
previousName: string,
|
||||
nextName: string
|
||||
): NetworkData {
|
||||
const cloned = cloneNetworkData(data);
|
||||
if (equalsIgnoreCase(previousName, nextName)) {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
if (kind === 'machine') {
|
||||
const machine = cloned.machines.find((item) => equalsIgnoreCase(item.machineName, previousName));
|
||||
if (machine) {
|
||||
machine.machineName = nextName;
|
||||
}
|
||||
} else {
|
||||
const device = cloned.devices.find((item) => equalsIgnoreCase(item.name, previousName));
|
||||
if (device) {
|
||||
device.name = nextName;
|
||||
}
|
||||
}
|
||||
|
||||
for (const owner of allOwners(cloned)) {
|
||||
for (const port of owner.ports) {
|
||||
if (port.connectedTo && equalsIgnoreCase(port.connectedTo.device, previousName)) {
|
||||
port.connectedTo.device = nextName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureReciprocalConnections(cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function renamePort(
|
||||
data: NetworkData,
|
||||
kind: OwnerKind,
|
||||
ownerName: string,
|
||||
previousPortName: string,
|
||||
nextPortName: string
|
||||
): NetworkData {
|
||||
const cloned = cloneNetworkData(data);
|
||||
if (equalsIgnoreCase(previousPortName, nextPortName)) {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
const owner = findOwnerByKindAndName(cloned, kind, ownerName);
|
||||
if (!owner) {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
const port = findPort(owner, previousPortName);
|
||||
if (!port) {
|
||||
return cloned;
|
||||
}
|
||||
port.portName = nextPortName;
|
||||
|
||||
for (const candidateOwner of allOwners(cloned)) {
|
||||
for (const candidatePort of candidateOwner.ports) {
|
||||
if (
|
||||
candidatePort.connectedTo &&
|
||||
equalsIgnoreCase(candidatePort.connectedTo.device, owner.name) &&
|
||||
equalsIgnoreCase(candidatePort.connectedTo.port, previousPortName)
|
||||
) {
|
||||
candidatePort.connectedTo.port = nextPortName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ensureReciprocalConnections(cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function deleteOwner(data: NetworkData, kind: OwnerKind, ownerName: string): NetworkData {
|
||||
const cloned = cloneNetworkData(data);
|
||||
|
||||
if (kind === 'machine') {
|
||||
cloned.machines = cloned.machines.filter((machine) => !equalsIgnoreCase(machine.machineName, ownerName));
|
||||
} else {
|
||||
cloned.devices = cloned.devices.filter((device) => !equalsIgnoreCase(device.name, ownerName));
|
||||
}
|
||||
|
||||
clearInboundReferences(
|
||||
cloned,
|
||||
(connection) => Boolean(connection && equalsIgnoreCase(connection.device, ownerName))
|
||||
);
|
||||
ensureReciprocalConnections(cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function deletePort(
|
||||
data: NetworkData,
|
||||
kind: OwnerKind,
|
||||
ownerName: string,
|
||||
portName: string
|
||||
): NetworkData {
|
||||
const cloned = cloneNetworkData(data);
|
||||
const owner = findOwnerByKindAndName(cloned, kind, ownerName);
|
||||
if (!owner) {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
owner.ports = owner.ports.filter((port) => !equalsIgnoreCase(port.portName, portName));
|
||||
|
||||
if (kind === 'machine') {
|
||||
const machine = cloned.machines.find((item) => equalsIgnoreCase(item.machineName, ownerName));
|
||||
if (machine) {
|
||||
machine.ports = owner.ports;
|
||||
}
|
||||
} else {
|
||||
const device = cloned.devices.find((item) => equalsIgnoreCase(item.name, ownerName));
|
||||
if (device) {
|
||||
device.ports = owner.ports;
|
||||
}
|
||||
}
|
||||
|
||||
clearInboundReferences(
|
||||
cloned,
|
||||
(connection) =>
|
||||
Boolean(
|
||||
connection &&
|
||||
equalsIgnoreCase(connection.device, ownerName) &&
|
||||
equalsIgnoreCase(connection.port, portName)
|
||||
)
|
||||
);
|
||||
ensureReciprocalConnections(cloned);
|
||||
return cloned;
|
||||
}
|
||||
|
||||
export function setPortConnection(
|
||||
data: NetworkData,
|
||||
kind: OwnerKind,
|
||||
ownerName: string,
|
||||
portName: string,
|
||||
target: Port['connectedTo'] | undefined
|
||||
): NetworkData {
|
||||
const cloned = cloneNetworkData(data);
|
||||
const owner = findOwnerByKindAndName(cloned, kind, ownerName);
|
||||
if (!owner) {
|
||||
return cloned;
|
||||
}
|
||||
const port = findPort(owner, portName);
|
||||
if (!port) {
|
||||
return cloned;
|
||||
}
|
||||
|
||||
const previousConnection = port.connectedTo;
|
||||
if (previousConnection) {
|
||||
const previousOwner = findOwnerByName(cloned, previousConnection.device);
|
||||
const previousPort = previousOwner ? findPort(previousOwner, previousConnection.port) : undefined;
|
||||
if (previousPort) {
|
||||
delete previousPort.connectedTo;
|
||||
}
|
||||
}
|
||||
|
||||
if (!target?.device || !target.port) {
|
||||
delete port.connectedTo;
|
||||
return cloned;
|
||||
}
|
||||
|
||||
port.connectedTo = {
|
||||
device: target.device,
|
||||
port: target.port
|
||||
};
|
||||
ensureReciprocalConnections(cloned);
|
||||
return cloned;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
normalizeNetworkData as normalizeNetworkDataCore,
|
||||
validateNetworkData as validateNetworkDataCore
|
||||
} from '../shared/networkSchemaCore.mjs';
|
||||
import type { NetworkData } from '../types';
|
||||
|
||||
export interface ValidationIssue {
|
||||
path: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: ValidationIssue[];
|
||||
data?: NetworkData;
|
||||
}
|
||||
|
||||
export function validateNetworkData(value: unknown): ValidationResult {
|
||||
return validateNetworkDataCore(value) as ValidationResult;
|
||||
}
|
||||
|
||||
export function normalizeNetworkData(data: NetworkData): NetworkData {
|
||||
return normalizeNetworkDataCore(data) as NetworkData;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ConnectedPortRef, NetworkData, Port } from '../types';
|
||||
import { resolveIconPath } from '../config/iconRegistry';
|
||||
import type { GraphEdgeElement, GraphNodeElement, GraphTransformResult } from './types';
|
||||
|
||||
interface EndpointOwner {
|
||||
@@ -8,8 +9,12 @@ interface EndpointOwner {
|
||||
ports: Map<string, Port>;
|
||||
}
|
||||
|
||||
function toOwnerKey(kind: EndpointOwner['kind'], name: string): string {
|
||||
return `${kind}:${name}`;
|
||||
function normalizeOwnerName(name: string): string {
|
||||
return name.trim().toLowerCase();
|
||||
}
|
||||
|
||||
function toOwnerLookupKey(kind: EndpointOwner['kind'], name: string): string {
|
||||
return `${kind}:${normalizeOwnerName(name)}`;
|
||||
}
|
||||
|
||||
function toMachineNodeId(name: string): string {
|
||||
@@ -75,7 +80,7 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
||||
vmCount: number;
|
||||
}
|
||||
> = {};
|
||||
const ownersByKey = new Map<string, EndpointOwner>();
|
||||
const ownersByLookup = new Map<string, EndpointOwner>();
|
||||
const seenWarnings = new Set<string>();
|
||||
|
||||
const warn = (message: string) => {
|
||||
@@ -86,14 +91,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
||||
};
|
||||
|
||||
const addOwner = (owner: EndpointOwner) => {
|
||||
const ownerKey = toOwnerKey(owner.kind, owner.name);
|
||||
if (ownersByKey.has(ownerKey)) {
|
||||
const ownerLookupKey = toOwnerLookupKey(owner.kind, owner.name);
|
||||
if (ownersByLookup.has(ownerLookupKey)) {
|
||||
warn(
|
||||
`Duplicate ${owner.kind} name "${owner.name}" found. First occurrence will be used for link resolution.`
|
||||
);
|
||||
return;
|
||||
}
|
||||
ownersByKey.set(ownerKey, owner);
|
||||
ownersByLookup.set(ownerLookupKey, owner);
|
||||
};
|
||||
|
||||
let hostingEdgeCounter = 0;
|
||||
@@ -110,24 +115,27 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
||||
|
||||
nodes.push({
|
||||
data: {
|
||||
id: machineNodeId,
|
||||
label: machine.machineName,
|
||||
rawName: machine.machineName,
|
||||
kind: 'machine',
|
||||
id: machineNodeId,
|
||||
label: machine.machineName,
|
||||
rawName: machine.machineName,
|
||||
kind: 'machine',
|
||||
iconKey: machine.iconKey,
|
||||
iconUrl: resolveIconPath(machine.iconKey),
|
||||
nodeWidth: 200,
|
||||
nodeHeight: 110,
|
||||
details: {
|
||||
type: 'machine',
|
||||
name: machine.machineName,
|
||||
ip: machine.ipAddress,
|
||||
role: machine.role,
|
||||
os: machine.operatingSystem,
|
||||
cpu: machine.hardware?.cpu ?? 'Unknown',
|
||||
ram: machine.hardware?.ram ?? 'Unknown',
|
||||
gpu: machine.hardware?.gpu ?? undefined,
|
||||
ports: machinePortsList,
|
||||
vmCount: machineVms.length,
|
||||
vms: machineVms
|
||||
name: machine.machineName,
|
||||
iconKey: machine.iconKey,
|
||||
ip: machine.ipAddress,
|
||||
role: machine.role,
|
||||
os: machine.operatingSystem,
|
||||
cpu: machine.hardware?.cpu ?? 'Unknown',
|
||||
ram: machine.hardware?.ram ?? 'Unknown',
|
||||
gpu: machine.hardware?.gpu ?? undefined,
|
||||
ports: machinePortsList,
|
||||
vmCount: machineVms.length,
|
||||
vms: machineVms
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -148,11 +156,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
||||
rawName: vm.name,
|
||||
kind: 'vm',
|
||||
hostMachineId: machineNodeId,
|
||||
iconKey: vm.iconKey,
|
||||
iconUrl: resolveIconPath(vm.iconKey),
|
||||
nodeWidth: 140,
|
||||
nodeHeight: 66,
|
||||
details: {
|
||||
type: 'vm',
|
||||
name: vm.name,
|
||||
iconKey: vm.iconKey,
|
||||
ip: vm.ipAddress,
|
||||
role: vm.role,
|
||||
hostName: machine.machineName
|
||||
@@ -199,11 +210,14 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
||||
label: device.name,
|
||||
rawName: device.name,
|
||||
kind: 'device',
|
||||
iconKey: device.iconKey,
|
||||
iconUrl: resolveIconPath(device.iconKey),
|
||||
nodeWidth: 176,
|
||||
nodeHeight: 116,
|
||||
details: {
|
||||
type: 'device',
|
||||
name: device.name,
|
||||
iconKey: device.iconKey,
|
||||
ip: device.ipAddress,
|
||||
deviceType: device.type,
|
||||
notes: device.notes,
|
||||
@@ -228,15 +242,17 @@ export default function transformNetworkDataToGraph(data: NetworkData): GraphTra
|
||||
const seenPhysicalConnections = new Set<string>();
|
||||
let physicalEdgeCounter = 0;
|
||||
|
||||
for (const owner of ownersByKey.values()) {
|
||||
for (const owner of ownersByLookup.values()) {
|
||||
for (const port of owner.ports.values()) {
|
||||
const connectedTo = parseConnectedTo(port.connectedTo);
|
||||
if (!connectedTo) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const machineTargetOwner = ownersByKey.get(toOwnerKey('machine', connectedTo.device));
|
||||
const deviceTargetOwner = ownersByKey.get(toOwnerKey('device', connectedTo.device));
|
||||
const machineTargetOwner = ownersByLookup.get(
|
||||
toOwnerLookupKey('machine', connectedTo.device)
|
||||
);
|
||||
const deviceTargetOwner = ownersByLookup.get(toOwnerLookupKey('device', connectedTo.device));
|
||||
if (machineTargetOwner && deviceTargetOwner) {
|
||||
warn(
|
||||
`Link from "${owner.name}:${port.portName}" references ambiguous device "${connectedTo.device}" (matches both machine and device names).`
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface PortDetails {
|
||||
export interface MachineDetails {
|
||||
type: 'machine';
|
||||
name: string;
|
||||
iconKey?: string;
|
||||
ip: string;
|
||||
role: string;
|
||||
os: string;
|
||||
@@ -31,6 +32,7 @@ export interface MachineDetails {
|
||||
export interface DeviceDetails {
|
||||
type: 'device';
|
||||
name: string;
|
||||
iconKey?: string;
|
||||
ip: string;
|
||||
deviceType: string;
|
||||
notes?: string;
|
||||
@@ -40,6 +42,7 @@ export interface DeviceDetails {
|
||||
export interface VmDetails {
|
||||
type: 'vm';
|
||||
name: string;
|
||||
iconKey?: string;
|
||||
ip: string;
|
||||
role: string;
|
||||
hostName: string;
|
||||
@@ -55,6 +58,8 @@ export interface GraphNodeData {
|
||||
hostMachineId?: string;
|
||||
nodeWidth?: number;
|
||||
nodeHeight?: number;
|
||||
iconUrl?: string;
|
||||
iconKey?: string;
|
||||
details?: GraphNodeDetails;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import {
|
||||
checkWritableState as checkWritableStateCore,
|
||||
isWriteEnabled as isWriteEnabledCore,
|
||||
readNetworkFile as readNetworkFileCore,
|
||||
writeNetworkFile as writeNetworkFileCore
|
||||
} from '../shared/networkPersistenceCore.mjs';
|
||||
|
||||
import type { NetworkData } from '../types';
|
||||
|
||||
export interface NetworkFileMetadata {
|
||||
source: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export function isWriteEnabled(): boolean {
|
||||
return isWriteEnabledCore();
|
||||
}
|
||||
|
||||
export async function readNetworkFile(): Promise<{ data: NetworkData } & NetworkFileMetadata> {
|
||||
return (await readNetworkFileCore()) as { data: NetworkData } & NetworkFileMetadata;
|
||||
}
|
||||
|
||||
export async function writeNetworkFile(data: NetworkData): Promise<NetworkFileMetadata> {
|
||||
return (await writeNetworkFileCore(data)) as NetworkFileMetadata;
|
||||
}
|
||||
|
||||
export async function checkWritableState(): Promise<boolean> {
|
||||
return checkWritableStateCore();
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
import { constants } from 'node:fs';
|
||||
import {
|
||||
access,
|
||||
copyFile,
|
||||
mkdir,
|
||||
open,
|
||||
readFile,
|
||||
readdir,
|
||||
rename,
|
||||
stat,
|
||||
unlink
|
||||
} from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
/**
|
||||
* @typedef {{ machines: unknown[]; devices: unknown[] }} NetworkData
|
||||
* @typedef {{ source: string; updatedAt: string }} NetworkFileMetadata
|
||||
* @typedef {{ data: NetworkData } & NetworkFileMetadata} NetworkFileReadResult
|
||||
*/
|
||||
|
||||
const NETWORK_READ_ONLY = process.env.NETWORK_READ_ONLY === 'true';
|
||||
const DATA_FILE_DEFAULT = path.resolve(process.cwd(), 'data/network.json');
|
||||
const BACKUP_DIR_DEFAULT = path.resolve(process.cwd(), 'data/.backups');
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveDataFilePath() {
|
||||
return path.resolve(process.env.NETWORK_DATA_FILE ?? DATA_FILE_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string}
|
||||
*/
|
||||
function resolveBackupDirectory() {
|
||||
return path.resolve(process.env.NETWORK_BACKUP_DIR ?? BACKUP_DIR_DEFAULT);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isWriteEnabled() {
|
||||
return !NETWORK_READ_ONLY;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dataFilePath
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
async function readUpdatedAt(dataFilePath) {
|
||||
const dataFileStats = await stat(dataFilePath);
|
||||
return dataFileStats.mtime.toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<NetworkFileReadResult>}
|
||||
*/
|
||||
export async function readNetworkFile() {
|
||||
const source = resolveDataFilePath();
|
||||
const raw = await readFile(source, 'utf8');
|
||||
const parsed = JSON.parse(raw);
|
||||
const updatedAt = await readUpdatedAt(source);
|
||||
return {
|
||||
data: parsed,
|
||||
source,
|
||||
updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} dataFilePath
|
||||
* @param {string} backupDirectory
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function createBackupIfPresent(dataFilePath, backupDirectory) {
|
||||
try {
|
||||
await access(dataFilePath, constants.F_OK);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
await mkdir(backupDirectory, { recursive: true });
|
||||
const base = path.basename(dataFilePath);
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const backupFilePath = path.join(backupDirectory, `${base}.${timestamp}.bak`);
|
||||
await copyFile(dataFilePath, backupFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} backupDirectory
|
||||
* @param {string} baseName
|
||||
* @param {number} [keep]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function trimBackups(backupDirectory, baseName, keep = 5) {
|
||||
/** @type {Array<{ name: string; time: number }>} */
|
||||
let entries = [];
|
||||
try {
|
||||
const files = await readdir(backupDirectory);
|
||||
entries = await Promise.all(
|
||||
files
|
||||
.filter((file) => file.startsWith(`${baseName}.`) && file.endsWith('.bak'))
|
||||
.map(async (file) => {
|
||||
const filePath = path.join(backupDirectory, file);
|
||||
const info = await stat(filePath);
|
||||
return { name: filePath, time: info.mtimeMs };
|
||||
})
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.sort((a, b) => b.time - a.time);
|
||||
await Promise.all(entries.slice(keep).map((entry) => unlink(entry.name).catch(() => undefined)));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NetworkData} data
|
||||
* @returns {Promise<NetworkFileMetadata>}
|
||||
*/
|
||||
export async function writeNetworkFile(data) {
|
||||
const source = resolveDataFilePath();
|
||||
const backupDirectory = resolveBackupDirectory();
|
||||
const directory = path.dirname(source);
|
||||
await mkdir(directory, { recursive: true });
|
||||
await createBackupIfPresent(source, backupDirectory);
|
||||
|
||||
const temporaryPath = `${source}.tmp-${Date.now()}-${process.pid}`;
|
||||
const jsonPayload = `${JSON.stringify(data, null, '\t')}\n`;
|
||||
const fileHandle = await open(temporaryPath, 'w');
|
||||
try {
|
||||
await fileHandle.writeFile(jsonPayload, 'utf8');
|
||||
await fileHandle.sync();
|
||||
} finally {
|
||||
await fileHandle.close();
|
||||
}
|
||||
try {
|
||||
await rename(temporaryPath, source);
|
||||
} catch (renameError) {
|
||||
await unlink(temporaryPath).catch(() => undefined);
|
||||
throw renameError;
|
||||
}
|
||||
|
||||
await trimBackups(backupDirectory, path.basename(source), 5);
|
||||
const updatedAt = await readUpdatedAt(source);
|
||||
|
||||
return {
|
||||
source,
|
||||
updatedAt
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
export async function checkWritableState() {
|
||||
if (!isWriteEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const source = resolveDataFilePath();
|
||||
const directory = path.dirname(source);
|
||||
try {
|
||||
await mkdir(directory, { recursive: true });
|
||||
await access(directory, constants.W_OK);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
/**
|
||||
* @typedef {{ path: string; message: string }} ValidationIssue
|
||||
* @typedef {{ device: string; port: string }} ConnectedTo
|
||||
* @typedef {{ portName: string; speedGbps?: number; connectedTo?: ConnectedTo }} Port
|
||||
* @typedef {{ name: string; role: string; ipAddress: string; iconKey?: string }} VM
|
||||
* @typedef {{ cpu: string; ram: string; networkPorts: number; networkPortSpeedGbps?: number; gpu?: string }} Hardware
|
||||
* @typedef {{ machineName: string; ipAddress: string; role: string; operatingSystem: string; iconKey?: string; software: { vms: VM[] }; hardware: Hardware; ports?: Port[] }} Machine
|
||||
* @typedef {{ name: string; ipAddress: string; type: string; iconKey?: string; notes?: string; ports?: Port[] }} NetworkDevice
|
||||
* @typedef {{ machines: Machine[]; devices: NetworkDevice[] }} NetworkData
|
||||
* @typedef {{ valid: boolean; errors: ValidationIssue[]; data?: NetworkData }} ValidationResult
|
||||
* @typedef {{ allowEmpty?: boolean; optional?: boolean }} ReadStringOptions
|
||||
* @typedef {{ optional?: boolean; min?: number }} ReadNumberOptions
|
||||
* @typedef {{ port: Port; originalIndex: number }} PortEntry
|
||||
* @typedef {{ vm: VM; originalIndex: number }} VmEntry
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} value
|
||||
* @returns {T}
|
||||
*/
|
||||
function deepClone(value) {
|
||||
if (typeof structuredClone === 'function') {
|
||||
return structuredClone(value);
|
||||
}
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {value is Record<string, unknown>}
|
||||
*/
|
||||
function isRecord(value) {
|
||||
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {string} path
|
||||
* @param {unknown} value
|
||||
* @param {ReadStringOptions} [options]
|
||||
* @returns {string | undefined}
|
||||
*/
|
||||
function readString(issues, path, value, options) {
|
||||
if (value === undefined || value === null) {
|
||||
if (options?.optional) {
|
||||
return undefined;
|
||||
}
|
||||
issues.push({ path, message: 'is required' });
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
issues.push({ path, message: 'must be a string' });
|
||||
return undefined;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!options?.allowEmpty && trimmed.length === 0) {
|
||||
issues.push({ path, message: 'cannot be empty' });
|
||||
return undefined;
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {string} path
|
||||
* @param {unknown} value
|
||||
* @param {ReadNumberOptions} [options]
|
||||
* @returns {number | undefined}
|
||||
*/
|
||||
function readNumber(issues, path, value, options) {
|
||||
if (value === undefined || value === null) {
|
||||
if (options?.optional) {
|
||||
return undefined;
|
||||
}
|
||||
issues.push({ path, message: 'is required' });
|
||||
return undefined;
|
||||
}
|
||||
if (typeof value !== 'number' || Number.isNaN(value)) {
|
||||
issues.push({ path, message: 'must be a number' });
|
||||
return undefined;
|
||||
}
|
||||
if (typeof options?.min === 'number' && value < options.min) {
|
||||
issues.push({ path, message: `must be >= ${options.min}` });
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {string} path
|
||||
* @param {unknown} value
|
||||
* @param {string} ownerLabel
|
||||
* @returns {Port | null}
|
||||
*/
|
||||
export function normalizePort(issues, path, value, ownerLabel) {
|
||||
if (!isRecord(value)) {
|
||||
issues.push({ path, message: `must be an object (${ownerLabel} port)` });
|
||||
return null;
|
||||
}
|
||||
|
||||
const portName = readString(issues, `${path}.portName`, value.portName);
|
||||
const speedGbps = readNumber(issues, `${path}.speedGbps`, value.speedGbps, {
|
||||
optional: true,
|
||||
min: 0
|
||||
});
|
||||
|
||||
let connectedTo;
|
||||
if (value.connectedTo !== undefined) {
|
||||
if (!isRecord(value.connectedTo)) {
|
||||
issues.push({ path: `${path}.connectedTo`, message: 'must be an object' });
|
||||
} else {
|
||||
const device = readString(issues, `${path}.connectedTo.device`, value.connectedTo.device);
|
||||
const port = readString(issues, `${path}.connectedTo.port`, value.connectedTo.port);
|
||||
if (device && port) {
|
||||
connectedTo = { device, port };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!portName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
portName,
|
||||
...(typeof speedGbps === 'number' ? { speedGbps } : {}),
|
||||
...(connectedTo ? { connectedTo } : {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {PortEntry[]} portEntries
|
||||
* @param {string} path
|
||||
* @param {string} ownerName
|
||||
* @returns {void}
|
||||
*/
|
||||
function validatePortUniqueness(issues, portEntries, path, ownerName) {
|
||||
const seen = new Set();
|
||||
for (const { port, originalIndex } of portEntries) {
|
||||
const key = port.portName.toLowerCase();
|
||||
if (seen.has(key)) {
|
||||
issues.push({
|
||||
path: `${path}[${originalIndex}].portName`,
|
||||
message: `duplicate port name "${port.portName}" for ${ownerName}`
|
||||
});
|
||||
}
|
||||
seen.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {string} path
|
||||
* @param {unknown} value
|
||||
* @returns {VM | null}
|
||||
*/
|
||||
export function normalizeVm(issues, path, value) {
|
||||
if (!isRecord(value)) {
|
||||
issues.push({ path, message: 'must be an object (vm)' });
|
||||
return null;
|
||||
}
|
||||
const name = readString(issues, `${path}.name`, value.name);
|
||||
const role = readString(issues, `${path}.role`, value.role);
|
||||
const ipAddress = readString(issues, `${path}.ipAddress`, value.ipAddress);
|
||||
const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true });
|
||||
|
||||
if (!name || !role || !ipAddress) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
role,
|
||||
ipAddress,
|
||||
...(iconKey ? { iconKey } : {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {string} path
|
||||
* @param {unknown} value
|
||||
* @returns {Machine | null}
|
||||
*/
|
||||
function normalizeMachine(issues, path, value) {
|
||||
if (!isRecord(value)) {
|
||||
issues.push({ path, message: 'must be an object (machine)' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const machineName = readString(issues, `${path}.machineName`, value.machineName);
|
||||
const ipAddress = readString(issues, `${path}.ipAddress`, value.ipAddress);
|
||||
const role = readString(issues, `${path}.role`, value.role);
|
||||
const operatingSystem = readString(issues, `${path}.operatingSystem`, value.operatingSystem);
|
||||
const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true });
|
||||
|
||||
const softwareRaw = value.software;
|
||||
if (!isRecord(softwareRaw)) {
|
||||
issues.push({ path: `${path}.software`, message: 'must be an object' });
|
||||
}
|
||||
const vmRaw = isRecord(softwareRaw) ? softwareRaw.vms : undefined;
|
||||
if (!Array.isArray(vmRaw)) {
|
||||
issues.push({ path: `${path}.software.vms`, message: 'must be an array' });
|
||||
}
|
||||
/** @type {VmEntry[]} */
|
||||
const vmEntries = [];
|
||||
const vms = [];
|
||||
for (const [vmIndex, vmValue] of (Array.isArray(vmRaw) ? vmRaw : []).entries()) {
|
||||
const vm = normalizeVm(issues, `${path}.software.vms[${vmIndex}]`, vmValue);
|
||||
if (vm) {
|
||||
vmEntries.push({ vm, originalIndex: vmIndex });
|
||||
vms.push(vm);
|
||||
}
|
||||
}
|
||||
|
||||
const vmSeen = new Set();
|
||||
for (const { vm, originalIndex } of vmEntries) {
|
||||
const vmKey = vm.name.toLowerCase();
|
||||
if (vmSeen.has(vmKey)) {
|
||||
issues.push({
|
||||
path: `${path}.software.vms[${originalIndex}].name`,
|
||||
message: `duplicate VM name "${vm.name}" for machine "${machineName ?? 'unknown'}"`
|
||||
});
|
||||
}
|
||||
vmSeen.add(vmKey);
|
||||
}
|
||||
|
||||
const hardwareRaw = value.hardware;
|
||||
if (!isRecord(hardwareRaw)) {
|
||||
issues.push({ path: `${path}.hardware`, message: 'must be an object' });
|
||||
}
|
||||
const hardware = {
|
||||
cpu: readString(issues, `${path}.hardware.cpu`, isRecord(hardwareRaw) ? hardwareRaw.cpu : undefined),
|
||||
ram: readString(issues, `${path}.hardware.ram`, isRecord(hardwareRaw) ? hardwareRaw.ram : undefined),
|
||||
networkPorts: readNumber(
|
||||
issues,
|
||||
`${path}.hardware.networkPorts`,
|
||||
isRecord(hardwareRaw) ? hardwareRaw.networkPorts : undefined,
|
||||
{ min: 0 }
|
||||
),
|
||||
networkPortSpeedGbps: readNumber(
|
||||
issues,
|
||||
`${path}.hardware.networkPortSpeedGbps`,
|
||||
isRecord(hardwareRaw) ? hardwareRaw.networkPortSpeedGbps : undefined,
|
||||
{ optional: true, min: 0 }
|
||||
),
|
||||
gpu: readString(issues, `${path}.hardware.gpu`, isRecord(hardwareRaw) ? hardwareRaw.gpu : undefined, {
|
||||
optional: true,
|
||||
allowEmpty: true
|
||||
})
|
||||
};
|
||||
|
||||
const portsRaw = value.ports;
|
||||
if (portsRaw !== undefined && !Array.isArray(portsRaw)) {
|
||||
issues.push({ path: `${path}.ports`, message: 'must be an array when provided' });
|
||||
}
|
||||
/** @type {PortEntry[]} */
|
||||
const portEntries = [];
|
||||
const ports = [];
|
||||
for (const [portIndex, portValue] of (Array.isArray(portsRaw) ? portsRaw : []).entries()) {
|
||||
const port = normalizePort(issues, `${path}.ports[${portIndex}]`, portValue, 'machine');
|
||||
if (port) {
|
||||
portEntries.push({ port, originalIndex: portIndex });
|
||||
ports.push(port);
|
||||
}
|
||||
}
|
||||
validatePortUniqueness(issues, portEntries, `${path}.ports`, machineName ?? 'machine');
|
||||
|
||||
if (
|
||||
!machineName ||
|
||||
!ipAddress ||
|
||||
!role ||
|
||||
!operatingSystem ||
|
||||
!hardware.cpu ||
|
||||
!hardware.ram ||
|
||||
typeof hardware.networkPorts !== 'number'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
machineName,
|
||||
ipAddress,
|
||||
role,
|
||||
operatingSystem,
|
||||
...(iconKey ? { iconKey } : {}),
|
||||
software: { vms },
|
||||
hardware: {
|
||||
cpu: hardware.cpu,
|
||||
ram: hardware.ram,
|
||||
networkPorts: hardware.networkPorts,
|
||||
...(typeof hardware.networkPortSpeedGbps === 'number'
|
||||
? { networkPortSpeedGbps: hardware.networkPortSpeedGbps }
|
||||
: {}),
|
||||
...(hardware.gpu ? { gpu: hardware.gpu } : {})
|
||||
},
|
||||
ports
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ValidationIssue[]} issues
|
||||
* @param {string} path
|
||||
* @param {unknown} value
|
||||
* @returns {NetworkDevice | null}
|
||||
*/
|
||||
function normalizeDevice(issues, path, value) {
|
||||
if (!isRecord(value)) {
|
||||
issues.push({ path, message: 'must be an object (device)' });
|
||||
return null;
|
||||
}
|
||||
|
||||
const name = readString(issues, `${path}.name`, value.name);
|
||||
const ipAddress = readString(issues, `${path}.ipAddress`, value.ipAddress);
|
||||
const type = readString(issues, `${path}.type`, value.type);
|
||||
const notes = readString(issues, `${path}.notes`, value.notes, { optional: true, allowEmpty: true });
|
||||
const iconKey = readString(issues, `${path}.iconKey`, value.iconKey, { optional: true });
|
||||
|
||||
const portsRaw = value.ports;
|
||||
if (portsRaw !== undefined && !Array.isArray(portsRaw)) {
|
||||
issues.push({ path: `${path}.ports`, message: 'must be an array when provided' });
|
||||
}
|
||||
/** @type {PortEntry[]} */
|
||||
const portEntries = [];
|
||||
const ports = [];
|
||||
for (const [portIndex, portValue] of (Array.isArray(portsRaw) ? portsRaw : []).entries()) {
|
||||
const port = normalizePort(issues, `${path}.ports[${portIndex}]`, portValue, 'device');
|
||||
if (port) {
|
||||
portEntries.push({ port, originalIndex: portIndex });
|
||||
ports.push(port);
|
||||
}
|
||||
}
|
||||
validatePortUniqueness(issues, portEntries, `${path}.ports`, name ?? 'device');
|
||||
|
||||
if (!name || !ipAddress || !type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
name,
|
||||
ipAddress,
|
||||
type,
|
||||
...(iconKey ? { iconKey } : {}),
|
||||
...(notes !== undefined ? { notes } : {}),
|
||||
ports
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {unknown} value
|
||||
* @returns {ValidationResult}
|
||||
*/
|
||||
export function validateNetworkData(value) {
|
||||
/** @type {ValidationIssue[]} */
|
||||
const issues = [];
|
||||
if (!isRecord(value)) {
|
||||
return {
|
||||
valid: false,
|
||||
errors: [{ path: '$', message: 'must be an object with machines/devices arrays' }]
|
||||
};
|
||||
}
|
||||
|
||||
const machinesRaw = value.machines;
|
||||
if (!Array.isArray(machinesRaw)) {
|
||||
issues.push({ path: '$.machines', message: 'must be an array' });
|
||||
}
|
||||
|
||||
const devicesRaw = value.devices;
|
||||
if (!Array.isArray(devicesRaw)) {
|
||||
issues.push({ path: '$.devices', message: 'must be an array' });
|
||||
}
|
||||
|
||||
const machines = [];
|
||||
const machineEntries = [];
|
||||
for (const [machineIndex, machineValue] of (Array.isArray(machinesRaw) ? machinesRaw : []).entries()) {
|
||||
const machine = normalizeMachine(issues, `$.machines[${machineIndex}]`, machineValue);
|
||||
if (machine) {
|
||||
machines.push(machine);
|
||||
machineEntries.push({ machine, originalIndex: machineIndex });
|
||||
}
|
||||
}
|
||||
|
||||
const devices = [];
|
||||
const deviceEntries = [];
|
||||
for (const [deviceIndex, deviceValue] of (Array.isArray(devicesRaw) ? devicesRaw : []).entries()) {
|
||||
const device = normalizeDevice(issues, `$.devices[${deviceIndex}]`, deviceValue);
|
||||
if (device) {
|
||||
devices.push(device);
|
||||
deviceEntries.push({ device, originalIndex: deviceIndex });
|
||||
}
|
||||
}
|
||||
|
||||
const seenMachineNames = new Set();
|
||||
for (const { machine, originalIndex } of machineEntries) {
|
||||
const key = machine.machineName.toLowerCase();
|
||||
if (seenMachineNames.has(key)) {
|
||||
issues.push({
|
||||
path: `$.machines[${originalIndex}].machineName`,
|
||||
message: `duplicate machine name "${machine.machineName}"`
|
||||
});
|
||||
}
|
||||
seenMachineNames.add(key);
|
||||
}
|
||||
|
||||
const seenDeviceNames = new Set();
|
||||
for (const { device, originalIndex } of deviceEntries) {
|
||||
const key = device.name.toLowerCase();
|
||||
if (seenDeviceNames.has(key)) {
|
||||
issues.push({
|
||||
path: `$.devices[${originalIndex}].name`,
|
||||
message: `duplicate device name "${device.name}"`
|
||||
});
|
||||
}
|
||||
seenDeviceNames.add(key);
|
||||
}
|
||||
|
||||
for (const { machine, originalIndex } of machineEntries) {
|
||||
if (seenDeviceNames.has(machine.machineName.toLowerCase())) {
|
||||
issues.push({
|
||||
path: `$.machines[${originalIndex}].machineName`,
|
||||
message: `name "${machine.machineName}" collides with a device name`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const data = {
|
||||
machines,
|
||||
devices
|
||||
};
|
||||
|
||||
return {
|
||||
valid: issues.length === 0,
|
||||
errors: issues,
|
||||
...(issues.length === 0 ? { data } : {})
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {NetworkData} data
|
||||
* @returns {NetworkData}
|
||||
*/
|
||||
export function normalizeNetworkData(data) {
|
||||
const cloned = deepClone(data);
|
||||
const validated = validateNetworkData(cloned);
|
||||
if (!validated.valid || !validated.data) {
|
||||
return {
|
||||
machines: [],
|
||||
devices: []
|
||||
};
|
||||
}
|
||||
return validated.data;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export type ThemeMode = 'light' | 'dark';
|
||||
|
||||
const STORAGE_KEY = 'ond-theme';
|
||||
|
||||
function getInitialTheme(): ThemeMode {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'light';
|
||||
}
|
||||
const stored = window.localStorage.getItem(STORAGE_KEY);
|
||||
if (stored === 'light' || stored === 'dark') {
|
||||
return stored;
|
||||
}
|
||||
return 'light';
|
||||
}
|
||||
|
||||
function applyTheme(theme: ThemeMode) {
|
||||
if (typeof document === 'undefined') {
|
||||
return;
|
||||
}
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
|
||||
function createThemeStore() {
|
||||
const store = writable<ThemeMode>('light');
|
||||
|
||||
return {
|
||||
subscribe: store.subscribe,
|
||||
initialize() {
|
||||
const theme = getInitialTheme();
|
||||
store.set(theme);
|
||||
applyTheme(theme);
|
||||
},
|
||||
toggle() {
|
||||
store.update((current) => {
|
||||
const next = current === 'dark' ? 'light' : 'dark';
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, next);
|
||||
}
|
||||
applyTheme(next);
|
||||
return next;
|
||||
});
|
||||
},
|
||||
set(theme: ThemeMode) {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.localStorage.setItem(STORAGE_KEY, theme);
|
||||
}
|
||||
applyTheme(theme);
|
||||
store.set(theme);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const themeMode = createThemeStore();
|
||||
@@ -21,6 +21,7 @@ export interface VM {
|
||||
name: string;
|
||||
role: string;
|
||||
ipAddress: string;
|
||||
iconKey?: string;
|
||||
}
|
||||
|
||||
export interface Software {
|
||||
@@ -32,6 +33,7 @@ export interface Machine {
|
||||
ipAddress: string;
|
||||
role: string;
|
||||
operatingSystem: string;
|
||||
iconKey?: string;
|
||||
software: Software;
|
||||
hardware: Hardware;
|
||||
ports?: Port[];
|
||||
@@ -41,6 +43,7 @@ export interface NetworkDevice {
|
||||
name: string;
|
||||
ipAddress: string;
|
||||
type: string;
|
||||
iconKey?: string;
|
||||
notes?: string;
|
||||
ports?: Port[];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
|
||||
import { validateNetworkData } from '$lib/data/networkSchema';
|
||||
import {
|
||||
checkWritableState,
|
||||
isWriteEnabled,
|
||||
readNetworkFile,
|
||||
writeNetworkFile
|
||||
} from '$lib/server/networkPersistence';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
function serializeValidationErrors(errors: Array<{ path: string; message: string }>) {
|
||||
return errors.map((issue) => ({
|
||||
path: issue.path,
|
||||
message: issue.message
|
||||
}));
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async () => {
|
||||
try {
|
||||
const [{ data, source, updatedAt }, writable] = await Promise.all([
|
||||
readNetworkFile(),
|
||||
checkWritableState()
|
||||
]);
|
||||
const validation = validateNetworkData(data);
|
||||
if (!validation.valid || !validation.data) {
|
||||
return json(
|
||||
{
|
||||
error: 'Stored network data is invalid',
|
||||
details: serializeValidationErrors(validation.errors)
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
return json({
|
||||
data: validation.data,
|
||||
writable,
|
||||
source,
|
||||
updatedAt
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error && error.message ? error.message : 'Failed to read network data file';
|
||||
return json({ error: message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
|
||||
export const PUT: RequestHandler = async ({ request }) => {
|
||||
if (!isWriteEnabled()) {
|
||||
return json(
|
||||
{
|
||||
error: 'Write API disabled. Unset NETWORK_READ_ONLY to enable persistence.'
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
let payload: unknown;
|
||||
try {
|
||||
payload = await request.json();
|
||||
} catch {
|
||||
return json({ error: 'Request body must be valid JSON' }, { status: 400 });
|
||||
}
|
||||
|
||||
const validation = validateNetworkData(payload);
|
||||
if (!validation.valid || !validation.data) {
|
||||
return json(
|
||||
{
|
||||
error: 'Network data validation failed',
|
||||
details: serializeValidationErrors(validation.errors)
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const metadata = await writeNetworkFile(validation.data);
|
||||
const writable = await checkWritableState();
|
||||
return json({
|
||||
data: validation.data,
|
||||
writable,
|
||||
source: metadata.source,
|
||||
updatedAt: metadata.updatedAt
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error && error.message ? error.message : 'Failed to persist network data';
|
||||
return json({ error: message }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<path d="M24 46h24a10 10 0 0 0 2-19 14 14 0 0 0-27-2 9 9 0 0 0 1 21z" fill="#0ea5e9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 169 B |
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="10" y="10" width="44" height="30" rx="4" fill="#7c3aed"/>
|
||||
<rect x="24" y="42" width="16" height="6" rx="2" fill="#c4b5fd"/>
|
||||
<rect x="18" y="50" width="28" height="4" rx="2" fill="#a78bfa"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="9" y="24" width="46" height="20" rx="5" fill="#2563eb"/>
|
||||
<path d="M18 20c4-4 8-6 14-6s10 2 14 6" stroke="#93c5fd" stroke-width="3" stroke-linecap="round"/>
|
||||
<path d="M22 24c3-3 6-4 10-4s7 1 10 4" stroke="#bfdbfe" stroke-width="2.5" stroke-linecap="round"/>
|
||||
<circle cx="20" cy="34" r="2" fill="#dbeafe"/>
|
||||
<circle cx="28" cy="34" r="2" fill="#dbeafe"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="10" y="10" width="44" height="18" rx="4" fill="#0ea5e9"/>
|
||||
<rect x="10" y="36" width="44" height="18" rx="4" fill="#0284c7"/>
|
||||
<circle cx="18" cy="19" r="2" fill="#e0f2fe"/>
|
||||
<circle cx="18" cy="45" r="2" fill="#e0f2fe"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 316 B |
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="12" y="10" width="40" height="44" rx="6" fill="#16a34a"/>
|
||||
<rect x="18" y="18" width="28" height="8" rx="2" fill="#dcfce7"/>
|
||||
<rect x="18" y="30" width="28" height="8" rx="2" fill="#dcfce7"/>
|
||||
<rect x="18" y="42" width="28" height="8" rx="2" fill="#dcfce7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@@ -0,0 +1,11 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
|
||||
<rect x="8" y="18" width="48" height="28" rx="6" fill="#f59e0b"/>
|
||||
<rect x="14" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="24" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="34" y="26" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="44" y="26" width="6" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="14" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="24" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="34" y="34" width="8" height="4" rx="1" fill="#fff7ed"/>
|
||||
<rect x="44" y="34" width="6" height="4" rx="1" fill="#fff7ed"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 684 B |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 52 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 44 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 45 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 202 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 76 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 49 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 29 KiB |
|
After Width: | Height: | Size: 64 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 30 KiB |