diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a8509aa --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +node_modules +**/node_modules +**/dist +.git diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..471b58e --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run check + - run: npm test + - run: npm run build + - name: Validate the demo house + run: node server/dist/cli.js validate examples/demo-house + + docker: + runs-on: ubuntu-latest + needs: build + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/login-action@v3 + if: github.event_name == 'push' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - uses: docker/build-push-action@v6 + with: + context: . + push: ${{ github.event_name == 'push' }} + tags: ghcr.io/matdata-eu/hadoc:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7f5ed9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.log +.DS_Store +.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e6f538f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1 +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +COPY server/package.json server/ +COPY web/package.json web/ +RUN npm ci +COPY tsconfig.base.json ./ +COPY schema schema +COPY server server +COPY web web +RUN npm run build && npm prune --omit=dev + +FROM node:22-alpine +ENV NODE_ENV=production \ + HADOC_DATA=/data \ + PORT=8080 +WORKDIR /app +COPY --from=build /app/package.json ./ +COPY --from=build /app/node_modules node_modules +COPY --from=build /app/schema schema +COPY --from=build /app/server/package.json server/ +COPY --from=build /app/server/dist server/dist +COPY --from=build /app/web/dist web/dist +COPY examples/demo-house /data +USER node +EXPOSE 8080 +HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://127.0.0.1:8080/api/health || exit 1 +CMD ["node", "server/dist/index.js"] diff --git a/README.md b/README.md index b222a00..3004675 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,111 @@ # HAdoc -Home automation documentation: floor plans, cabling, power and automation links for your house, in a phone-friendly web app. +**Home automation documentation.** One phone-friendly web app that answers, for anything in the house: *what is this, where does it get power, how is it wired, and what controls it?* + +HAdoc keeps floor plans, cabling, power circuits, controller I/O (PLC channels, ESP pins) and the link to your home automation software in one place, as plain YAML files you keep in git. + +> Status: early scaffold. The viewer, data format, validation and trace work; editing in the app, importers and the AREI diagrams are on the [roadmap](#roadmap). + +## What you get + +- **Floor plan** per level, with rooms, points and cable routes. Pinch and drag on a phone, scroll and drag on a desktop. +- **Layers** for power, automation, data, hidden things and cables. +- **Tap anything** to see its trace: the circuit and breaker that feed it, the cables and terminals it lands on, the device or PLC channel behind it, and the MQTT topic or openHAB item that reads it. +- **"If it stops working"** notes in plain language, for everyone in the house. +- **Checks** that flag broken references, IDs that don't follow your scheme, and what is still undocumented. +- **Search** by room, ID, device, PLC name or openHAB item. +- Installs on a phone home screen and keeps showing the last copy when the server is down. + +## Quick start + +```bash +docker run -d --name hadoc -p 8090:8080 -v "$PWD/house:/data" ghcr.io/matdata-eu/hadoc:latest +``` + +Open http://localhost:8090. Without a volume the image shows the demo house from [`examples/demo-house`](examples/demo-house). See [`docker-compose.yml`](docker-compose.yml) for a compose example. + +To run from source (Node 22): + +```bash +npm install +npm run build +node server/dist/index.js examples/demo-house +``` + +For development, run `npm run dev -w server` and `npm run dev -w web` in two terminals and open the Vite URL. + +## Your data + +Copy [`examples/demo-house`](examples/demo-house) into its own (private) git repo and edit the YAML. Every file has a JSON Schema in [`schema/`](schema); the `# yaml-language-server` line at the top of each file gives completion and validation in VS Code with the YAML extension. + +| File | Holds | +| --- | --- | +| `settings.yaml` | House name, the **point ID pattern** and the **level and type abbreviations** | +| `site.yaml` | Floors and rooms, in centimetres, with an optional plan image as tracing underlay | +| `points/*.yaml` | Switches, sensors, lights, sockets, hidden boards: anything you can touch | +| `enclosures.yaml` | Distribution boards, PLC cabinets, racks | +| `devices.yaml` | PLC and I/O modules, ESP boards, relays, breakers, with their ports | +| `cables.yaml` | Cables by the label on the cable, their ends and route | +| `circuits.yaml` | Breaker circuits and what they feed | +| `signals.yaml` | Where software sees a port: PLC name and address, MQTT topic, openHAB item | + +Cable and circuit ends are written as a point or device ID, or as `device:port` (for example `plc-di01:DI 01.1`). Plan images go in `plans/`, photos in `photos/`. + +### Point IDs + +The default pattern is `{level}-{room}-{type}-{nr}`, for example `1-DRS-PIR-01`. The pattern and every abbreviation are settings: + +```yaml +ids: + pattern: "{level}-{room}-{type}-{nr}" + nr_digits: 2 +levels: + - { code: MIN1, name: Basement } + - { code: "0", name: Ground floor } +types: + - { code: PIR, name: Motion sensor, layer: automation } +``` + +Room codes are defined per floor in `site.yaml`. + +### Validate + +```bash +node server/dist/cli.js validate path/to/house +``` + +Errors (broken references, schema violations, IDs that contradict their fields) exit with code 1, so this works as a pre-commit hook or CI step in your data repo. Warnings list what is still undocumented. + +## API + +| Endpoint | Returns | +| --- | --- | +| `GET /api/house` | All data plus error and warning counts | +| `GET /api/trace/:id` | The trace for a point, device or enclosure | +| `GET /api/issues` | Every error and warning | +| `GET /api/health` | Liveness and when the data was last loaded | + +The server reloads the data when files in the data folder change. + +## Roadmap + +1. **MVP**: plan underlay, rooms, points, point pages, trace, checks *(this scaffold)*. Next: placing and editing points in the app, with each change committed to git. +2. **WAGO import** from CODESYS 2.3 `PLC CONFIGURATION.EXP`, seeding points from channel names and comments. +3. **Cables and circuits**: drawing routes, terminals and cores, breaker colouring. +4. **Importers**: openHAB, UniFi (status and IP), ESPHome, Tasmota, MQTT, plus the "undocumented" report and the rules behind each item. +5. **PLC logic viewer** from POU exports, wall and enclosure views, QR labels. +6. **AREI** single-line diagram (eendraadschema) and situation sketch (situatieschets). +7. Importers for other systems (Home Assistant, KNX, Loxone, Shelly). + +## Project layout + +``` +schema/ JSON Schemas for every data file +server/ Fastify API, YAML loader, checks, trace, CLI (TypeScript) +web/ Svelte app: floor plan, details, issues +examples/ Demo house +``` + +## License + +[MIT](LICENSE) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..67a5169 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,11 @@ +# Example: run HAdoc next to openHAB. Point the volume at a git checkout of your house data. +services: + hadoc: + image: ghcr.io/matdata-eu/hadoc:latest + build: . + container_name: hadoc + restart: unless-stopped + ports: + - "8090:8080" + volumes: + - ./house:/data diff --git a/examples/demo-house/cables.yaml b/examples/demo-house/cables.yaml new file mode 100644 index 0000000..e3aa6c1 --- /dev/null +++ b/examples/demo-house/cables.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=../../schema/cables.schema.json +# Cable IDs are the labels on the cables. +- id: K-0012 + type: SVV 4x0.8 + from: 0-HAL-SW-01 + to: "plc-di01:DI 01.1" + route: + - level: "0" + path: [[40, 760], [20, 760], [20, 20]] + +- id: K-0031 + type: XVB 3G1.5 + from: "plc-do01:DO 01.1" + to: 0-HAL-LGT-01 + +- id: K-0107 + type: null + from: 1-DRS-PIR-01 + to: "esp-dressing:GPIO14" + route: + - level: "1" + path: [[450, 30], [520, 30], [520, 80]] diff --git a/examples/demo-house/circuits.yaml b/examples/demo-house/circuits.yaml new file mode 100644 index 0000000..fb75bfd --- /dev/null +++ b/examples/demo-house/circuits.yaml @@ -0,0 +1,8 @@ +# yaml-language-server: $schema=../../schema/circuits.schema.json +- id: F3 + name: Lights first floor + breaker: F3 + rating: C16 + rcd: RCD1 + phase: L1 + feeds: [1-DRS-LGT-01, sonoff-mirror, 1-BED-BLD-01] diff --git a/examples/demo-house/devices.yaml b/examples/demo-house/devices.yaml new file mode 100644 index 0000000..82f3f9e --- /dev/null +++ b/examples/demo-house/devices.yaml @@ -0,0 +1,55 @@ +# yaml-language-server: $schema=../../schema/devices.schema.json +- id: plc + kind: plc + model: WAGO 750-881 + location: plc-cabinet + hostname: plc.lan + notes: Low-level logic for switches and blinds. + +- id: plc-di01 + kind: io-module + model: WAGO 750-403 (4 DI 24V) + label: DI 01 + location: plc + ports: + - { name: "DI 01.1", signal: DI 24V } + - { name: "DI 01.2", signal: DI 24V } + - { name: "DI 01.3", signal: DI 24V } + - { name: "DI 01.4", signal: DI 24V } + +- id: plc-do01 + kind: io-module + model: WAGO 750-504 (4 DO 24V) + label: DO 01 + location: plc + ports: + - { name: "DO 01.1", signal: DO 24V } + - { name: "DO 01.2", signal: DO 24V } + +- id: esp-dressing + kind: esp + model: NodeMCU v3 + location: 1-DRS-ESP-01 + firmware: ESPHome + hostname: esp-dressing + mac: null + powered_by: sonoff-mirror + ports: + - { name: GPIO14, signal: GPIO } + +- id: sonoff-mirror + kind: relay + model: Sonoff Mini + location: 1-DRS-LGT-01 + firmware: Tasmota + hostname: sonoff-mirror + +- id: F3 + kind: breaker + model: C16 + location: board-main + +- id: RCD1 + kind: rcd + model: 30 mA type A + location: board-main diff --git a/examples/demo-house/enclosures.yaml b/examples/demo-house/enclosures.yaml new file mode 100644 index 0000000..0c5ec01 --- /dev/null +++ b/examples/demo-house/enclosures.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=../../schema/enclosures.schema.json +- id: board-main + name: Main distribution board + level: MIN1 + room: null + pos: null + rails: 4 + +- id: plc-cabinet + name: PLC cabinet + level: MIN1 + room: null + pos: null + rails: 3 diff --git a/examples/demo-house/points/0-ground-floor.yaml b/examples/demo-house/points/0-ground-floor.yaml new file mode 100644 index 0000000..07b1350 --- /dev/null +++ b/examples/demo-house/points/0-ground-floor.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=../../../schema/points.schema.json +- id: 0-HAL-SW-01 + type: SW + level: "0" + room: HAL + name: Hall light switch by the front door + pos: [40, 760] + height: 110 + if_broken: The hall light can also be switched from the app. + +- id: 0-HAL-LGT-01 + type: LGT + level: "0" + room: HAL + name: Hall ceiling light + pos: [150, 250] + height: 260 + +- id: 0-KIT-SKT-01 + type: SKT + level: "0" + room: KIT + name: Socket above the sink + pos: [700, 20] + height: 110 diff --git a/examples/demo-house/points/1-first-floor.yaml b/examples/demo-house/points/1-first-floor.yaml new file mode 100644 index 0000000..d3400d4 --- /dev/null +++ b/examples/demo-house/points/1-first-floor.yaml @@ -0,0 +1,35 @@ +# yaml-language-server: $schema=../../../schema/points.schema.json +- id: 1-DRS-PIR-01 + type: PIR + level: "1" + room: DRS + name: Motion sensor above the mirror + pos: [450, 30] + height: 240 + notes: Wired to a hidden ESP board above the ceiling hatch. + if_broken: Check that the ESP board is online in the app. It is powered from the mirror light relay. + +- id: 1-DRS-ESP-01 + type: ESP + level: "1" + room: DRS + name: Hidden ESP board above the ceiling hatch + pos: [520, 80] + height: null + device: esp-dressing + +- id: 1-DRS-LGT-01 + type: LGT + level: "1" + room: DRS + name: Mirror light + pos: [450, 60] + height: 200 + +- id: 1-BED-BLD-01 + type: BLD + level: "1" + room: BED + name: Bedroom blind + pos: [990, 250] + height: 240 diff --git a/examples/demo-house/settings.yaml b/examples/demo-house/settings.yaml new file mode 100644 index 0000000..38e16b4 --- /dev/null +++ b/examples/demo-house/settings.yaml @@ -0,0 +1,22 @@ +# yaml-language-server: $schema=../../schema/settings.schema.json +name: Demo house +language: en + +# Point IDs. Change the pattern and the abbreviations to your own convention. +ids: + pattern: "{level}-{room}-{type}-{nr}" + nr_digits: 2 + +levels: + - { code: MIN1, name: Basement } + - { code: "0", name: Ground floor } + - { code: "1", name: First floor } + +types: + - { code: SW, name: Wall switch, layer: automation } + - { code: PIR, name: Motion sensor, layer: automation } + - { code: LGT, name: Light, layer: power } + - { code: SKT, name: Socket, layer: power } + - { code: BLD, name: Blind motor, layer: automation } + - { code: ESP, name: Hidden ESP board, layer: hidden } + - { code: JB, name: Junction box, layer: hidden } diff --git a/examples/demo-house/signals.yaml b/examples/demo-house/signals.yaml new file mode 100644 index 0000000..0e72338 --- /dev/null +++ b/examples/demo-house/signals.yaml @@ -0,0 +1,15 @@ +# yaml-language-server: $schema=../../schema/signals.schema.json +- id: SIG-0-HAL-SW-01 + name: Hall switch + port: "plc-di01:DI 01.1" + point: 0-HAL-SW-01 + plc: { name: SB_0_1L, address: "%IX4.0" } + logic: plc + +- id: SIG-1-DRS-PIR-01 + name: Dressing room motion + port: "esp-dressing:GPIO14" + point: 1-DRS-PIR-01 + mqtt: esp-dressing/binary_sensor/motion/state + openhab: { thing: null, item: Dressing_Motion } + logic: openhab diff --git a/examples/demo-house/site.yaml b/examples/demo-house/site.yaml new file mode 100644 index 0000000..0dd345e --- /dev/null +++ b/examples/demo-house/site.yaml @@ -0,0 +1,32 @@ +# yaml-language-server: $schema=../../schema/site.schema.json +# Coordinates in centimetres, origin at the top-left corner of each floor. +floors: + - level: "0" + width: 1000 + height: 800 + rooms: + - code: HAL + name: Hall + outline: [[0, 0], [300, 0], [300, 800], [0, 800]] + - code: KIT + name: Kitchen + outline: [[300, 0], [1000, 0], [1000, 450], [300, 450]] + - code: LIV + name: Living room + outline: [[300, 450], [1000, 450], [1000, 800], [300, 800]] + - level: "1" + width: 1000 + height: 800 + rooms: + - code: LND + name: Landing + outline: [[0, 0], [300, 0], [300, 800], [0, 800]] + - code: DRS + name: Dressing room + outline: [[300, 0], [600, 0], [600, 500], [300, 500]] + - code: BED + name: Bedroom + outline: [[600, 0], [1000, 0], [1000, 500], [600, 500]] + - code: BTH + name: Bathroom + outline: [[300, 500], [1000, 500], [1000, 800], [300, 800]] diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..6f43dde --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2923 @@ +{ + "name": "hadoc", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "hadoc", + "version": "0.1.0", + "license": "MIT", + "workspaces": [ + "server", + "web" + ], + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.1.1.tgz", + "integrity": "sha512-gO48sJ/4+P9hYE24tEoWQF4fZvCfRmStf6NFhuT5hwXkH+3JXGdemoE/7ympeQZglieeLJx256rbg8e58IrCyA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.7.tgz", + "integrity": "sha512-VP0yIfwlc1ohj733keYDqyHm1VWeP9VQxYomunhr90jkC4aNNibbkrRiaNgPgqj1ORxUenILR4qzA/ppYBNAtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/fast-uri": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.2.tgz", + "integrity": "sha512-EwVpUMzLVNOmcWW0zi5G3JJwkopB6XXjnVvNBGfqrMMsVew2hkQWl5e/pe/CfYUeS6jt4cIO2MnamEW7v/7wPw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/@fastify/deepmerge": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-3.2.2.tgz", + "integrity": "sha512-155eqA/WMQdN6gUD4RrhS2K0Pcz2Ou9pQ86L0sipOpP9ONbTA6oYw/hW4VdBjR4egcDHqmWUK9x80oUX7S1mDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.1.tgz", + "integrity": "sha512-MYBumuIwP+5MIpXSyKm6/o1Eigcoij186lWUssX3LH5U8KWqU0ktZhzC4CErIX9EJeQeRGI+zJZQWakBlFbUcg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.1.tgz", + "integrity": "sha512-pyxRKk7UpUnnrztP9pF4+ZYxusEQ4LifTyzRdiXmyql1QezNHxO02D25MVU2o4kMeAMYWAjG6RKRlojgkbiDAw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.1.1.tgz", + "integrity": "sha512-XfDtW2Icvhqi6DP1YHFK4q/E8WpNd3+raBdzAyVZxmTInbFTtnEEKvEAMMOVdFuRyv+ynYipaA9D8FvjPqACkw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.2.tgz", + "integrity": "sha512-d/gx9sRbUCVgGRGzma46wLcwiY8noTqO2Nmjg/vbTARxHRE5bdh5PIL8s97Lt1X7ScRWRW52eVYxwIoPwIdVUA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.2.tgz", + "integrity": "sha512-uqB4QsgVoro0XDC+DIftIMGXHdBPn+lK6Ek3xXAIxPMCJfqxnbtyEoMCm2KPUVOuIosd7Cyw9HawEqMFM/UA+g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@fastify/send": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.2.tgz", + "integrity": "sha512-PitRBXg/N4IrE5ToLuP7lITI5Mz8MmP13dv2aMtMNfaGfc5W5hNOwHqcsdQdqu0Lwm5cpDiYLkiOsZTgmhyt2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@lukeed/ms": "^2.0.2", + "escape-html": "~1.0.3", + "fast-decode-uri-component": "^1.0.1", + "http-errors": "^2.0.0", + "mime": "^3" + } + }, + "node_modules/@fastify/static": { + "version": "10.1.6", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.6.tgz", + "integrity": "sha512-f3x9XisI+y6AIhIfA/+Q3SFsuXeqNNk5HQh5htgHsPxmMLb4pLFskV6eFaLSW3IbI4azrS5np+/tkzXNH99LyQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/accept-negotiator": "^2.0.0", + "@fastify/error": "^4.0.0", + "@fastify/send": "^4.0.0", + "content-disposition": "^3.0.0", + "fastify-plugin": "^6.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@hadoc/server": { + "resolved": "server", + "link": true + }, + "node_modules/@hadoc/web": { + "resolved": "web", + "link": true + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lukeed/ms": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", + "integrity": "sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sveltejs/acorn-typescript": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.13.tgz", + "integrity": "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8.9.0" + } + }, + "node_modules/@sveltejs/load-config": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@sveltejs/load-config/-/load-config-0.2.3.tgz", + "integrity": "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.3.1.tgz", + "integrity": "sha512-ZPsLN8B1e/En+Ak5s4V7srFDT532oS0qieLsQwu63NGKsS+iAjoO2Js1BochlHlglcU+Pt7WAO3C5Ee+4f6gVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "deepmerge": "^4.3.1", + "magic-string": "^1.0.0", + "obug": "^2.1.0", + "vitefu": "^1.1.2" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "svelte": "^5.46.4", + "vite": "^8.0.0-beta.7 || ^8.0.0" + } + }, + "node_modules/@tsconfig/svelte": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/@tsconfig/svelte/-/svelte-5.0.8.tgz", + "integrity": "sha512-UkNnw1/oFEfecR8ypyHIQuWYdkPvHiwcQ78sh+ymIiYoF+uc5H1UBetbjyqT+vgGJ3qQN6nhucJviX6HesWtKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.20.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz", + "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.3.tgz", + "integrity": "sha512-T8sWAIbkSyAjkwTcaEc3Iu0o9A27X1/kdXrizhZkGuSKScRQtRzclfAMpOTcGdXCsqxeWlpGy3XjqaW8CpLORg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.31", + "@vitest/spy": "5.0.3", + "estree-walker": "^3.0.3", + "magic-string": "^1.2.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/spy": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.3.tgz", + "integrity": "sha512-XhFysQTB8AZ+P4gMi+Lpo99vg2AZi0qKpaB9yXQl37+CaMEAPO3iH/wGVnSyL5MPERiLezpqTVtrR6UZH5GCXg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/aria-query": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", + "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.1.tgz", + "integrity": "sha512-QyTKW61/qlba+cUQBm/sT9X8lri4UdomqSDwCnrnj1IvLVDRRiXRkz4CgBRWB+/cmfC7SK5WQc8T1vSdGvaUcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/chai": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/content-disposition": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-3.0.0.tgz", + "integrity": "sha512-ZH/0Xs9rMIFWCOmGdmS9eHBTF62qqQYNz4nVjQhkdIO/a0fCP4UIM3mRz/wiqL0L14YgAz/1xio4OaSY4+ON/A==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/esm-env": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", + "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esrap": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.4.0.tgz", + "integrity": "sha512-CQPcneEHw/czj32uwQC52nYnax8H4SXt25FCM+LIwGZ49cMIxpOj3bP8wmzcI+Z0HIy5KkClaPhRLfCySzwHPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "peerDependencies": { + "@typescript-eslint/types": "^8.2.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/types": { + "optional": true + } + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.1.0.tgz", + "integrity": "sha512-wAhAQ+es0aH+x5OofdCHlZZFdqwtiM6lpEosaRIwfg271WYeEQ/tPcXdu5vpzlqZpbJ4WNqaW4QR5UjfG4Dc2w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/fast-uri": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.2.2.tgz", + "integrity": "sha512-EwVpUMzLVNOmcWW0zi5G3JJwkopB6XXjnVvNBGfqrMMsVew2hkQWl5e/pe/CfYUeS6jt4cIO2MnamEW7v/7wPw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.12.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.5.tgz", + "integrity": "sha512-OB2k1dlxs5/NAABqeKV2FUHkSD2BbENsCak8yULVcymn3fHIPDVa9TI3SDnJSWYSllZmSYuZXy2gTnsT+Sut1A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.1.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.1.tgz", + "integrity": "sha512-4wW4RxSmvPXLrh5mgQEiu43qU3bw44N2ss4FTkTkQIbprUhE+/s43QX0j/ttcztdWK7PEkHqVthdc5GdEGDvjA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.3.tgz", + "integrity": "sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-my-way": { + "version": "9.10.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.10.0.tgz", + "integrity": "sha512-eru+uCPQ2g+nLBkf84MHP/KyNNMar0BDy0j+i8Fb72GF9/YHBZ58q7FODUtfVV7XU9nfRUHXSmaM8NmZcIbZ4Q==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-reference": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", + "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.6" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.1.tgz", + "integrity": "sha512-w+HwgljXpVwZ3IidoUR1qPZxSOQ+NRQWuVe6a5pe8XbHsNZE4UAwVPUvzZagn7yBKbJtt6UJnRFYbNTRPZI45Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.8.0.tgz", + "integrity": "sha512-5Q8PjoSEdtGIB8fikZsTem5vgRo4F2mR6vUKTDW/H1g6MtAt8e3duQbg/QxL5yzg6BZhor2KOUm3hhWMWLTZwg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "@fastify/deepmerge": "^3.1.0", + "cookie": "^2.0.0", + "process-warning": "^5.0.0", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nanoid": { + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", + "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.4.0.tgz", + "integrity": "sha512-bk1ZMTwG/Vymx+zPoa28MhNzucFvXJWp3csrSuxuHPSMeUettaCoQ5Jvg07mtC4JGNpcfTwz41oVCnNgBK9+vA==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^1.0.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.29", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.29.tgz", + "integrity": "sha512-49cGhUbXj8Qenv0iTMxA1cFBzxXoctpC9Ujd77t1WcbJIr6nF/eI7g/8MgxrYldFRuAXvja7xQRwavoW7kgrxQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.19", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.1.tgz", + "integrity": "sha512-oCkNVj4OKnKEcocVBcDfiIugN1/cE6rgfT3nCtzeqZPA8+rIxMaQ0xXzIkGP8Gj2qUaP1UR7uhJ1VD8lUCpzCg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.2.tgz", + "integrity": "sha512-tAtCGFPPP02VroDVzp4daGi6bC7jWG+41D47GKf18FeH/ubvKQHT9VmhAkvtjNOmJwqtApzWeUpEhkyEYULvmw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.1.tgz", + "integrity": "sha512-lnd81MQEZKjhW/Gd2TQqdd4XP6QYNdDW/EP+I7nTrFHP9y2LmaGPIHk9xsnzcOiEr1c39c/e84xFgGviQKBTRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.3.tgz", + "integrity": "sha512-xletSdAI5efyVJjsatBbzDkXl3M/Zmad3xfnQe7ZXuBB0yNwW7C2WFsNGjdRNfZunmNHQuX79xs/ACMTLCdcew==", + "license": "MIT" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", + "dev": true, + "license": "MIT" + }, + "node_modules/svelte": { + "version": "5.57.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.57.2.tgz", + "integrity": "sha512-1YiPCAXT0Ct6g1vOMmSLa0Nsc3QRMvtDF32ihgglGVrugIuIK38RyDiFNXVDhtQ81TJjSO57NdMw6DyYLqSGDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "@jridgewell/sourcemap-codec": "^1.6.0", + "@sveltejs/acorn-typescript": "^1.0.13", + "@types/estree": "^1.0.9", + "acorn": "^8.18.0", + "aria-query": "5.3.1", + "axobject-query": "^4.1.0", + "clsx": "^2.1.1", + "devalue": "^5.9.2", + "esm-env": "^1.2.1", + "esrap": "^2.3.6", + "is-reference": "^3.0.3", + "locate-character": "^3.0.0", + "magic-string": "^0.30.11", + "zimmerframe": "^1.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/svelte-check": { + "version": "4.7.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.7.6.tgz", + "integrity": "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "@sveltejs/load-config": "^0.2.3", + "chokidar": "^4.0.1", + "fdir": "^6.2.0", + "picocolors": "^1.0.0", + "sade": "^1.7.4" + }, + "bin": { + "svelte-check": "bin/svelte-check" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "typescript": "^5.0.0 || ^6.0.0" + } + }, + "node_modules/svelte/node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tinybench": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.3.tgz", + "integrity": "sha512-xMw97S3rjdtj5dkVat7jCsqWBpvchs3RlpQctUqwJD0KkERk40vz2fJ77lDwW/Vzh/pk18eItYAzkodhSes3jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/mocker": "5.0.3", + "chai": "^6.2.2", + "es-module-lexer": "^2.3.2", + "expect-type": "^1.4.0", + "magic-string": "^1.2.3", + "obug": "^2.1.4", + "picomatch": "^4.0.7", + "std-env": "^4.2.0", + "tinybench": "^6.1.4", + "tinyexec": "^1.3.0", + "tinyglobby": "^0.2.17", + "why-is-node-running": "3.2.1" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^22.12.0 || ^24.0.0 || >=26.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "5.0.3", + "@vitest/browser-preview": "5.0.3", + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "@vitest/coverage-istanbul": "5.0.3", + "@vitest/coverage-v8": "5.0.3", + "@vitest/ui": "5.0.3", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", + "dev": true, + "license": "MIT", + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=20.11" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zimmerframe": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.5.tgz", + "integrity": "sha512-msJxIvYDYcoNL+PJsu+7qmpDWsYmAxTY+2TNYXXF0hzBzBk0BMecOqDOG/EckUoKCuKwObfbugIl8QpqHDXeFA==", + "dev": true, + "license": "MIT" + }, + "server": { + "name": "@hadoc/server", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@fastify/static": "^10.1.6", + "ajv": "^8.20.0", + "fastify": "^5.12.5", + "yaml": "^2.9.1" + }, + "bin": { + "hadoc": "dist/cli.js" + }, + "devDependencies": { + "@types/node": "^22.20.5", + "tsx": "^4.23.15", + "typescript": "^6.0.3", + "vitest": "^5.0.3" + } + }, + "web": { + "name": "@hadoc/web", + "version": "0.1.0", + "license": "MIT", + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.57.2", + "svelte-check": "^4.7.6", + "typescript": "^6.0.3", + "vite": "^8.3.4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..68b9b21 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "hadoc", + "version": "0.1.0", + "private": true, + "description": "Home automation documentation: floor plans, cabling, power and automation links in one phone-friendly web app", + "license": "MIT", + "workspaces": [ + "server", + "web" + ], + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "npm run build -w web && npm run build -w server", + "check": "npm run check -w server && npm run check -w web", + "test": "npm test -w server", + "validate": "npm run validate -w server --" + } +} diff --git a/schema/cables.schema.json b/schema/cables.schema.json new file mode 100644 index 0000000..8f3724a --- /dev/null +++ b/schema/cables.schema.json @@ -0,0 +1,46 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/cables.schema.json", + "title": "HAdoc cables", + "description": "Physical cables. Endpoints are a point ID, a device ID, or 'device:port'.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "from", "to"], + "properties": { + "id": { "type": "string", "description": "The label already on the cable." }, + "type": { "type": ["string", "null"], "description": "e.g. 'XVB 3G2.5', 'UTP Cat6', 'SVV 4x0.8'." }, + "length": { "type": ["number", "null"], "description": "Length in metres." }, + "from": { "type": ["string", "null"] }, + "to": { "type": ["string", "null"] }, + "cores": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["colour"], + "properties": { + "colour": { "type": "string" }, + "from": { "type": ["string", "null"] }, + "to": { "type": ["string", "null"] } + } + } + }, + "route": { + "type": "array", + "description": "Route on a floor plan, in cm.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["level", "path"], + "properties": { + "level": { "type": "string" }, + "path": { "type": "array", "items": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }, "minItems": 2 } + } + } + }, + "notes": { "type": "string" } + } + } +} diff --git a/schema/circuits.schema.json b/schema/circuits.schema.json new file mode 100644 index 0000000..9f96901 --- /dev/null +++ b/schema/circuits.schema.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/circuits.schema.json", + "title": "HAdoc circuits", + "description": "Power circuits from a breaker in a distribution board.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "feeds"], + "properties": { + "id": { "type": "string", "description": "Circuit label as on the board, e.g. 'F12'." }, + "name": { "type": "string" }, + "breaker": { "type": ["string", "null"], "description": "Device ID of the breaker." }, + "rating": { "type": ["string", "null"], "description": "e.g. 'C16'." }, + "rcd": { "type": ["string", "null"], "description": "Device ID of the upstream RCD." }, + "phase": { "type": ["string", "null"] }, + "feeds": { "type": "array", "items": { "type": "string" }, "description": "Points and devices fed by this circuit." } + } + } +} diff --git a/schema/devices.schema.json b/schema/devices.schema.json new file mode 100644 index 0000000..8450321 --- /dev/null +++ b/schema/devices.schema.json @@ -0,0 +1,36 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/devices.schema.json", + "title": "HAdoc devices", + "description": "Controllers and modules: PLC couplers and I/O modules, ESP boards, relays, power supplies, breakers.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "kind"], + "properties": { + "id": { "type": "string" }, + "kind": { "enum": ["plc", "io-module", "esp", "relay", "psu", "breaker", "rcd", "switch", "other"] }, + "model": { "type": ["string", "null"] }, + "label": { "type": "string", "description": "Physical label, e.g. 'DI 01'." }, + "location": { "type": "string", "description": "Point or enclosure where the device sits." }, + "firmware": { "type": ["string", "null"] }, + "hostname": { "type": ["string", "null"] }, + "mac": { "type": ["string", "null"], "pattern": "^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$" }, + "powered_by": { "type": ["string", "null"], "description": "Endpoint that feeds this device: a device, port or circuit." }, + "ports": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["name"], + "properties": { + "name": { "type": "string", "description": "Physical label as printed, e.g. 'DI 01.3', 'GPIO14', 'L'." }, + "signal": { "type": "string", "description": "e.g. 'DI 24V', 'DO 24V', '230V', 'GPIO'." } + } + } + }, + "notes": { "type": "string" } + } + } +} diff --git a/schema/enclosures.schema.json b/schema/enclosures.schema.json new file mode 100644 index 0000000..6ae02a0 --- /dev/null +++ b/schema/enclosures.schema.json @@ -0,0 +1,27 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/enclosures.schema.json", + "title": "HAdoc enclosures", + "description": "Distribution boards, PLC cabinets, network racks.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "name"], + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "level": { "type": ["string", "null"] }, + "room": { "type": ["string", "null"] }, + "pos": { + "oneOf": [ + { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }, + { "type": "null" } + ] + }, + "rails": { "type": "integer", "minimum": 1, "description": "Number of DIN rails." }, + "photo": { "type": "string" }, + "notes": { "type": "string" } + } + } +} diff --git a/schema/points.schema.json b/schema/points.schema.json new file mode 100644 index 0000000..7cae745 --- /dev/null +++ b/schema/points.schema.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/points.schema.json", + "title": "HAdoc points", + "description": "Physical things you can touch: switches, sensors, lights, sockets, flush boxes, hidden devices.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "type", "level", "room"], + "properties": { + "id": { "type": "string", "description": "Point ID following settings.ids.pattern." }, + "type": { "type": "string", "description": "Type code from settings.types." }, + "level": { "type": "string" }, + "room": { "type": "string", "description": "Room code on that level." }, + "name": { "type": "string", "description": "Plain-language name, e.g. 'Motion sensor above the mirror'." }, + "pos": { + "description": "Position on the floor plan in cm; null while unknown.", + "oneOf": [ + { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 }, + { "type": "null" } + ] + }, + "height": { "type": ["number", "null"], "description": "Mounting height above the floor in cm." }, + "photo": { "type": "string" }, + "notes": { "type": "string" }, + "if_broken": { "type": "string", "description": "What to do when it stops working, in plain words." }, + "device": { "type": "string", "description": "Device ID when this point is (or hides) a device, e.g. a hidden ESP." } + } + } +} diff --git a/schema/settings.schema.json b/schema/settings.schema.json new file mode 100644 index 0000000..397a40c --- /dev/null +++ b/schema/settings.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/settings.schema.json", + "title": "HAdoc settings", + "description": "House-wide settings, including the configurable point ID scheme and abbreviations.", + "type": "object", + "additionalProperties": false, + "required": ["ids", "levels", "types"], + "properties": { + "name": { "type": "string", "description": "Name of the house, shown in the app header." }, + "language": { "type": "string", "description": "UI language hint, e.g. 'nl' or 'en'." }, + "ids": { + "type": "object", + "additionalProperties": false, + "required": ["pattern"], + "properties": { + "pattern": { + "type": "string", + "description": "Point ID pattern. Placeholders: {level}, {room}, {type}, {nr}.", + "default": "{level}-{room}-{type}-{nr}" + }, + "nr_digits": { "type": "integer", "minimum": 1, "maximum": 6, "default": 2 } + } + }, + "levels": { + "type": "array", + "description": "Level codes as used in IDs, e.g. MIN1, 0, 1.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["code", "name"], + "properties": { + "code": { "type": "string", "pattern": "^[A-Za-z0-9]+$" }, + "name": { "type": "string" } + } + } + }, + "types": { + "type": "array", + "description": "Point type abbreviations, e.g. PIR, SW, LGT.", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["code", "name"], + "properties": { + "code": { "type": "string", "pattern": "^[A-Za-z0-9]+$" }, + "name": { "type": "string" }, + "symbol": { "type": "string", "description": "Short text shown on the plan symbol; defaults to the code." }, + "layer": { "enum": ["power", "data", "automation", "hidden"], "default": "power" } + } + } + } + } +} diff --git a/schema/signals.schema.json b/schema/signals.schema.json new file mode 100644 index 0000000..d7505af --- /dev/null +++ b/schema/signals.schema.json @@ -0,0 +1,38 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/signals.schema.json", + "title": "HAdoc signals", + "description": "Logical I/O: where software sees a physical port.", + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["id", "port"], + "properties": { + "id": { "type": "string" }, + "name": { "type": "string" }, + "port": { "type": ["string", "null"], "description": "'device:port' this signal reads or drives." }, + "point": { "type": ["string", "null"], "description": "Point this signal belongs to." }, + "plc": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "type": ["string", "null"], "description": "Software name, e.g. SB_0_8L." }, + "address": { "type": ["string", "null"], "description": "IEC address, e.g. %IX4.2." }, + "modbus": { "type": ["string", "null"] } + } + }, + "mqtt": { "type": ["string", "null"] }, + "openhab": { + "type": "object", + "additionalProperties": false, + "properties": { + "thing": { "type": ["string", "null"] }, + "item": { "type": ["string", "null"] } + } + }, + "logic": { "enum": ["plc", "openhab", "both", "none", null] }, + "notes": { "type": "string" } + } + } +} diff --git a/schema/site.schema.json b/schema/site.schema.json new file mode 100644 index 0000000..e588efe --- /dev/null +++ b/schema/site.schema.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hadoc.dev/schema/site.schema.json", + "title": "HAdoc site", + "description": "Floors and rooms. Coordinates are in centimetres, origin top-left of the floor.", + "type": "object", + "additionalProperties": false, + "required": ["floors"], + "$defs": { + "xy": { "type": "array", "items": { "type": "number" }, "minItems": 2, "maxItems": 2 } + }, + "properties": { + "floors": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["level", "width", "height", "rooms"], + "properties": { + "level": { "type": "string", "description": "Level code from settings.levels." }, + "width": { "type": "number", "exclusiveMinimum": 0 }, + "height": { "type": "number", "exclusiveMinimum": 0 }, + "underlay": { + "type": "object", + "description": "Image exported from the existing plan, drawn under the rooms for tracing.", + "additionalProperties": false, + "required": ["file"], + "properties": { + "file": { "type": "string", "description": "Path relative to the data folder, e.g. plans/level-1.png." }, + "x": { "type": "number", "default": 0 }, + "y": { "type": "number", "default": 0 }, + "width": { "type": "number" }, + "height": { "type": "number" }, + "opacity": { "type": "number", "minimum": 0, "maximum": 1, "default": 0.5 } + } + }, + "rooms": { + "type": "array", + "items": { + "type": "object", + "additionalProperties": false, + "required": ["code", "name", "outline"], + "properties": { + "code": { "type": "string", "pattern": "^[A-Za-z0-9]+$" }, + "name": { "type": "string" }, + "outline": { "type": "array", "items": { "$ref": "#/$defs/xy" }, "minItems": 3 } + } + } + } + } + } + } + } +} diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..cfe5d63 --- /dev/null +++ b/server/package.json @@ -0,0 +1,29 @@ +{ + "name": "@hadoc/server", + "version": "0.1.0", + "private": true, + "type": "module", + "license": "MIT", + "bin": { + "hadoc": "dist/cli.js" + }, + "scripts": { + "dev": "tsx watch src/index.ts ../examples/demo-house", + "build": "tsc -p tsconfig.build.json", + "check": "tsc --noEmit", + "test": "vitest run", + "validate": "tsx src/cli.ts validate" + }, + "dependencies": { + "@fastify/static": "^10.1.6", + "ajv": "^8.20.0", + "fastify": "^5.12.5", + "yaml": "^2.9.1" + }, + "devDependencies": { + "@types/node": "^22.20.5", + "tsx": "^4.23.15", + "typescript": "^6.0.3", + "vitest": "^5.0.3" + } +} diff --git a/server/src/app.ts b/server/src/app.ts new file mode 100644 index 0000000..0453b9a --- /dev/null +++ b/server/src/app.ts @@ -0,0 +1,82 @@ +import { existsSync, watch } from "node:fs"; +import path from "node:path"; +import Fastify from "fastify"; +import fastifyStatic from "@fastify/static"; +import { indexHouse, type HouseIndex } from "./index-house.js"; +import { loadHouse } from "./load.js"; +import type { House } from "./model.js"; +import { traceThing } from "./trace.js"; + +export interface AppOptions { + dataDir: string; + /** Built web app (web/dist). Optional so the API can run on its own in tests. */ + webDir?: string; + /** Reload the data when files in the data folder change. */ + watch?: boolean; + logger?: boolean; +} + +export async function buildApp(options: AppOptions) { + const app = Fastify({ logger: options.logger ?? false }); + const dataDir = path.resolve(options.dataDir); + + let house: House = await loadHouse(dataDir); + let index: HouseIndex = indexHouse(house); + let loadedAt = new Date(); + + const reload = async () => { + house = await loadHouse(dataDir); + index = indexHouse(house); + loadedAt = new Date(); + app.log.info({ issues: house.issues.length }, "data reloaded"); + }; + + if (options.watch) { + let timer: NodeJS.Timeout | undefined; + const watcher = watch(dataDir, { recursive: true }, () => { + clearTimeout(timer); + timer = setTimeout(() => void reload().catch((err) => app.log.error(err)), 300); + }); + app.addHook("onClose", async () => watcher.close()); + } + + app.get("/api/health", async () => ({ ok: true, loadedAt: loadedAt.toISOString() })); + + app.get("/api/house", async () => { + const { issues, ...rest } = house; + return { + ...rest, + issueCount: { + errors: issues.filter((i) => i.severity === "error").length, + warnings: issues.filter((i) => i.severity === "warning").length, + }, + }; + }); + + app.get("/api/issues", async () => house.issues); + + app.get<{ Params: { id: string } }>("/api/trace/:id", async (req, reply) => { + const trace = traceThing(house, index, req.params.id); + if (!trace) return reply.code(404).send({ error: `Nothing with ID "${req.params.id}"` }); + return trace; + }); + + // Plan underlays and photos live in the data folder. + for (const dir of ["plans", "photos"]) { + const root = path.join(dataDir, dir); + if (existsSync(root)) { + await app.register(fastifyStatic, { root, prefix: `/${dir}/`, decorateReply: false }); + } + } + + if (options.webDir && existsSync(options.webDir)) { + await app.register(fastifyStatic, { root: path.resolve(options.webDir), prefix: "/" }); + // Client-side routes fall back to the app shell. + app.setNotFoundHandler((req, reply) => { + if (req.url.startsWith("/api/")) return reply.code(404).send({ error: "Not found" }); + return reply.sendFile("index.html"); + }); + } + + return app; +} diff --git a/server/src/checks.ts b/server/src/checks.ts new file mode 100644 index 0000000..2efd78e --- /dev/null +++ b/server/src/checks.ts @@ -0,0 +1,139 @@ +import { idScheme } from "./ids.js"; +import { indexHouse } from "./index-house.js"; +import type { House, Issue } from "./model.js"; + +function pointInPolygon([x, y]: [number, number], polygon: [number, number][]): boolean { + let inside = false; + for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { + const [xi, yi] = polygon[i]!; + const [xj, yj] = polygon[j]!; + if (yi > y !== yj > y && x < ((xj - xi) * (y - yi)) / (yj - yi) + xi) inside = !inside; + } + return inside; +} + +/** Cross-file checks: references, the ID scheme, and what is still undocumented. */ +export function checkHouse(house: House): Issue[] { + const issues: Issue[] = []; + const error = (file: string, id: string | undefined, message: string) => + issues.push({ severity: "error", file, id, message }); + const warn = (file: string, id: string | undefined, message: string) => + issues.push({ severity: "warning", file, id, message }); + + const levels = new Set(house.settings.levels.map((l) => l.code)); + const types = new Set(house.settings.types.map((t) => t.code)); + const floors = new Map(house.site.floors.map((f) => [f.level, f])); + + for (const floor of house.site.floors) { + if (!levels.has(floor.level)) error("site.yaml", floor.level, `Floor level "${floor.level}" is not in settings.levels`); + const seen = new Set(); + for (const room of floor.rooms) { + if (seen.has(room.code)) error("site.yaml", room.code, `Room code "${room.code}" appears twice on level ${floor.level}`); + seen.add(room.code); + } + } + + // One namespace for everything a cable or circuit can land on. + const owners = new Map(); + const claim = (file: string, id: string) => { + const other = owners.get(id); + if (other) error(file, id, `ID "${id}" is already used in ${other}`); + else owners.set(id, file); + }; + for (const p of house.points) claim("points", p.id); + for (const d of house.devices) claim("devices.yaml", d.id); + for (const e of house.enclosures) claim("enclosures.yaml", e.id); + for (const [file, list] of [ + ["cables.yaml", house.cables], + ["circuits.yaml", house.circuits], + ["signals.yaml", house.signals], + ] as const) { + const seen = new Set(); + for (const item of list) { + if (seen.has(item.id)) error(file, item.id, `ID "${item.id}" appears twice`); + seen.add(item.id); + } + } + + const index = indexHouse(house); + const mustResolve = (file: string, owner: string, field: string, ref: string | null | undefined) => { + if (ref == null) return; + const r = index.resolve(ref); + if (!r.ok) error(file, owner, `${field}: ${r.reason}`); + }; + + let scheme: ReturnType | undefined; + try { + scheme = idScheme(house.settings); + } catch (err) { + error("settings.yaml", undefined, (err as Error).message); + } + + for (const p of house.points) { + const file = "points"; + if (!levels.has(p.level)) error(file, p.id, `Level "${p.level}" is not in settings.levels`); + if (!types.has(p.type)) error(file, p.id, `Type "${p.type}" is not in settings.types`); + const floor = floors.get(p.level); + const room = floor?.rooms.find((r) => r.code === p.room); + if (floor && !room) error(file, p.id, `Room "${p.room}" does not exist on level ${p.level}`); + + if (scheme) { + const parts = scheme.parse(p.id); + if (!parts) { + error(file, p.id, `ID does not follow the pattern ${house.settings.ids.pattern}`); + } else { + if (parts.level !== p.level) error(file, p.id, `ID says level ${parts.level}, but level is ${p.level}`); + if (parts.room !== p.room) error(file, p.id, `ID says room ${parts.room}, but room is ${p.room}`); + if (parts.type !== p.type) error(file, p.id, `ID says type ${parts.type}, but type is ${p.type}`); + } + } + + if (!p.pos) warn(file, p.id, "Not placed on the plan yet"); + else if (room && !pointInPolygon(p.pos, room.outline)) warn(file, p.id, `Position is outside room ${p.room}`); + + if (p.device) { + const t = index.things.get(p.device); + if (!t || t.kind !== "device") error(file, p.id, `device: "${p.device}" is not a device`); + } + } + + for (const e of house.enclosures) { + if (e.level && !levels.has(e.level)) error("enclosures.yaml", e.id, `Level "${e.level}" is not in settings.levels`); + } + + for (const d of house.devices) { + mustResolve("devices.yaml", d.id, "location", d.location); + if (d.powered_by && !index.circuits.has(d.powered_by)) mustResolve("devices.yaml", d.id, "powered_by", d.powered_by); + if (d.kind === "esp" && !d.mac) warn("devices.yaml", d.id, "No MAC address, so network status cannot be matched"); + } + + for (const c of house.cables) { + mustResolve("cables.yaml", c.id, "from", c.from); + mustResolve("cables.yaml", c.id, "to", c.to); + if (c.from == null || c.to == null) warn("cables.yaml", c.id, "One end of the cable is not documented"); + if (c.type == null) warn("cables.yaml", c.id, "Cable type is unknown"); + for (const segment of c.route ?? []) { + if (!levels.has(segment.level)) error("cables.yaml", c.id, `Route level "${segment.level}" is not in settings.levels`); + } + } + + for (const c of house.circuits) { + for (const [field, ref] of [["breaker", c.breaker], ["rcd", c.rcd]] as const) { + if (ref == null) continue; + const t = index.things.get(ref); + if (!t || t.kind !== "device") error("circuits.yaml", c.id, `${field}: "${ref}" is not a device`); + } + for (const ref of c.feeds) mustResolve("circuits.yaml", c.id, "feeds", ref); + } + + for (const s of house.signals) { + mustResolve("signals.yaml", s.id, "port", s.port); + if (s.port == null) warn("signals.yaml", s.id, "Port is unknown"); + if (s.point != null) { + const t = index.things.get(s.point); + if (!t || t.kind !== "point") error("signals.yaml", s.id, `point: "${s.point}" is not a point`); + } + } + + return issues; +} diff --git a/server/src/cli.ts b/server/src/cli.ts new file mode 100644 index 0000000..b74ff1e --- /dev/null +++ b/server/src/cli.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +import { loadHouse } from "./load.js"; + +const [command, dir] = process.argv.slice(2); + +if (command !== "validate" || !dir) { + console.error("Usage: hadoc validate "); + process.exit(2); +} + +const house = await loadHouse(dir); +const errors = house.issues.filter((i) => i.severity === "error"); +const warnings = house.issues.filter((i) => i.severity === "warning"); + +for (const issue of house.issues) { + const where = issue.id ? `${issue.file} ${issue.id}` : issue.file; + console.log(`${issue.severity === "error" ? "error " : "warning"} ${where}: ${issue.message}`); +} +console.log( + `\n${house.points.length} points, ${house.devices.length} devices, ${house.cables.length} cables, ` + + `${house.circuits.length} circuits, ${house.signals.length} signals. ` + + `${errors.length} errors, ${warnings.length} warnings.`, +); +process.exit(errors.length > 0 ? 1 : 0); diff --git a/server/src/ids.ts b/server/src/ids.ts new file mode 100644 index 0000000..e87099b --- /dev/null +++ b/server/src/ids.ts @@ -0,0 +1,61 @@ +import type { Settings } from "./model.js"; + +export interface IdParts { + level: string; + room: string; + type: string; + nr: string; +} + +const PLACEHOLDERS = ["level", "room", "type", "nr"] as const; + +function escapeRegExp(text: string): string { + return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +/** Builds a matcher for the configured point ID pattern, e.g. "{level}-{room}-{type}-{nr}". */ +export function idScheme(settings: Settings) { + const pattern = settings.ids.pattern; + const digits = settings.ids.nr_digits ?? 2; + + for (const name of PLACEHOLDERS) { + if (!pattern.includes(`{${name}}`)) { + throw new Error(`ID pattern "${pattern}" is missing {${name}}`); + } + } + + const source = pattern + .split(/(\{level\}|\{room\}|\{type\}|\{nr\})/) + .map((part) => { + switch (part) { + case "{level}": + return "(?[A-Za-z0-9]+)"; + case "{room}": + return "(?[A-Za-z0-9]+)"; + case "{type}": + return "(?[A-Za-z0-9]+)"; + case "{nr}": + return `(?\\d{${digits}})`; + default: + return escapeRegExp(part); + } + }) + .join(""); + const regex = new RegExp(`^${source}$`); + + return { + parse(id: string): IdParts | null { + const groups = regex.exec(id)?.groups; + if (!groups) return null; + return { level: groups.level!, room: groups.room!, type: groups.type!, nr: groups.nr! }; + }, + format(parts: Omit & { nr: number | string }): string { + const nr = String(parts.nr).padStart(digits, "0"); + return pattern + .replace("{level}", parts.level) + .replace("{room}", parts.room) + .replace("{type}", parts.type) + .replace("{nr}", nr); + }, + }; +} diff --git a/server/src/index-house.ts b/server/src/index-house.ts new file mode 100644 index 0000000..5b0c7fa --- /dev/null +++ b/server/src/index-house.ts @@ -0,0 +1,53 @@ +import type { Device, Enclosure, House, Point } from "./model.js"; + +export type Thing = + | { kind: "point"; item: Point } + | { kind: "device"; item: Device } + | { kind: "enclosure"; item: Enclosure }; + +export interface Endpoint { + /** The point, device or enclosure the endpoint lands on. */ + thing: Thing; + /** Port name when the endpoint is written as "device:port". */ + port?: string; +} + +export type Resolved = { ok: true; endpoint: Endpoint } | { ok: false; reason: string }; + +/** Lookup tables over a loaded house. Points, devices and enclosures share one ID namespace. */ +export function indexHouse(house: House) { + const things = new Map(); + for (const item of house.points) if (!things.has(item.id)) things.set(item.id, { kind: "point", item }); + for (const item of house.devices) if (!things.has(item.id)) things.set(item.id, { kind: "device", item }); + for (const item of house.enclosures) if (!things.has(item.id)) things.set(item.id, { kind: "enclosure", item }); + + const circuits = new Map(house.circuits.map((c) => [c.id, c])); + + function resolve(ref: string): Resolved { + const colon = ref.indexOf(":"); + if (colon === -1) { + const thing = things.get(ref); + return thing ? { ok: true, endpoint: { thing } } : { ok: false, reason: `"${ref}" does not exist` }; + } + const id = ref.slice(0, colon); + const port = ref.slice(colon + 1); + const thing = things.get(id); + if (!thing) return { ok: false, reason: `"${id}" does not exist` }; + if (thing.kind !== "device") return { ok: false, reason: `"${id}" is not a device, so it has no ports` }; + const ports = thing.item.ports; + if (ports && !ports.some((p) => p.name === port)) { + return { ok: false, reason: `device "${id}" has no port "${port}"` }; + } + return { ok: true, endpoint: { thing, port } }; + } + + /** The ID of the thing an endpoint reference points at, without the port. */ + function thingId(ref: string): string { + const colon = ref.indexOf(":"); + return colon === -1 ? ref : ref.slice(0, colon); + } + + return { things, circuits, resolve, thingId }; +} + +export type HouseIndex = ReturnType; diff --git a/server/src/index.ts b/server/src/index.ts new file mode 100644 index 0000000..38b800b --- /dev/null +++ b/server/src/index.ts @@ -0,0 +1,11 @@ +import { fileURLToPath } from "node:url"; +import { buildApp } from "./app.js"; + +// Data folder: HADOC_DATA, else the first argument, else /data (the Docker volume). +const dataDir = process.env.HADOC_DATA ?? process.argv[2] ?? "/data"; +const webDir = process.env.HADOC_WEB ?? fileURLToPath(new URL("../../web/dist/", import.meta.url)); +const port = Number(process.env.PORT ?? 8080); +const host = process.env.HOST ?? "0.0.0.0"; + +const app = await buildApp({ dataDir, webDir, watch: true, logger: true }); +await app.listen({ port, host }); diff --git a/server/src/load.ts b/server/src/load.ts new file mode 100644 index 0000000..a5b927a --- /dev/null +++ b/server/src/load.ts @@ -0,0 +1,111 @@ +import { readFile, readdir } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { Ajv2020, type ValidateFunction } from "ajv/dist/2020.js"; +import { parse as parseYaml } from "yaml"; +import { checkHouse } from "./checks.js"; +import type { House, Issue } from "./model.js"; + +const DEFAULT_SCHEMA_DIR = fileURLToPath(new URL("../../schema/", import.meta.url)); + +const FILES = { + settings: "settings.yaml", + site: "site.yaml", + enclosures: "enclosures.yaml", + devices: "devices.yaml", + cables: "cables.yaml", + circuits: "circuits.yaml", + signals: "signals.yaml", +} as const; + +type FileKey = keyof typeof FILES; +const REQUIRED: FileKey[] = ["settings", "site"]; + +let validators: Map | undefined; + +async function loadValidators(schemaDir: string): Promise> { + if (validators) return validators; + const ajv = new Ajv2020({ allErrors: true, strict: false }); + const map = new Map(); + for (const name of [...Object.keys(FILES), "points"]) { + const schema = JSON.parse(await readFile(path.join(schemaDir, `${name}.schema.json`), "utf8")); + map.set(name, ajv.compile(schema)); + } + validators = map; + return map; +} + +async function readYaml(file: string, issues: Issue[], label: string): Promise { + try { + return parseYaml(await readFile(file, "utf8")) ?? null; + } catch (err) { + issues.push({ severity: "error", file: label, message: `Cannot parse YAML: ${(err as Error).message}` }); + return undefined; + } +} + +function validate( + validator: ValidateFunction, + data: unknown, + label: string, + issues: Issue[], +): boolean { + if (validator(data)) return true; + for (const err of validator.errors ?? []) { + issues.push({ + severity: "error", + file: label, + message: `${err.instancePath || "/"} ${err.message ?? "is invalid"}`, + }); + } + return false; +} + +/** + * Loads a HAdoc data folder. Never throws on bad data: every problem lands in + * `house.issues`, so the app can still show what is valid. + */ +export async function loadHouse(dataDir: string, schemaDir = DEFAULT_SCHEMA_DIR): Promise { + const v = await loadValidators(schemaDir); + const issues: Issue[] = []; + const raw: Partial> = {}; + + for (const [key, file] of Object.entries(FILES) as [FileKey, string][]) { + const full = path.join(dataDir, file); + if (!existsSync(full)) { + if (REQUIRED.includes(key)) issues.push({ severity: "error", file, message: "File is missing" }); + continue; + } + const data = await readYaml(full, issues, file); + if (data !== undefined && validate(v.get(key)!, data, file, issues)) raw[key] = data; + } + + const points: House["points"] = []; + const pointsDir = path.join(dataDir, "points"); + if (existsSync(pointsDir)) { + const files = (await readdir(pointsDir)).filter((f) => /\.ya?ml$/.test(f)).sort(); + for (const f of files) { + const label = `points/${f}`; + const data = await readYaml(path.join(pointsDir, f), issues, label); + if (data === null) continue; + if (data !== undefined && validate(v.get("points")!, data, label, issues)) { + points.push(...(data as House["points"])); + } + } + } + + const house: House = { + settings: (raw.settings as House["settings"]) ?? { ids: { pattern: "{level}-{room}-{type}-{nr}" }, levels: [], types: [] }, + site: (raw.site as House["site"]) ?? { floors: [] }, + points, + enclosures: (raw.enclosures as House["enclosures"]) ?? [], + devices: (raw.devices as House["devices"]) ?? [], + cables: (raw.cables as House["cables"]) ?? [], + circuits: (raw.circuits as House["circuits"]) ?? [], + signals: (raw.signals as House["signals"]) ?? [], + issues, + }; + house.issues.push(...checkHouse(house)); + return house; +} diff --git a/server/src/model.ts b/server/src/model.ts new file mode 100644 index 0000000..c8d1555 --- /dev/null +++ b/server/src/model.ts @@ -0,0 +1,128 @@ +// Types mirroring the JSON Schemas in /schema. The schemas are the source of truth; +// keep these in sync when a schema changes. + +export type XY = [number, number]; + +export interface Settings { + name?: string; + language?: string; + ids: { pattern: string; nr_digits?: number }; + levels: { code: string; name: string }[]; + types: { code: string; name: string; symbol?: string; layer?: Layer }[]; +} + +export type Layer = "power" | "data" | "automation" | "hidden"; + +export interface Room { + code: string; + name: string; + outline: XY[]; +} + +export interface Floor { + level: string; + width: number; + height: number; + underlay?: { file: string; x?: number; y?: number; width?: number; height?: number; opacity?: number }; + rooms: Room[]; +} + +export interface Site { + floors: Floor[]; +} + +export interface Point { + id: string; + type: string; + level: string; + room: string; + name?: string; + pos?: XY | null; + height?: number | null; + photo?: string; + notes?: string; + if_broken?: string; + device?: string; +} + +export interface Enclosure { + id: string; + name: string; + level?: string | null; + room?: string | null; + pos?: XY | null; + rails?: number; + photo?: string; + notes?: string; +} + +export interface Port { + name: string; + signal?: string; +} + +export interface Device { + id: string; + kind: string; + model?: string | null; + label?: string; + location?: string; + firmware?: string | null; + hostname?: string | null; + mac?: string | null; + powered_by?: string | null; + ports?: Port[]; + notes?: string; +} + +export interface Cable { + id: string; + type?: string | null; + length?: number | null; + from: string | null; + to: string | null; + cores?: { colour: string; from?: string | null; to?: string | null }[]; + route?: { level: string; path: XY[] }[]; + notes?: string; +} + +export interface Circuit { + id: string; + name?: string; + breaker?: string | null; + rating?: string | null; + rcd?: string | null; + phase?: string | null; + feeds: string[]; +} + +export interface Signal { + id: string; + name?: string; + port: string | null; + point?: string | null; + plc?: { name?: string | null; address?: string | null; modbus?: string | null }; + mqtt?: string | null; + openhab?: { thing?: string | null; item?: string | null }; + logic?: "plc" | "openhab" | "both" | "none" | null; + notes?: string; +} + +export interface Issue { + severity: "error" | "warning"; + file: string; + id?: string; + message: string; +} + +export interface House { + settings: Settings; + site: Site; + points: Point[]; + enclosures: Enclosure[]; + devices: Device[]; + cables: Cable[]; + circuits: Circuit[]; + signals: Signal[]; + issues: Issue[]; +} diff --git a/server/src/trace.ts b/server/src/trace.ts new file mode 100644 index 0000000..9c43282 --- /dev/null +++ b/server/src/trace.ts @@ -0,0 +1,115 @@ +import type { HouseIndex, Thing } from "./index-house.js"; +import type { Cable, Circuit, House, Signal } from "./model.js"; + +export interface ThingRef { + kind: Thing["kind"] | "circuit" | "unknown"; + id: string; + label: string; + port?: string; +} + +export interface CableEnd { + cable: Cable; + /** The end of the cable at the subject. */ + here: string; + /** The other end, or null when it is not documented. */ + there: ThingRef | null; +} + +export interface Trace { + subject: ThingRef; + location: ThingRef | null; + /** Devices that sit at this point, e.g. a hidden ESP. */ + contains: ThingRef[]; + /** Power path upwards: the device or circuit that feeds the subject, and what feeds that. */ + poweredBy: ThingRef[]; + circuits: Circuit[]; + cables: CableEnd[]; + signals: Signal[]; +} + +function label(thing: Thing): string { + switch (thing.kind) { + case "point": + return thing.item.name ?? thing.item.id; + case "device": + return [thing.item.label, thing.item.model].filter(Boolean).join(" · ") || thing.item.id; + case "enclosure": + return thing.item.name; + } +} + +function refFor(index: HouseIndex, ref: string): ThingRef { + const circuit = index.circuits.get(ref); + if (circuit) return { kind: "circuit", id: circuit.id, label: circuit.name ?? circuit.id }; + const r = index.resolve(ref); + if (!r.ok) return { kind: "unknown", id: ref, label: ref }; + const { thing, port } = r.endpoint; + return { kind: thing.kind, id: thing.item.id, label: label(thing), port }; +} + +/** Everything the app shows when someone taps a point, device or enclosure. */ +export function traceThing(house: House, index: HouseIndex, id: string): Trace | null { + const subject = index.things.get(id); + if (!subject) return null; + + // The subject plus the devices that physically sit in it count as "here". + const here = new Set([id]); + const contains: ThingRef[] = []; + if (subject.kind === "point" && subject.item.device) here.add(subject.item.device); + for (const d of house.devices) { + if (d.location === id || here.has(d.id)) here.add(d.id); + } + for (const hereId of here) { + if (hereId === id) continue; + contains.push(refFor(index, hereId)); + } + + const location = + subject.kind === "device" && subject.item.location ? refFor(index, subject.item.location) : null; + + // Follow powered_by upwards, guarding against loops in hand-written data. + const poweredBy: ThingRef[] = []; + const visited = new Set(); + let next: string | null | undefined; + for (const h of here) { + const t = index.things.get(h); + if (t?.kind === "device" && t.item.powered_by) { + next = t.item.powered_by; + break; + } + } + while (next && !visited.has(next)) { + visited.add(next); + const ref = refFor(index, next); + poweredBy.push(ref); + const t = index.things.get(index.thingId(next)); + next = t?.kind === "device" ? t.item.powered_by : undefined; + } + + const touches = (ref: string | null | undefined) => ref != null && here.has(index.thingId(ref)); + // A circuit counts when it feeds the subject directly or anything up its power path. + const feedsHere = (c: Circuit) => + c.feeds.some(touches) || + poweredBy.some((p) => (p.kind === "circuit" ? p.id === c.id : c.feeds.includes(p.id))); + + const circuits = house.circuits.filter(feedsHere); + + const cables: CableEnd[] = []; + for (const cable of house.cables) { + if (touches(cable.from)) cables.push({ cable, here: cable.from!, there: cable.to ? refFor(index, cable.to) : null }); + else if (touches(cable.to)) cables.push({ cable, here: cable.to!, there: cable.from ? refFor(index, cable.from) : null }); + } + + const signals = house.signals.filter((s) => (s.point != null && here.has(s.point)) || touches(s.port)); + + return { + subject: { kind: subject.kind, id, label: label(subject) }, + location, + contains, + poweredBy, + circuits, + cables, + signals, + }; +} diff --git a/server/test/house.test.ts b/server/test/house.test.ts new file mode 100644 index 0000000..ee93f35 --- /dev/null +++ b/server/test/house.test.ts @@ -0,0 +1,117 @@ +import { cp, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { buildApp } from "../src/app.js"; +import { idScheme } from "../src/ids.js"; +import { indexHouse } from "../src/index-house.js"; +import { loadHouse } from "../src/load.js"; +import { traceThing } from "../src/trace.js"; + +const DEMO = fileURLToPath(new URL("../../examples/demo-house/", import.meta.url)); + +async function demoCopy(): Promise { + const dir = await mkdtemp(path.join(tmpdir(), "hadoc-")); + await cp(DEMO, dir, { recursive: true }); + return dir; +} + +describe("id scheme", () => { + const scheme = idScheme({ + ids: { pattern: "{level}-{room}-{type}-{nr}", nr_digits: 2 }, + levels: [], + types: [], + }); + + it("parses and formats", () => { + expect(scheme.parse("MIN1-GAR-SW-03")).toEqual({ level: "MIN1", room: "GAR", type: "SW", nr: "03" }); + expect(scheme.format({ level: "1", room: "DRS", type: "PIR", nr: 1 })).toBe("1-DRS-PIR-01"); + }); + + it("rejects IDs that do not follow the pattern", () => { + expect(scheme.parse("1-DRS-PIR-1")).toBeNull(); + expect(scheme.parse("DRS-PIR-01")).toBeNull(); + }); + + it("supports a custom pattern", () => { + const custom = idScheme({ ids: { pattern: "{room}.{type}{nr}@{level}", nr_digits: 3 }, levels: [], types: [] }); + expect(custom.parse("KIT.SKT007@0")).toEqual({ level: "0", room: "KIT", type: "SKT", nr: "007" }); + }); +}); + +describe("demo house", () => { + it("loads without errors", async () => { + const house = await loadHouse(DEMO); + const errors = house.issues.filter((i) => i.severity === "error"); + expect(errors).toEqual([]); + expect(house.points.length).toBeGreaterThan(0); + }); + + it("warns about what is still undocumented", async () => { + const house = await loadHouse(DEMO); + const warnings = house.issues.filter((i) => i.severity === "warning").map((i) => `${i.id}: ${i.message}`); + expect(warnings).toContain("K-0107: Cable type is unknown"); + expect(warnings).toContain("esp-dressing: No MAC address, so network status cannot be matched"); + }); + + it("traces the dressing room motion sensor to its ESP, power and signal", async () => { + const house = await loadHouse(DEMO); + const trace = traceThing(house, indexHouse(house), "1-DRS-PIR-01")!; + + expect(trace.cables.map((c) => [c.cable.id, c.there?.id, c.there?.port])).toEqual([ + ["K-0107", "esp-dressing", "GPIO14"], + ]); + expect(trace.signals.map((s) => s.openhab?.item)).toEqual(["Dressing_Motion"]); + }); + + it("follows the power path of a hidden device", async () => { + const house = await loadHouse(DEMO); + const trace = traceThing(house, indexHouse(house), "1-DRS-ESP-01")!; + + expect(trace.contains.map((c) => c.id)).toEqual(["esp-dressing"]); + expect(trace.poweredBy.map((p) => p.id)).toEqual(["sonoff-mirror"]); + expect(trace.circuits.map((c) => c.id)).toEqual(["F3"]); + }); +}); + +describe("checks", () => { + it("reports broken references and IDs that contradict their fields", async () => { + const dir = await demoCopy(); + const cables = path.join(dir, "cables.yaml"); + await writeFile(cables, (await readFile(cables, "utf8")).replace('"plc-di01:DI 01.1"', '"plc-di01:DI 09.9"')); + const points = path.join(dir, "points/0-ground-floor.yaml"); + await writeFile(points, (await readFile(points, "utf8")).replace("id: 0-KIT-SKT-01", "id: 0-HAL-SKT-01")); + + const house = await loadHouse(dir); + const errors = house.issues.filter((i) => i.severity === "error").map((i) => `${i.id}: ${i.message}`); + expect(errors).toContain('K-0012: to: device "plc-di01" has no port "DI 09.9"'); + expect(errors).toContain("0-HAL-SKT-01: ID says room HAL, but room is KIT"); + }); + + it("reports schema errors with the file name", async () => { + const dir = await demoCopy(); + await writeFile(path.join(dir, "circuits.yaml"), "- id: F9\n"); + + const house = await loadHouse(dir); + expect(house.issues).toContainEqual( + expect.objectContaining({ severity: "error", file: "circuits.yaml", message: "/0 must have required property 'feeds'" }), + ); + }); +}); + +describe("api", () => { + it("serves the house and a trace", async () => { + const app = await buildApp({ dataDir: DEMO }); + const house = await app.inject({ url: "/api/house" }); + expect(house.statusCode).toBe(200); + expect(house.json().issueCount.errors).toBe(0); + + const trace = await app.inject({ url: "/api/trace/0-HAL-SW-01" }); + expect(trace.json().signals[0].plc).toEqual({ name: "SB_0_1L", address: "%IX4.0" }); + + const missing = await app.inject({ url: "/api/trace/nope" }); + expect(missing.statusCode).toBe(404); + await app.close(); + }); +}); diff --git a/server/tsconfig.build.json b/server/tsconfig.build.json new file mode 100644 index 0000000..f3b4122 --- /dev/null +++ b/server/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { "rootDir": "src" }, + "include": ["src"] +} diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 0000000..1b93bb0 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["src", "test"] +} diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..4b27c6c --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "noUncheckedIndexedAccess": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true + } +} diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..cf2a6bf --- /dev/null +++ b/web/index.html @@ -0,0 +1,15 @@ + + + + + + + + + HAdoc + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..86383b7 --- /dev/null +++ b/web/package.json @@ -0,0 +1,20 @@ +{ + "name": "@hadoc/web", + "version": "0.1.0", + "private": true, + "type": "module", + "license": "MIT", + "scripts": { + "dev": "vite", + "build": "vite build", + "check": "svelte-check --tsconfig ./tsconfig.json" + }, + "devDependencies": { + "@sveltejs/vite-plugin-svelte": "^7.3.1", + "@tsconfig/svelte": "^5.0.8", + "svelte": "^5.57.2", + "svelte-check": "^4.7.6", + "typescript": "^6.0.3", + "vite": "^8.3.4" + } +} diff --git a/web/public/icon.svg b/web/public/icon.svg new file mode 100644 index 0000000..a380acf --- /dev/null +++ b/web/public/icon.svg @@ -0,0 +1 @@ + diff --git a/web/public/manifest.webmanifest b/web/public/manifest.webmanifest new file mode 100644 index 0000000..f96f056 --- /dev/null +++ b/web/public/manifest.webmanifest @@ -0,0 +1,10 @@ +{ + "name": "HAdoc", + "short_name": "HAdoc", + "description": "Home automation documentation: what is this thing, how is it wired, what controls it.", + "start_url": "/", + "display": "standalone", + "background_color": "#ffffff", + "theme_color": "#1f6feb", + "icons": [{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }] +} diff --git a/web/public/sw.js b/web/public/sw.js new file mode 100644 index 0000000..07d8dfc --- /dev/null +++ b/web/public/sw.js @@ -0,0 +1,22 @@ +// Network first, falling back to the last copy we saw, so the plan still opens +// when the server is down. +const CACHE = "hadoc-v1"; + +self.addEventListener("install", () => self.skipWaiting()); +self.addEventListener("activate", (event) => event.waitUntil(self.clients.claim())); + +self.addEventListener("fetch", (event) => { + const { request } = event; + if (request.method !== "GET" || new URL(request.url).origin !== self.location.origin) return; + event.respondWith( + fetch(request) + .then((response) => { + if (response.ok) { + const copy = response.clone(); + caches.open(CACHE).then((cache) => cache.put(request, copy)); + } + return response; + }) + .catch(async () => (await caches.match(request)) ?? (await caches.match("/")) ?? Response.error()), + ); +}); diff --git a/web/src/App.svelte b/web/src/App.svelte new file mode 100644 index 0000000..32110ed --- /dev/null +++ b/web/src/App.svelte @@ -0,0 +1,321 @@ + + +
+
+

{house?.settings.name ?? "HAdoc"}

+ + {#if house} + + + + + + {/if} +
+ +
+ {#if error} +

Cannot load the house data: {error}

+ {:else if house && floor} +
+ +
+ {#each LAYERS as [key, name] (key)} + + {/each} +
+
+ {:else if house} +

No floors yet. Add one to site.yaml.

+ {:else} +

Loading…

+ {/if} + + {#if house && selected} +
select(null)} /> + {:else if showIssues} + (showIssues = false)} /> + {/if} +
+
+ + diff --git a/web/src/app.css b/web/src/app.css new file mode 100644 index 0000000..8876fef --- /dev/null +++ b/web/src/app.css @@ -0,0 +1,62 @@ +:root { + --bg: #ffffff; + --surface: #f6f7f9; + --text: #1b1f24; + --muted: #5b6470; + --border: #d6dae0; + --accent: #1f6feb; + --accent-soft: rgba(31, 111, 235, 0.12); + --room: #eef1f5; + --room-stroke: #9aa3ad; + --layer-power: #c2410c; + --layer-automation: #1f6feb; + --layer-data: #0f766e; + --layer-hidden: #7c3aed; + --cable: #64748b; + --error: #b91c1c; + --warning: #a16207; + color-scheme: light dark; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #111418; + --surface: #1a1f25; + --text: #e6e9ed; + --muted: #9aa4af; + --border: #2d343c; + --accent: #4c8dff; + --accent-soft: rgba(76, 141, 255, 0.18); + --room: #1d232a; + --room-stroke: #4b5561; + --layer-power: #fb923c; + --layer-automation: #4c8dff; + --layer-data: #2dd4bf; + --layer-hidden: #a78bfa; + --cable: #94a3b8; + --error: #f87171; + --warning: #facc15; + } +} + +* { + box-sizing: border-box; +} + +html, +body, +#app { + height: 100%; + margin: 0; +} + +body { + font: 15px/1.45 system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; + background: var(--bg); + color: var(--text); +} + +button { + font: inherit; + color: inherit; +} diff --git a/web/src/lib/Details.svelte b/web/src/lib/Details.svelte new file mode 100644 index 0000000..0a1f6d1 --- /dev/null +++ b/web/src/lib/Details.svelte @@ -0,0 +1,259 @@ + + +{#snippet link(ref: ThingRef | null)} + {#if ref === null} + not documented + {:else if canOpen(ref)} + {#if ref.port}{ref.port}{/if} + {:else} + {ref.label} + {/if} +{/snippet} + + + + diff --git a/web/src/lib/FloorPlan.svelte b/web/src/lib/FloorPlan.svelte new file mode 100644 index 0000000..42d9304 --- /dev/null +++ b/web/src/lib/FloorPlan.svelte @@ -0,0 +1,227 @@ + + + pointers.delete(e.pointerId)} +> + {#if floor.underlay} + + {/if} + + {#each floor.rooms as room (room.code)} + {@const [cx, cy] = centroid(room.outline)} + + p.join(",")).join(" ")} /> + {room.name} + + {/each} + + {#each routes as route, i (`${route.id}-${i}`)} + p.join(",")).join(" ")} + stroke-width={r / (highlight.has(route.id) ? 3 : 5)} + /> + {/each} + + {#each visiblePoints as p (p.id)} + {@const info = typeInfo.get(p.type)} + + {p.name ?? p.id} + + {info?.symbol ?? p.type} + + {/each} + + + diff --git a/web/src/lib/Issues.svelte b/web/src/lib/Issues.svelte new file mode 100644 index 0000000..3f0bd3d --- /dev/null +++ b/web/src/lib/Issues.svelte @@ -0,0 +1,87 @@ + + + + + diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts new file mode 100644 index 0000000..cb8e33f --- /dev/null +++ b/web/src/lib/api.ts @@ -0,0 +1,19 @@ +import type { House, Issue } from "../../../server/src/model.ts"; +import type { Trace } from "../../../server/src/trace.ts"; + +export type { Cable, Device, Floor, House, Issue, Point, Settings } from "../../../server/src/model.ts"; +export type { Trace, ThingRef } from "../../../server/src/trace.ts"; + +export type HouseData = Omit & { issueCount: { errors: number; warnings: number } }; + +async function getJson(url: string): Promise { + const res = await fetch(url); + if (!res.ok) throw new Error(`${url}: ${res.status} ${res.statusText}`); + return res.json() as Promise; +} + +export const api = { + house: () => getJson("/api/house"), + issues: () => getJson("/api/issues"), + trace: (id: string) => getJson(`/api/trace/${encodeURIComponent(id)}`), +}; diff --git a/web/src/main.ts b/web/src/main.ts new file mode 100644 index 0000000..baddb71 --- /dev/null +++ b/web/src/main.ts @@ -0,0 +1,9 @@ +import { mount } from "svelte"; +import App from "./App.svelte"; +import "./app.css"; + +mount(App, { target: document.getElementById("app")! }); + +if ("serviceWorker" in navigator && import.meta.env.PROD) { + void navigator.serviceWorker.register("/sw.js"); +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..ac23b6d --- /dev/null +++ b/web/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUncheckedIndexedAccess": true, + "verbatimModuleSyntax": true, + "isolatedModules": true, + "skipLibCheck": true, + "types": [ + "vite/client" + ], + "allowImportingTsExtensions": true, + "noEmit": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.svelte" + ] +} diff --git a/web/vite.config.ts b/web/vite.config.ts new file mode 100644 index 0000000..c85f57b --- /dev/null +++ b/web/vite.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from "vite"; +import { svelte } from "@sveltejs/vite-plugin-svelte"; + +const api = process.env.HADOC_API ?? "http://localhost:8080"; + +export default defineConfig({ + plugins: [svelte()], + server: { + proxy: { + "/api": api, + "/plans": api, + "/photos": api, + }, + }, +});