From 53fa112aa8dc56f1b52bd5674c9fc255771d8822 Mon Sep 17 00:00:00 2001 From: Willem Serruys Date: Wed, 19 Aug 2026 18:35:47 +0200 Subject: [PATCH] fix: added energy-management --- energy-management/.env.example | 22 + energy-management/.gitignore | 9 + energy-management/Dockerfile | 27 + energy-management/README.md | 57 + energy-management/backend/data/energy.db-shm | Bin 0 -> 32768 bytes energy-management/backend/data/energy.db-wal | Bin 0 -> 2566792 bytes energy-management/backend/package.json | 29 + .../backend/src/__tests__/loop.test.ts | 133 + .../backend/src/__tests__/quarterHour.test.ts | 72 + energy-management/backend/src/api/routes.ts | 80 + energy-management/backend/src/control/loop.ts | 100 + .../backend/src/control/quarterHour.ts | 54 + .../backend/src/control/runner.ts | 88 + energy-management/backend/src/db/index.ts | 72 + energy-management/backend/src/db/store.ts | 140 + energy-management/backend/src/env.ts | 37 + .../backend/src/homeassistant/client.ts | 56 + .../backend/src/homeassistant/devices.ts | 87 + energy-management/backend/src/index.ts | 53 + .../backend/src/influx/client.ts | 7 + energy-management/backend/src/influx/power.ts | 51 + energy-management/backend/src/types.ts | 47 + energy-management/backend/tsconfig.json | 16 + energy-management/docker-compose.yml | 15 + energy-management/frontend/index.html | 12 + energy-management/frontend/package.json | 21 + energy-management/frontend/src/App.tsx | 23 + energy-management/frontend/src/Dashboard.tsx | 136 + energy-management/frontend/src/Devices.tsx | 247 ++ .../frontend/src/PeakHistoryChart.tsx | 114 + energy-management/frontend/src/api.ts | 71 + energy-management/frontend/src/main.tsx | 10 + energy-management/frontend/src/theme.css | 260 ++ energy-management/frontend/tsconfig.json | 16 + .../frontend/tsconfig.tsbuildinfo | 1 + energy-management/frontend/vite.config.ts | 11 + energy-management/package-lock.json | 3872 +++++++++++++++++ energy-management/package.json | 14 + 38 files changed, 6060 insertions(+) create mode 100644 energy-management/.env.example create mode 100644 energy-management/.gitignore create mode 100644 energy-management/Dockerfile create mode 100644 energy-management/README.md create mode 100644 energy-management/backend/data/energy.db-shm create mode 100644 energy-management/backend/data/energy.db-wal create mode 100644 energy-management/backend/package.json create mode 100644 energy-management/backend/src/__tests__/loop.test.ts create mode 100644 energy-management/backend/src/__tests__/quarterHour.test.ts create mode 100644 energy-management/backend/src/api/routes.ts create mode 100644 energy-management/backend/src/control/loop.ts create mode 100644 energy-management/backend/src/control/quarterHour.ts create mode 100644 energy-management/backend/src/control/runner.ts create mode 100644 energy-management/backend/src/db/index.ts create mode 100644 energy-management/backend/src/db/store.ts create mode 100644 energy-management/backend/src/env.ts create mode 100644 energy-management/backend/src/homeassistant/client.ts create mode 100644 energy-management/backend/src/homeassistant/devices.ts create mode 100644 energy-management/backend/src/index.ts create mode 100644 energy-management/backend/src/influx/client.ts create mode 100644 energy-management/backend/src/influx/power.ts create mode 100644 energy-management/backend/src/types.ts create mode 100644 energy-management/backend/tsconfig.json create mode 100644 energy-management/docker-compose.yml create mode 100644 energy-management/frontend/index.html create mode 100644 energy-management/frontend/package.json create mode 100644 energy-management/frontend/src/App.tsx create mode 100644 energy-management/frontend/src/Dashboard.tsx create mode 100644 energy-management/frontend/src/Devices.tsx create mode 100644 energy-management/frontend/src/PeakHistoryChart.tsx create mode 100644 energy-management/frontend/src/api.ts create mode 100644 energy-management/frontend/src/main.tsx create mode 100644 energy-management/frontend/src/theme.css create mode 100644 energy-management/frontend/tsconfig.json create mode 100644 energy-management/frontend/tsconfig.tsbuildinfo create mode 100644 energy-management/frontend/vite.config.ts create mode 100644 energy-management/package-lock.json create mode 100644 energy-management/package.json diff --git a/energy-management/.env.example b/energy-management/.env.example new file mode 100644 index 0000000..0d8d6b6 --- /dev/null +++ b/energy-management/.env.example @@ -0,0 +1,22 @@ +# InfluxDB 2.x +INFLUX_URL=http://influxdb.local:8086 +INFLUX_TOKEN=replace-with-a-read-scoped-token +INFLUX_ORG=your-org +INFLUX_BUCKET=your-bucket +# Measurement/field that holds live power draw for the whole house +INFLUX_MEASUREMENT=power +INFLUX_FIELD=value +# Unit of INFLUX_FIELD as stored in Influx: W or kW +INFLUX_FIELD_UNIT=W + +# Home Assistant +HA_URL=http://homeassistant.local:8123 +HA_TOKEN=replace-with-a-long-lived-access-token + +# Capacity tariff target: keep every 15-minute block's average under this many kW +TARGET_KW=5 + +# Optional +PORT=3000 +DB_PATH=./data/energy.db +CONTROL_LOOP_INTERVAL_MS=10000 diff --git a/energy-management/.gitignore b/energy-management/.gitignore new file mode 100644 index 0000000..c42f881 --- /dev/null +++ b/energy-management/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +dist/ +build/ +*.db +*.db-journal +.env +.env.local +*.log +.DS_Store diff --git a/energy-management/Dockerfile b/energy-management/Dockerfile new file mode 100644 index 0000000..1a12483 --- /dev/null +++ b/energy-management/Dockerfile @@ -0,0 +1,27 @@ +FROM node:22-slim AS build +RUN apt-get update && apt-get install -y --no-install-recommends python3 make g++ \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /repo +COPY package.json ./ +COPY backend/package.json backend/package.json +COPY frontend/package.json frontend/package.json +RUN npm install + +COPY backend backend +COPY frontend frontend +RUN npm run build + +FROM node:22-slim AS runtime +WORKDIR /app +ENV NODE_ENV=production + +COPY --from=build /repo/package.json ./package.json +COPY --from=build /repo/backend/package.json ./backend/package.json +COPY --from=build /repo/node_modules ./node_modules +COPY --from=build /repo/backend/node_modules ./backend/node_modules +COPY --from=build /repo/backend/dist ./backend/dist +COPY --from=build /repo/frontend/dist ./backend/dist/public + +VOLUME ["/app/data"] +EXPOSE 3000 +CMD ["node", "backend/dist/index.js"] diff --git a/energy-management/README.md b/energy-management/README.md new file mode 100644 index 0000000..5c337c7 --- /dev/null +++ b/energy-management/README.md @@ -0,0 +1,57 @@ +# Peak Power Manager + +Keeps your electricity capacity tariff in check by watching live power draw +(from InfluxDB) and shedding/restoring large consumers (via Home Assistant) +whenever the current 15-minute billing block is projected to exceed a target. + +## How it works + +Belgian-style capacity tariffs bill on the highest 15-minute average power of +the month. Every `CONTROL_LOOP_INTERVAL_MS` (default 10s), the app: + +1. Reads recent power samples for the current quarter-hour block from InfluxDB. +2. Projects what that block's final average will be if current draw continues. +3. If the projection exceeds `TARGET_KW`, it sheds the lowest-priority + configured device (turns a switch off, or lowers a number set-point to its + floor) via Home Assistant. +4. Once the projection is comfortably back under target, it restores the + highest-priority shed device. + +A minimum dwell time per device (configurable) prevents rapid on/off cycling. +Devices with "manual override" enabled are left alone by the control loop. + +## Setup + +1. Copy `.env.example` to `.env` and fill in your InfluxDB and Home Assistant + connection details, the measurement/field that holds live power, and your + target kW. +2. `docker compose up --build` +3. Open `http://localhost:3000`, go to the **Devices** tab, and add your EV + charger, heat pump, and any other large consumers with their Home + Assistant entity IDs, control type (switch or number), and priority + (lower priority number = shed first). + +## Local development + +``` +npm install +npm run dev:backend # Fastify API on :3000 +npm run dev:frontend # Vite dev server on :5173, proxies /api to :3000 +npm test # backend unit tests (quarter-hour projection, control decisions) +``` + +## Architecture + +- `backend/src/influx` — queries live power samples from InfluxDB (Flux). +- `backend/src/control/quarterHour.ts` — pure logic computing the running and + projected average power for the current 15-minute block. +- `backend/src/control/loop.ts` — pure decision logic: which device to shed + or restore given the projection and device states. +- `backend/src/control/runner.ts` — orchestrates the above against real + InfluxDB/Home Assistant/SQLite on each tick. +- `backend/src/homeassistant` — Home Assistant REST client and device + shed/restore abstraction. +- `backend/src/db` — SQLite config store (devices, target, action log, block + history). +- `frontend/` — React dashboard: live status, device list, 30-day peak + history chart, recent actions log, and a Devices config page. diff --git a/energy-management/backend/data/energy.db-shm b/energy-management/backend/data/energy.db-shm new file mode 100644 index 0000000000000000000000000000000000000000..80befa6c3f5128c95db94f5c6f7577ec83c774e9 GIT binary patch literal 32768 zcmeI)b&!=+7{~G7_t|CXuBE$Kx|U|??(XgdR6+p>3l&7dphF3RP*MqzP*7U21tkOn zlu#+fdmZk~j(<3#i!-eAoVn-TbDn$N=Y8(yoSFOI8+kHH!8Qje@Jchxq$GiP%G_t_ zHeMMsogL9JBMP>ivixwjQ!~0RnjQHM*Z-M3@D>}$dn-;0#gE+{1n#ZC5`_hNc%TR8 zi2^<1_BiqF&tPqF

o9*oDmJ~R*J3SJwW|5rV34gb|W;^qzg&bYC=c^s-YxEG;u z+Q3%Gy_xPS||fD5>Q3%Gy_xPS||fD5>Q3%Gy_xPS||fD5>Q z3%Gy_xPS||fD5>Q3%Gy_xPS||fD5>Q3%Gy_xPS||fD5>Q3%Gy_+#vx1&yq-@WJ;yf zN~2OLeTVXT8W*_x1x)FbNli3HV>M9^Yrd9hg;wh0yT5d=&;{Zt5T^8Ms{1ugle9oD z#50>;?g9x(AY2)gSr2Hk9?^^XBq5dVwYoq&1tOF}Gvb-UFL!~2DUetx6K3IFwF|^s zAgLnts6LH%9#7x`2~Qxo(ki2}D66t5yK*R}aw)g+D6jG(Z)u0#)=urxZtc-KdRKe3Pw(k{?biVv z)FB<#5gpYB`cTL8kv`Ms`a)moxK8LReXWx^rPDg2Z**4Y^sTxORXH~p?#frlA`5k@$Ph#)aZNJ=u2lY*3_A~k79OFGh%fsAA# zGm&H=E7{0S4sw!<+~grI`N&TJ3Q~x|6rm`^C{7e5C`l0BFk9L3RdzGFY^ko zvWnHLVJ)xmI_p@^1~#&Z&Ah>zY+)(_^IYIZe&QmRxXcxP z<`;hDD%ZHq4Q}!qzjF&2W)^1QmdGM3u_dvjmduh{3QK9JEVZSvw3g1&TL#N$nJlwK zS{BP{*(|%|u$-35a$6qDYxyj{6|jO<$O>B#D{958xJ6kBD`};yw3V^4R?f;>1*>S4 zEZQnt6{~91th&{(npVqVthUv$x>nEXTLWuojjXXXv8L9{np+ENX|1fawXwF=&e~fC z>u8;CzShtB+W;GAgKV%3v7r`g!KaIRa{(7{0T*xq7jOX= vZ~+%^0T*xq7jOX=Z~+%^0T*xq7jOX=Z~+%^0T*xq7jOX=Z~+&%Qv!bggT7MZ literal 0 HcmV?d00001 diff --git a/energy-management/backend/data/energy.db-wal b/energy-management/backend/data/energy.db-wal new file mode 100644 index 0000000000000000000000000000000000000000..c3dbc338e09fc026edc5042b7b9c05e8400ed401 GIT binary patch literal 2566792 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

~v00Iag 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⋘Qj0(^-80RjXF5FkK+009C7VipLx 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 R0u#%heB@xO14-)w{{cKz6hr_3 literal 0 HcmV?d00001 diff --git a/energy-management/backend/package.json b/energy-management/backend/package.json new file mode 100644 index 0000000..5552370 --- /dev/null +++ b/energy-management/backend/package.json @@ -0,0 +1,29 @@ +{ + "name": "backend", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc -p tsconfig.json", + "start": "node dist/index.js", + "test": "vitest run" + }, + "dependencies": { + "@fastify/cors": "^10.0.1", + "@fastify/static": "^10.1.3", + "@influxdata/influxdb-client": "^1.35.0", + "better-sqlite3": "^11.8.1", + "dotenv": "^17.4.2", + "fastify": "^5.2.1", + "pino": "^9.6.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.7", + "pino-pretty": "^13.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vitest": "^3.0.4" + } +} diff --git a/energy-management/backend/src/__tests__/loop.test.ts b/energy-management/backend/src/__tests__/loop.test.ts new file mode 100644 index 0000000..cfa4c21 --- /dev/null +++ b/energy-management/backend/src/__tests__/loop.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from "vitest"; +import { decideControlAction } from "../control/loop.js"; +import type { DeviceStatus, QuarterHourStatus } from "../types.js"; + +function makeStatus(overrides: Partial = {}): QuarterHourStatus { + return { + blockStartMs: 0, + blockEndMs: 900_000, + elapsedFractionOfBlock: 0.5, + runningAverageW: 4000, + projectedAverageW: 4000, + targetW: 5000, + overTarget: false, + ...overrides, + }; +} + +function makeDevice(overrides: Partial = {}): DeviceStatus { + return { + id: 1, + entityId: "switch.test", + name: "Test Device", + kind: "switch", + priority: 1, + minValue: 0, + maxValue: 1, + dwellSeconds: 300, + manualOverride: false, + onlyWhenSun: false, + fromTime: null, + toTime: null, + enableUrl: null, + disableUrl: null, + currentValue: 1, + shed: false, + lastChangedAt: null, + available: true, + ...overrides, + }; +} + +describe("decideControlAction", () => { + it("sheds the lowest-priority unshed device when over target", () => { + const status = makeStatus({ overTarget: true, projectedAverageW: 6000 }); + const devices = [ + makeDevice({ entityId: "switch.a", priority: 3 }), + makeDevice({ entityId: "switch.b", priority: 1 }), + makeDevice({ entityId: "switch.c", priority: 2 }), + ]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: true }); + expect(action).toMatchObject({ entityId: "switch.b", action: "shed" }); + }); + + it("does not shed devices under manual override or dwell lockout", () => { + const status = makeStatus({ overTarget: true, projectedAverageW: 6000 }); + const devices = [ + makeDevice({ entityId: "switch.a", priority: 1, manualOverride: true }), + makeDevice({ + entityId: "switch.b", + priority: 2, + lastChangedAt: new Date(999_800).toISOString(), + dwellSeconds: 300, + }), + makeDevice({ entityId: "switch.c", priority: 3 }), + ]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: true }); + expect(action).toMatchObject({ entityId: "switch.c", action: "shed" }); + }); + + it("returns null when over target but no controllable device is available", () => { + const status = makeStatus({ overTarget: true, projectedAverageW: 6000 }); + const devices = [makeDevice({ manualOverride: true }), makeDevice({ shed: true })]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: true }); + expect(action).toBeNull(); + }); + + it("restores the highest-priority shed device once margin is comfortable", () => { + const status = makeStatus({ overTarget: false, projectedAverageW: 3000, targetW: 5000 }); + const devices = [ + makeDevice({ entityId: "switch.a", priority: 3, shed: true }), + makeDevice({ entityId: "switch.b", priority: 1, shed: true }), + ]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: true }); + expect(action).toMatchObject({ entityId: "switch.a", action: "restore" }); + }); + + it("does not restore when margin is below the hysteresis threshold", () => { + const status = makeStatus({ overTarget: false, projectedAverageW: 4800, targetW: 5000 }); + const devices = [makeDevice({ shed: true })]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: true }); + expect(action).toBeNull(); + }); + + it("returns null when nothing is shed and under target", () => { + const status = makeStatus({ overTarget: false, projectedAverageW: 1000, targetW: 5000 }); + const devices = [makeDevice({ shed: false })]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: true }); + expect(action).toBeNull(); + }); + + it("sheds a sun-only device when the sun is down, even under target", () => { + const status = makeStatus({ overTarget: false, projectedAverageW: 1000, targetW: 5000 }); + const devices = [makeDevice({ entityId: "switch.solar", onlyWhenSun: true, shed: false })]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: false }); + expect(action).toMatchObject({ entityId: "switch.solar", action: "shed", reason: "outside allowed sun hours" }); + }); + + it("does not restore a sun-only device while the sun is down", () => { + const status = makeStatus({ overTarget: false, projectedAverageW: 3000, targetW: 5000 }); + const devices = [makeDevice({ entityId: "switch.solar", onlyWhenSun: true, shed: true })]; + const action = decideControlAction({ status, devices, nowMs: 1_000_000, restoreMarginW: 500, sunUp: false }); + expect(action).toBeNull(); + }); + + it("sheds a device once it falls outside its allowed time window", () => { + const status = makeStatus({ overTarget: false, projectedAverageW: 1000, targetW: 5000 }); + // nowMs = 23:00 local time-of-day in a day starting at epoch-aligned midnight is awkward to + // construct portably, so drive minutesSinceMidnight indirectly via a known Date. + const outsideWindow = new Date(); + outsideWindow.setHours(23, 0, 0, 0); + const devices = [ + makeDevice({ entityId: "switch.scheduled", fromTime: "08:00", toTime: "20:00", shed: false }), + ]; + const action = decideControlAction({ + status, + devices, + nowMs: outsideWindow.getTime(), + restoreMarginW: 500, + sunUp: true, + }); + expect(action).toMatchObject({ entityId: "switch.scheduled", action: "shed", reason: "outside allowed time window" }); + }); +}); diff --git a/energy-management/backend/src/__tests__/quarterHour.test.ts b/energy-management/backend/src/__tests__/quarterHour.test.ts new file mode 100644 index 0000000..1837fcb --- /dev/null +++ b/energy-management/backend/src/__tests__/quarterHour.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { blockStartFor, computeQuarterHourStatus } from "../control/quarterHour.js"; + +const BLOCK_MS = 15 * 60 * 1000; + +describe("blockStartFor", () => { + it("aligns to the current 15-minute boundary", () => { + const t = new Date("2026-08-18T10:07:30Z").getTime(); + expect(blockStartFor(t)).toBe(new Date("2026-08-18T10:00:00Z").getTime()); + }); + + it("is idempotent on an exact boundary", () => { + const t = new Date("2026-08-18T10:15:00Z").getTime(); + expect(blockStartFor(t)).toBe(t); + }); +}); + +describe("computeQuarterHourStatus", () => { + it("returns zero averages when no samples exist yet", () => { + const blockStart = new Date("2026-08-18T10:00:00Z").getTime(); + const now = blockStart + 60_000; + const status = computeQuarterHourStatus([], now, 5000); + expect(status.runningAverageW).toBe(0); + expect(status.projectedAverageW).toBe(0); + expect(status.overTarget).toBe(false); + }); + + it("computes a flat running average for a constant power draw", () => { + const blockStart = new Date("2026-08-18T10:00:00Z").getTime(); + const now = blockStart + 5 * 60_000; + const samples = [{ timestampMs: blockStart, watts: 3000 }]; + const status = computeQuarterHourStatus(samples, now, 5000); + expect(status.runningAverageW).toBeCloseTo(3000); + // Projection assumes the last known power (3000W) holds for the rest of the block. + expect(status.projectedAverageW).toBeCloseTo(3000); + expect(status.overTarget).toBe(false); + }); + + it("projects over target when a late spike would push the block average up", () => { + const blockStart = new Date("2026-08-18T10:00:00Z").getTime(); + const tenMinIn = blockStart + 10 * 60_000; + const now = blockStart + 12 * 60_000; + const samples = [ + { timestampMs: blockStart, watts: 1000 }, + { timestampMs: tenMinIn, watts: 9000 }, + ]; + const status = computeQuarterHourStatus(samples, now, 3000); + // energy so far: 1000W*10min + 9000W*2min; remaining 3min projected at 9000W + const expectedEnergy = 1000 * 10 * 60_000 + 9000 * 2 * 60_000; + const expectedRunningAvg = expectedEnergy / (12 * 60_000); + expect(status.runningAverageW).toBeCloseTo(expectedRunningAvg); + const expectedProjectedEnergy = expectedEnergy + 9000 * 3 * 60_000; + const expectedProjectedAvg = expectedProjectedEnergy / BLOCK_MS; + expect(status.projectedAverageW).toBeCloseTo(expectedProjectedAvg); + expect(status.overTarget).toBe(true); + }); + + it("ignores samples from before the current block", () => { + const prevBlockSample = { timestampMs: new Date("2026-08-18T09:59:00Z").getTime(), watts: 20000 }; + const blockStart = new Date("2026-08-18T10:00:00Z").getTime(); + const now = blockStart + 60_000; + const status = computeQuarterHourStatus([prevBlockSample], now, 5000); + expect(status.runningAverageW).toBe(0); + }); + + it("elapsedFractionOfBlock reflects how far into the block now is", () => { + const blockStart = new Date("2026-08-18T10:00:00Z").getTime(); + const now = blockStart + 3 * 60_000; + const status = computeQuarterHourStatus([], now, 5000); + expect(status.elapsedFractionOfBlock).toBeCloseTo(3 / 15); + }); +}); diff --git a/energy-management/backend/src/api/routes.ts b/energy-management/backend/src/api/routes.ts new file mode 100644 index 0000000..a9ebda7 --- /dev/null +++ b/energy-management/backend/src/api/routes.ts @@ -0,0 +1,80 @@ +import type { FastifyInstance } from "fastify"; +import { z } from "zod"; +import type { Store } from "../db/store.js"; +import type { ControlLoopRunner } from "../control/runner.js"; + +const timeOfDaySchema = z + .string() + .regex(/^([01]\d|2[0-3]):[0-5]\d$/, "expected HH:MM") + .nullable(); + +const endpointUrlSchema = z.string().url().nullable(); + +const deviceInputSchema = z.object({ + entityId: z.string().min(1), + name: z.string().min(1), + kind: z.enum(["switch", "number"]), + priority: z.number().int(), + minValue: z.number().default(0), + maxValue: z.number().default(1), + dwellSeconds: z.number().int().positive().default(300), + manualOverride: z.boolean().default(false), + onlyWhenSun: z.boolean().default(false), + fromTime: timeOfDaySchema.default(null), + toTime: timeOfDaySchema.default(null), + enableUrl: endpointUrlSchema.default(null), + disableUrl: endpointUrlSchema.default(null), +}); + +const deviceUpdateSchema = deviceInputSchema.partial(); + +export function registerRoutes(app: FastifyInstance, store: Store, runner: ControlLoopRunner): void { + app.get("/api/status", async () => { + return runner.getStatus(); + }); + + app.get("/api/history", async (req) => { + const query = z.object({ sinceMs: z.coerce.number().optional() }).parse(req.query); + const sinceMs = query.sinceMs ?? Date.now() - 30 * 24 * 60 * 60 * 1000; + return store.listBlockHistory(sinceMs); + }); + + app.get("/api/actions", async (req) => { + const query = z.object({ limit: z.coerce.number().optional() }).parse(req.query); + return store.listRecentActions(query.limit ?? 50); + }); + + app.get("/api/devices", async () => { + return store.listDevices(); + }); + + app.post("/api/devices", async (req, reply) => { + const body = deviceInputSchema.parse(req.body); + const device = store.createDevice(body); + reply.code(201); + return device; + }); + + app.patch("/api/devices/:id", async (req) => { + const params = z.object({ id: z.coerce.number() }).parse(req.params); + const body = deviceUpdateSchema.parse(req.body); + store.updateDevice(params.id, body); + return { ok: true }; + }); + + app.delete("/api/devices/:id", async (req) => { + const params = z.object({ id: z.coerce.number() }).parse(req.params); + store.deleteDevice(params.id); + return { ok: true }; + }); + + app.get("/api/settings/target", async () => { + return { targetW: store.getTargetW() }; + }); + + app.put("/api/settings/target", async (req) => { + const body = z.object({ targetKw: z.number().positive() }).parse(req.body); + store.setTargetKw(body.targetKw); + return { ok: true }; + }); +} diff --git a/energy-management/backend/src/control/loop.ts b/energy-management/backend/src/control/loop.ts new file mode 100644 index 0000000..b9b8505 --- /dev/null +++ b/energy-management/backend/src/control/loop.ts @@ -0,0 +1,100 @@ +import type { ControlAction, DeviceStatus, QuarterHourStatus } from "../types.js"; + +export interface LoopDecisionInput { + status: QuarterHourStatus; + devices: DeviceStatus[]; + nowMs: number; + /** Only restore once projected power is at least this far below target, to avoid flapping. */ + restoreMarginW: number; + /** Whether HA's sun.sun entity currently reports above_horizon. */ + sunUp: boolean; +} + +function isDwellElapsed(device: DeviceStatus, nowMs: number): boolean { + if (device.lastChangedAt === null) return true; + const lastChangeMs = new Date(device.lastChangedAt).getTime(); + return nowMs - lastChangeMs >= device.dwellSeconds * 1000; +} + +function isControllable(device: DeviceStatus, nowMs: number): boolean { + return !device.manualOverride && device.available && isDwellElapsed(device, nowMs); +} + +function minutesSinceMidnight(nowMs: number): number { + const d = new Date(nowMs); + return d.getHours() * 60 + d.getMinutes(); +} + +function isWithinTimeWindow(fromTime: string | null, toTime: string | null, nowMs: number): boolean { + if (!fromTime || !toTime) return true; + const [fromH, fromM] = fromTime.split(":").map(Number); + const [toH, toM] = toTime.split(":").map(Number); + const fromMinutes = fromH * 60 + fromM; + const toMinutes = toH * 60 + toM; + const nowMinutes = minutesSinceMidnight(nowMs); + if (fromMinutes === toMinutes) return true; + if (fromMinutes < toMinutes) return nowMinutes >= fromMinutes && nowMinutes < toMinutes; + return nowMinutes >= fromMinutes || nowMinutes < toMinutes; // window wraps past midnight +} + +/** Whether device is currently allowed to be enabled, per its sun/time-window constraints. */ +function isWithinSchedule(device: DeviceStatus, nowMs: number, sunUp: boolean): boolean { + if (device.onlyWhenSun && !sunUp) return false; + return isWithinTimeWindow(device.fromTime, device.toTime, nowMs); +} + +/** + * Pure decision function: given the current quarter-hour projection and device + * states, decides at most one shed or restore action to take this tick. + * Acting on a single device per tick (rather than all eligible at once) avoids + * overshooting the target and keeps the effect of each action observable. + */ +export function decideControlAction(input: LoopDecisionInput): ControlAction | null { + const { status, devices, nowMs, restoreMarginW, sunUp } = input; + + // Devices currently enabled outside their allowed sun/time window get shed + // first, independent of the power budget. + const scheduleViolators = devices + .filter((d) => !d.shed && isControllable(d, nowMs) && !isWithinSchedule(d, nowMs, sunUp)) + .sort((a, b) => a.priority - b.priority); + if (scheduleViolators[0]) { + const target = scheduleViolators[0]; + return { + timestampMs: nowMs, + entityId: target.entityId, + action: "shed", + reason: target.onlyWhenSun && !sunUp ? "outside allowed sun hours" : "outside allowed time window", + }; + } + + if (status.overTarget) { + const candidates = devices + .filter((d) => !d.shed && isControllable(d, nowMs)) + .sort((a, b) => a.priority - b.priority); // least important (lowest priority) first + const target = candidates[0]; + if (!target) return null; + return { + timestampMs: nowMs, + entityId: target.entityId, + action: "shed", + reason: `projected block average ${Math.round(status.projectedAverageW)}W exceeds target ${Math.round(status.targetW)}W`, + }; + } + + const marginW = status.targetW - status.projectedAverageW; + if (marginW >= restoreMarginW) { + const candidates = devices + .filter((d) => d.shed && isControllable(d, nowMs) && isWithinSchedule(d, nowMs, sunUp)) + .sort((a, b) => b.priority - a.priority); // most important (highest priority) first + const target = candidates[0]; + if (!target) return null; + return { + timestampMs: nowMs, + entityId: target.entityId, + action: "restore", + reason: `projected block average ${Math.round(status.projectedAverageW)}W is ${Math.round(marginW)}W under target ${Math.round(status.targetW)}W`, + }; + } + + return null; +} diff --git a/energy-management/backend/src/control/quarterHour.ts b/energy-management/backend/src/control/quarterHour.ts new file mode 100644 index 0000000..c50dfea --- /dev/null +++ b/energy-management/backend/src/control/quarterHour.ts @@ -0,0 +1,54 @@ +import type { PowerSample, QuarterHourStatus } from "../types.js"; + +export const BLOCK_DURATION_MS = 15 * 60 * 1000; + +export function blockStartFor(timestampMs: number): number { + return Math.floor(timestampMs / BLOCK_DURATION_MS) * BLOCK_DURATION_MS; +} + +/** + * Computes the running and projected average power for the current 15-minute + * billing block, given a step-function reading of power samples (each sample + * holds until the next one arrives). Samples outside the current block are + * ignored; if the block has no samples yet, power is assumed to be 0 so far. + */ +export function computeQuarterHourStatus( + samples: PowerSample[], + nowMs: number, + targetW: number, +): QuarterHourStatus { + const blockStartMs = blockStartFor(nowMs); + const blockEndMs = blockStartMs + BLOCK_DURATION_MS; + + const relevant = samples + .filter((s) => s.timestampMs >= blockStartMs && s.timestampMs <= nowMs) + .sort((a, b) => a.timestampMs - b.timestampMs); + + let energyWms = 0; + let cursorMs = blockStartMs; + let cursorWatts = relevant.length > 0 ? relevant[0].watts : 0; + + for (const sample of relevant) { + energyWms += cursorWatts * (sample.timestampMs - cursorMs); + cursorMs = sample.timestampMs; + cursorWatts = sample.watts; + } + energyWms += cursorWatts * (nowMs - cursorMs); + + const elapsedMs = nowMs - blockStartMs; + const runningAverageW = elapsedMs > 0 ? energyWms / elapsedMs : 0; + + const remainingMs = blockEndMs - nowMs; + const projectedEnergyWms = energyWms + cursorWatts * remainingMs; + const projectedAverageW = projectedEnergyWms / BLOCK_DURATION_MS; + + return { + blockStartMs, + blockEndMs, + elapsedFractionOfBlock: elapsedMs / BLOCK_DURATION_MS, + runningAverageW, + projectedAverageW, + targetW, + overTarget: projectedAverageW > targetW, + }; +} diff --git a/energy-management/backend/src/control/runner.ts b/energy-management/backend/src/control/runner.ts new file mode 100644 index 0000000..eae058e --- /dev/null +++ b/energy-management/backend/src/control/runner.ts @@ -0,0 +1,88 @@ +import type { QueryApi } from "@influxdata/influxdb-client"; +import type { Env } from "../env.js"; +import { fetchPowerSamplesSince } from "../influx/power.js"; +import { HomeAssistantClient } from "../homeassistant/client.js"; +import { getDeviceStatus, restoreDevice, shedDevice } from "../homeassistant/devices.js"; +import type { Store } from "../db/store.js"; +import type { DeviceStatus, QuarterHourStatus } from "../types.js"; +import { blockStartFor, computeQuarterHourStatus } from "./quarterHour.js"; +import { decideControlAction } from "./loop.js"; + +const RESTORE_MARGIN_W = 300; + +export class ControlLoopRunner { + constructor( + private readonly env: Env, + private readonly store: Store, + private readonly queryApi: QueryApi, + private readonly ha: HomeAssistantClient, + private readonly log: { info: (o: unknown, msg?: string) => void; error: (o: unknown, msg?: string) => void }, + ) {} + + async getStatus(): Promise<{ quarterHour: QuarterHourStatus; devices: DeviceStatus[] }> { + const nowMs = Date.now(); + const blockStart = blockStartFor(nowMs); + const targetW = this.store.getTargetW(); + + const samples = await fetchPowerSamplesSince(this.queryApi, this.env, blockStart); + const quarterHour = computeQuarterHourStatus(samples, nowMs, targetW); + + const devices = await this.loadDeviceStatuses(); + + return { quarterHour, devices }; + } + + private async loadDeviceStatuses(): Promise { + const configs = this.store.listDevices(); + return Promise.all( + configs.map(async (config) => { + const { shed, lastChangedAt } = this.store.getDeviceShedState(config.id); + return getDeviceStatus(this.ha, config, shed, lastChangedAt); + }), + ); + } + + async tick(): Promise { + const nowMs = Date.now(); + const { quarterHour, devices } = await this.getStatus(); + + // Upserting every tick means the last write before a block rolls over holds + // as that block's final average, with no separate "finalize" step needed. + this.store.upsertBlockHistory(quarterHour.blockStartMs, quarterHour.runningAverageW); + + const sunUp = await this.ha.isSunUp().catch((err) => { + this.log.error({ err }, "failed to read sun.sun state; treating as unrestricted"); + return true; + }); + const decision = decideControlAction({ status: quarterHour, devices, nowMs, restoreMarginW: RESTORE_MARGIN_W, sunUp }); + if (!decision) return; + + const device = devices.find((d) => d.entityId === decision.entityId); + if (!device) return; + + try { + if (decision.action === "shed") { + await shedDevice(this.ha, device); + } else { + await restoreDevice(this.ha, device); + } + const changedAtIso = new Date(decision.timestampMs).toISOString(); + this.store.setDeviceShed(device.id, decision.action === "shed", changedAtIso); + this.store.logAction(decision); + this.log.info({ decision }, "control action applied"); + } catch (err) { + this.log.error({ err, decision }, "failed to apply control action"); + } + } + +} + +export function createControlLoopRunner( + env: Env, + store: Store, + queryApi: QueryApi, + log: { info: (o: unknown, msg?: string) => void; error: (o: unknown, msg?: string) => void }, +): ControlLoopRunner { + const ha = new HomeAssistantClient(env); + return new ControlLoopRunner(env, store, queryApi, ha, log); +} diff --git a/energy-management/backend/src/db/index.ts b/energy-management/backend/src/db/index.ts new file mode 100644 index 0000000..884ec32 --- /dev/null +++ b/energy-management/backend/src/db/index.ts @@ -0,0 +1,72 @@ +import Database from "better-sqlite3"; +import { mkdirSync } from "node:fs"; +import { dirname } from "node:path"; +import type { Env } from "../env.js"; + +export function openDb(env: Env): Database.Database { + mkdirSync(dirname(env.DB_PATH), { recursive: true }); + const db = new Database(env.DB_PATH); + db.pragma("journal_mode = WAL"); + migrate(db, env); + return db; +} + +function migrate(db: Database.Database, env: Env): void { + db.exec(` + CREATE TABLE IF NOT EXISTS devices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + entity_id TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + kind TEXT NOT NULL CHECK (kind IN ('switch', 'number')), + priority INTEGER NOT NULL, + min_value REAL NOT NULL DEFAULT 0, + max_value REAL NOT NULL DEFAULT 1, + dwell_seconds INTEGER NOT NULL DEFAULT 300, + manual_override INTEGER NOT NULL DEFAULT 0, + shed INTEGER NOT NULL DEFAULT 0, + last_changed_at TEXT + ); + + CREATE TABLE IF NOT EXISTS settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS control_actions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + timestamp_ms INTEGER NOT NULL, + entity_id TEXT NOT NULL, + action TEXT NOT NULL CHECK (action IN ('shed', 'restore')), + reason TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS block_history ( + block_start_ms INTEGER PRIMARY KEY, + average_w REAL NOT NULL + ); + `); + + const existingTarget = db.prepare("SELECT value FROM settings WHERE key = 'target_kw'").get(); + if (!existingTarget) { + db.prepare("INSERT INTO settings (key, value) VALUES ('target_kw', ?)").run(String(env.TARGET_KW)); + } + + const deviceColumns = new Set( + (db.prepare("PRAGMA table_info(devices)").all() as { name: string }[]).map((c) => c.name), + ); + if (!deviceColumns.has("only_when_sun")) { + db.exec("ALTER TABLE devices ADD COLUMN only_when_sun INTEGER NOT NULL DEFAULT 0"); + } + if (!deviceColumns.has("from_time")) { + db.exec("ALTER TABLE devices ADD COLUMN from_time TEXT"); + } + if (!deviceColumns.has("to_time")) { + db.exec("ALTER TABLE devices ADD COLUMN to_time TEXT"); + } + if (!deviceColumns.has("enable_url")) { + db.exec("ALTER TABLE devices ADD COLUMN enable_url TEXT"); + } + if (!deviceColumns.has("disable_url")) { + db.exec("ALTER TABLE devices ADD COLUMN disable_url TEXT"); + } +} diff --git a/energy-management/backend/src/db/store.ts b/energy-management/backend/src/db/store.ts new file mode 100644 index 0000000..3995cbb --- /dev/null +++ b/energy-management/backend/src/db/store.ts @@ -0,0 +1,140 @@ +import type Database from "better-sqlite3"; +import type { ControlAction, DeviceConfig } from "../types.js"; + +interface DeviceRow { + id: number; + entity_id: string; + name: string; + kind: "switch" | "number"; + priority: number; + min_value: number; + max_value: number; + dwell_seconds: number; + manual_override: number; + only_when_sun: number; + from_time: string | null; + to_time: string | null; + enable_url: string | null; + disable_url: string | null; + shed: number; + last_changed_at: string | null; +} + +function rowToDeviceConfig(row: DeviceRow): DeviceConfig { + return { + id: row.id, + entityId: row.entity_id, + name: row.name, + kind: row.kind, + priority: row.priority, + minValue: row.min_value, + maxValue: row.max_value, + dwellSeconds: row.dwell_seconds, + manualOverride: row.manual_override === 1, + onlyWhenSun: row.only_when_sun === 1, + fromTime: row.from_time, + toTime: row.to_time, + enableUrl: row.enable_url, + disableUrl: row.disable_url, + }; +} + +export class Store { + constructor(private readonly db: Database.Database) {} + + listDevices(): DeviceConfig[] { + const rows = this.db.prepare("SELECT * FROM devices ORDER BY priority ASC").all() as DeviceRow[]; + return rows.map(rowToDeviceConfig); + } + + getDeviceShedState(id: number): { shed: boolean; lastChangedAt: string | null } { + const row = this.db.prepare("SELECT shed, last_changed_at FROM devices WHERE id = ?").get(id) as + | { shed: number; last_changed_at: string | null } + | undefined; + return { shed: row?.shed === 1, lastChangedAt: row?.last_changed_at ?? null }; + } + + createDevice(device: Omit): DeviceConfig { + const result = this.db + .prepare( + `INSERT INTO devices (entity_id, name, kind, priority, min_value, max_value, dwell_seconds, manual_override, only_when_sun, from_time, to_time, enable_url, disable_url) + VALUES (@entityId, @name, @kind, @priority, @minValue, @maxValue, @dwellSeconds, @manualOverride, @onlyWhenSun, @fromTime, @toTime, @enableUrl, @disableUrl)`, + ) + .run({ ...device, manualOverride: device.manualOverride ? 1 : 0, onlyWhenSun: device.onlyWhenSun ? 1 : 0 }); + return { ...device, id: Number(result.lastInsertRowid) }; + } + + updateDevice(id: number, patch: Partial>): void { + const fields = Object.entries(patch); + if (fields.length === 0) return; + const columnMap: Record = { + entityId: "entity_id", + name: "name", + kind: "kind", + priority: "priority", + minValue: "min_value", + maxValue: "max_value", + dwellSeconds: "dwell_seconds", + manualOverride: "manual_override", + onlyWhenSun: "only_when_sun", + fromTime: "from_time", + toTime: "to_time", + enableUrl: "enable_url", + disableUrl: "disable_url", + }; + const setClause = fields.map(([key]) => `${columnMap[key]} = @${key}`).join(", "); + const values: Record = { id }; + for (const [key, value] of fields) { + values[key] = key === "manualOverride" || key === "onlyWhenSun" ? (value ? 1 : 0) : value; + } + this.db.prepare(`UPDATE devices SET ${setClause} WHERE id = @id`).run(values); + } + + deleteDevice(id: number): void { + this.db.prepare("DELETE FROM devices WHERE id = ?").run(id); + } + + setDeviceShed(id: number, shed: boolean, changedAtIso: string): void { + this.db + .prepare("UPDATE devices SET shed = ?, last_changed_at = ? WHERE id = ?") + .run(shed ? 1 : 0, changedAtIso, id); + } + + getTargetW(): number { + const row = this.db.prepare("SELECT value FROM settings WHERE key = 'target_kw'").get() as { value: string }; + return Number(row.value) * 1000; + } + + setTargetKw(targetKw: number): void { + this.db.prepare("UPDATE settings SET value = ? WHERE key = 'target_kw'").run(String(targetKw)); + } + + logAction(action: ControlAction): void { + this.db + .prepare("INSERT INTO control_actions (timestamp_ms, entity_id, action, reason) VALUES (?, ?, ?, ?)") + .run(action.timestampMs, action.entityId, action.action, action.reason); + } + + listRecentActions(limit = 50): ControlAction[] { + const rows = this.db + .prepare("SELECT timestamp_ms, entity_id, action, reason FROM control_actions ORDER BY timestamp_ms DESC LIMIT ?") + .all(limit) as { timestamp_ms: number; entity_id: string; action: "shed" | "restore"; reason: string }[]; + return rows.map((r) => ({ timestampMs: r.timestamp_ms, entityId: r.entity_id, action: r.action, reason: r.reason })); + } + + upsertBlockHistory(blockStartMs: number, averageW: number): void { + this.db + .prepare( + `INSERT INTO block_history (block_start_ms, average_w) VALUES (?, ?) + ON CONFLICT(block_start_ms) DO UPDATE SET average_w = excluded.average_w`, + ) + .run(blockStartMs, averageW); + } + + listBlockHistory(sinceMs: number): { blockStartMs: number; averageW: number }[] { + const rows = this.db + .prepare("SELECT block_start_ms, average_w FROM block_history WHERE block_start_ms >= ? ORDER BY block_start_ms ASC") + .all(sinceMs) as { block_start_ms: number; average_w: number }[]; + return rows.map((r) => ({ blockStartMs: r.block_start_ms, averageW: r.average_w })); + } +} diff --git a/energy-management/backend/src/env.ts b/energy-management/backend/src/env.ts new file mode 100644 index 0000000..46fcb83 --- /dev/null +++ b/energy-management/backend/src/env.ts @@ -0,0 +1,37 @@ +import { config as loadDotenv } from "dotenv"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { z } from "zod"; + +// In production (docker-compose) env vars are already injected via env_file; +// this only fills gaps for local `npm run dev`, and is a no-op if the file is absent. +loadDotenv({ path: join(dirname(fileURLToPath(import.meta.url)), "../../.env") }); + +const envSchema = z.object({ + PORT: z.coerce.number().default(3000), + INFLUX_URL: z.string().url(), + INFLUX_TOKEN: z.string().min(1), + INFLUX_ORG: z.string().min(1), + INFLUX_BUCKET: z.string().min(1), + INFLUX_MEASUREMENT: z.string().default("power"), + INFLUX_DOMAIN: z.string(), + INFLUX_ENTITY_ID: z.string(), + INFLUX_FIELD: z.string().default("value"), + INFLUX_FIELD_UNIT: z.enum(["W", "kW"]).default("W"), + HA_URL: z.string().url(), + HA_TOKEN: z.string().min(1), + TARGET_KW: z.coerce.number().positive(), + DB_PATH: z.string().default("./data/energy.db"), + CONTROL_LOOP_INTERVAL_MS: z.coerce.number().default(10_000), +}); + +export type Env = z.infer; + +export function loadEnv(): Env { + const parsed = envSchema.safeParse(process.env); + if (!parsed.success) { + console.error("Invalid environment configuration:", parsed.error.flatten().fieldErrors); + process.exit(1); + } + return parsed.data; +} diff --git a/energy-management/backend/src/homeassistant/client.ts b/energy-management/backend/src/homeassistant/client.ts new file mode 100644 index 0000000..9d55b0d --- /dev/null +++ b/energy-management/backend/src/homeassistant/client.ts @@ -0,0 +1,56 @@ +import type { Env } from "../env.js"; + +export interface HaState { + entity_id: string; + state: string; + attributes: Record; +} + +export class HomeAssistantClient { + constructor(private readonly env: Env) {} + + private async request(path: string, init?: RequestInit): Promise { + const res = await fetch(`${this.env.HA_URL}${path}`, { + ...init, + headers: { + Authorization: `Bearer ${this.env.HA_TOKEN}`, + "Content-Type": "application/json", + ...init?.headers, + }, + }); + if (!res.ok) { + throw new Error(`Home Assistant request failed: ${init?.method ?? "GET"} ${path} -> ${res.status} ${await res.text()}`); + } + return (await res.json()) as T; + } + + async getState(entityId: string): Promise { + return this.request(`/api/states/${entityId}`); + } + + async getStates(): Promise { + return this.request(`/api/states`); + } + + async callService(domain: string, service: string, data: Record): Promise { + await this.request(`/api/services/${domain}/${service}`, { + method: "POST", + body: JSON.stringify(data), + }); + } + + async setSwitch(entityId: string, on: boolean): Promise { + const domain = entityId.split(".")[0]; + await this.callService(domain, on ? "turn_on" : "turn_off", { entity_id: entityId }); + } + + async setNumber(entityId: string, value: number): Promise { + await this.callService("number", "set_value", { entity_id: entityId, value }); + } + + /** Whether HA's sun.sun entity currently reports the sun above the horizon. */ + async isSunUp(): Promise { + const state = await this.getState("sun.sun"); + return state.state === "above_horizon"; + } +} diff --git a/energy-management/backend/src/homeassistant/devices.ts b/energy-management/backend/src/homeassistant/devices.ts new file mode 100644 index 0000000..c697868 --- /dev/null +++ b/energy-management/backend/src/homeassistant/devices.ts @@ -0,0 +1,87 @@ +import type { DeviceConfig, DeviceStatus } from "../types.js"; +import type { HomeAssistantClient } from "./client.js"; + +function normalizeHaValue(kind: DeviceConfig["kind"], state: string): number | null { + if (kind === "switch") { + if (state === "on") return 1; + if (state === "off") return 0; + return null; + } + const n = Number(state); + return Number.isFinite(n) ? n : null; +} + +/** True when both directions are driven by custom endpoints rather than Home Assistant. */ +function isExternallyControlled(device: DeviceConfig): boolean { + return device.enableUrl !== null && device.disableUrl !== null; +} + +async function callEndpoint(url: string): Promise { + const res = await fetch(url, { method: "POST" }); + if (!res.ok) { + throw new Error(`custom endpoint request failed: POST ${url} -> ${res.status} ${await res.text()}`); + } +} + +export async function getDeviceStatus( + ha: HomeAssistantClient, + device: DeviceConfig, + shed: boolean, + lastChangedAt: string | null, +): Promise { + // Externally controlled devices have no Home Assistant entity to poll; state is + // whatever we last commanded via the custom endpoints. + if (isExternallyControlled(device)) { + return { + ...device, + currentValue: shed ? (device.kind === "switch" ? 0 : device.minValue) : device.kind === "switch" ? 1 : device.maxValue, + shed, + lastChangedAt, + available: true, + }; + } + try { + const haState = await ha.getState(device.entityId); + return { + ...device, + currentValue: normalizeHaValue(device.kind, haState.state), + shed, + lastChangedAt, + available: haState.state !== "unavailable" && haState.state !== "unknown", + }; + } catch { + return { + ...device, + currentValue: null, + shed, + lastChangedAt, + available: false, + }; + } +} + +/** Sheds (reduces load of) a device: turns a switch off, or lowers a number to its floor. */ +export async function shedDevice(ha: HomeAssistantClient, device: DeviceConfig): Promise { + if (device.disableUrl) { + await callEndpoint(device.disableUrl); + return; + } + if (device.kind === "switch") { + await ha.setSwitch(device.entityId, false); + } else { + await ha.setNumber(device.entityId, device.minValue); + } +} + +/** Restores a device to its normal operating value. */ +export async function restoreDevice(ha: HomeAssistantClient, device: DeviceConfig): Promise { + if (device.enableUrl) { + await callEndpoint(device.enableUrl); + return; + } + if (device.kind === "switch") { + await ha.setSwitch(device.entityId, true); + } else { + await ha.setNumber(device.entityId, device.maxValue); + } +} diff --git a/energy-management/backend/src/index.ts b/energy-management/backend/src/index.ts new file mode 100644 index 0000000..788a570 --- /dev/null +++ b/energy-management/backend/src/index.ts @@ -0,0 +1,53 @@ +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import staticPlugin from "@fastify/static"; +import { existsSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; +import { loadEnv } from "./env.js"; +import { openDb } from "./db/index.js"; +import { Store } from "./db/store.js"; +import { createInfluxQueryApi } from "./influx/client.js"; +import { createControlLoopRunner } from "./control/runner.js"; +import { registerRoutes } from "./api/routes.js"; + +async function main() { + const env = loadEnv(); + const app = Fastify({ + logger: { + transport: process.env.NODE_ENV === "production" ? undefined : { target: "pino-pretty" }, + }, + }); + + await app.register(cors, { origin: true }); + + const db = openDb(env); + const store = new Store(db); + const queryApi = createInfluxQueryApi(env); + const runner = createControlLoopRunner(env, store, queryApi, app.log); + + registerRoutes(app, store, runner); + + const publicDir = join(dirname(fileURLToPath(import.meta.url)), "public"); + if (existsSync(publicDir)) { + await app.register(staticPlugin, { root: publicDir }); + app.setNotFoundHandler((req, reply) => { + if (req.raw.url?.startsWith("/api")) { + reply.code(404).send({ error: "not found" }); + return; + } + reply.sendFile("index.html"); + }); + } + + setInterval(() => { + runner.tick().catch((err) => app.log.error({ err }, "control loop tick failed")); + }, env.CONTROL_LOOP_INTERVAL_MS); + + await app.listen({ port: env.PORT, host: "0.0.0.0" }); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/energy-management/backend/src/influx/client.ts b/energy-management/backend/src/influx/client.ts new file mode 100644 index 0000000..be433d0 --- /dev/null +++ b/energy-management/backend/src/influx/client.ts @@ -0,0 +1,7 @@ +import { InfluxDB, type QueryApi } from "@influxdata/influxdb-client"; +import type { Env } from "../env.js"; + +export function createInfluxQueryApi(env: Env): QueryApi { + const influx = new InfluxDB({ url: env.INFLUX_URL, token: env.INFLUX_TOKEN }); + return influx.getQueryApi(env.INFLUX_ORG); +} diff --git a/energy-management/backend/src/influx/power.ts b/energy-management/backend/src/influx/power.ts new file mode 100644 index 0000000..9fe6726 --- /dev/null +++ b/energy-management/backend/src/influx/power.ts @@ -0,0 +1,51 @@ +import type { QueryApi } from "@influxdata/influxdb-client"; +import type { Env } from "../env.js"; +import type { PowerSample } from "../types.js"; + +/** + * Fetches raw power samples between `sinceMs` and now. + * Converts the configured field to watts based on INFLUX_FIELD_UNIT. + */ +export async function fetchPowerSamplesSince( + queryApi: QueryApi, + env: Env, + sinceMs: number, +): Promise { + const sinceIso = new Date(sinceMs).toISOString(); + const flux = ` + from(bucket: "${env.INFLUX_BUCKET}") + |> range(start: ${sinceIso}) + |> filter(fn: (r) => r["_measurement"] == "${env.INFLUX_FIELD_UNIT}") + |> filter(fn: (r) => r["_field"] == "${env.INFLUX_FIELD}") + |> filter(fn: (r) => r["domain"] == "${env.INFLUX_DOMAIN}") + |> filter(fn: (r) => r["entity_id"] == "${env.INFLUX_ENTITY_ID}") + |> sort(columns: ["_time"]) + `; + + const unitMultiplier = env.INFLUX_FIELD_UNIT === "kW" ? 1000 : 1; + const samples: PowerSample[] = []; + + for await (const { values, tableMeta } of queryApi.iterateRows(flux)) { + const row = tableMeta.toObject(values) as { _time: string; _value: number }; + samples.push({ + timestampMs: new Date(row._time).getTime(), + watts: row._value * unitMultiplier, + }); + } + + return samples; +} + +/** Fetches the single most recent power sample, if any exists within the lookback window. */ +export async function fetchLatestPowerSample( + queryApi: QueryApi, + env: Env, + lookbackMs = 60_000, +): Promise { + const samples = await fetchPowerSamplesSince( + queryApi, + env, + Date.now() - lookbackMs, + ); + return samples.length > 0 ? samples[samples.length - 1] : null; +} diff --git a/energy-management/backend/src/types.ts b/energy-management/backend/src/types.ts new file mode 100644 index 0000000..ce19e34 --- /dev/null +++ b/energy-management/backend/src/types.ts @@ -0,0 +1,47 @@ +export type ControlKind = "switch" | "number"; + +export interface DeviceConfig { + id: number; + entityId: string; + name: string; + kind: ControlKind; + priority: number; // lower = shed first + minValue: number; // for "number" kind: floor to shed to; for "switch": unused (0) + maxValue: number; // for "number" kind: normal operating value; for "switch": unused (1) + dwellSeconds: number; // minimum time between state changes for this device + manualOverride: boolean; // if true, control loop leaves this device alone + onlyWhenSun: boolean; // if true, device may only be enabled while sun.sun reports above_horizon + fromTime: string | null; // "HH:MM", start of allowed enable window (local time), null = no restriction + toTime: string | null; // "HH:MM", end of allowed enable window (local time), null = no restriction + enableUrl: string | null; // if set, called (HTTP POST) to enable/restore the device instead of Home Assistant + disableUrl: string | null; // if set, called (HTTP POST) to disable/shed the device instead of Home Assistant +} + +export interface DeviceStatus extends DeviceConfig { + currentValue: number | null; // current HA state, normalized (0/1 for switch, number value) + shed: boolean; + lastChangedAt: string | null; // ISO timestamp of last control-loop action + available: boolean; // whether HA reports the entity as reachable +} + +export interface PowerSample { + timestampMs: number; + watts: number; +} + +export interface QuarterHourStatus { + blockStartMs: number; + blockEndMs: number; + elapsedFractionOfBlock: number; // 0..1 + runningAverageW: number; + projectedAverageW: number; + targetW: number; + overTarget: boolean; +} + +export interface ControlAction { + timestampMs: number; + entityId: string; + action: "shed" | "restore"; + reason: string; +} diff --git a/energy-management/backend/tsconfig.json b/energy-management/backend/tsconfig.json new file mode 100644 index 0000000..b125b50 --- /dev/null +++ b/energy-management/backend/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "declaration": false, + "sourceMap": true + }, + "include": ["src"] +} diff --git a/energy-management/docker-compose.yml b/energy-management/docker-compose.yml new file mode 100644 index 0000000..5d3abac --- /dev/null +++ b/energy-management/docker-compose.yml @@ -0,0 +1,15 @@ +services: + energy-management: + build: . + ports: + - "3000:3000" + env_file: + - .env + environment: + DB_PATH: /app/data/energy.db + volumes: + - energy-management-data:/app/data + restart: unless-stopped + +volumes: + energy-management-data: diff --git a/energy-management/frontend/index.html b/energy-management/frontend/index.html new file mode 100644 index 0000000..145912f --- /dev/null +++ b/energy-management/frontend/index.html @@ -0,0 +1,12 @@ + + + + + + Peak Power Manager + + +

+ + + diff --git a/energy-management/frontend/package.json b/energy-management/frontend/package.json new file mode 100644 index 0000000..5a37605 --- /dev/null +++ b/energy-management/frontend/package.json @@ -0,0 +1,21 @@ +{ + "name": "frontend", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.3", + "vite": "^7.0.0" + } +} diff --git a/energy-management/frontend/src/App.tsx b/energy-management/frontend/src/App.tsx new file mode 100644 index 0000000..81c8f5a --- /dev/null +++ b/energy-management/frontend/src/App.tsx @@ -0,0 +1,23 @@ +import { useState } from "react"; +import { Dashboard } from "./Dashboard"; +import { Devices } from "./Devices"; + +type Tab = "dashboard" | "devices"; + +export function App() { + const [tab, setTab] = useState("dashboard"); + + return ( +
+ + {tab === "dashboard" ? : } +
+ ); +} diff --git a/energy-management/frontend/src/Dashboard.tsx b/energy-management/frontend/src/Dashboard.tsx new file mode 100644 index 0000000..3dfbebb --- /dev/null +++ b/energy-management/frontend/src/Dashboard.tsx @@ -0,0 +1,136 @@ +import { useEffect, useState } from "react"; +import { api, type BlockHistoryPoint, type ControlAction, type DeviceStatus, type QuarterHourStatus } from "./api"; +import { PeakHistoryChart } from "./PeakHistoryChart"; + +const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; + +export function Dashboard() { + const [quarterHour, setQuarterHour] = useState(null); + const [devices, setDevices] = useState([]); + const [history, setHistory] = useState([]); + const [actions, setActions] = useState([]); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + + async function refresh() { + try { + const [status, hist, acts] = await Promise.all([ + api.getStatus(), + api.getHistory(Date.now() - THIRTY_DAYS_MS), + api.getActions(10), + ]); + if (cancelled) return; + setQuarterHour(status.quarterHour); + setDevices(status.devices); + setHistory(hist); + setActions(acts); + setError(null); + } catch (err) { + if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load status"); + } + } + + refresh(); + const interval = setInterval(refresh, 10_000); + return () => { + cancelled = true; + clearInterval(interval); + }; + }, []); + + if (error) { + return ( +
+

Error

+

{error}

+
+ ); + } + + if (!quarterHour) { + return
Loading…
; + } + + const overTarget = quarterHour.overTarget; + const shedCount = devices.filter((d) => d.shed).length; + + return ( + <> +
+

Current quarter-hour block

+
{(quarterHour.projectedAverageW / 1000).toFixed(2)} kW
+
+ projected average · target {(quarterHour.targetW / 1000).toFixed(2)} kW · running average{" "} + {(quarterHour.runningAverageW / 1000).toFixed(2)} kW ·{" "} + {Math.round(quarterHour.elapsedFractionOfBlock * 100)}% through block +
+
+ + {overTarget ? `Over target · ${shedCount} device(s) shed` : "Under target"} + +
+
+ +
+

Devices

+ {devices.length === 0 ? ( +

No devices configured yet — add them in the Devices tab.

+ ) : ( + devices + .slice() + .sort((a, b) => a.priority - b.priority) + .map((d) => ( +
+
+
{d.name}
+
+ {d.entityId} · priority {d.priority} + {!d.available ? " · unavailable" : ""} +
+
+
+ {d.manualOverride && Manual} + {d.shed ? "Shed" : "Active"} +
+
+ )) + )} +
+ +
+

Daily peak (last 30 days)

+ +
+ +
+

Recent control actions

+ {actions.length === 0 ? ( +

No actions yet.

+ ) : ( + + + + + + + + + + + {actions.map((a, i) => ( + + + + + + + ))} + +
TimeDeviceActionReason
{new Date(a.timestampMs).toLocaleTimeString()}{a.entityId}{a.action}{a.reason}
+ )} +
+ + ); +} diff --git a/energy-management/frontend/src/Devices.tsx b/energy-management/frontend/src/Devices.tsx new file mode 100644 index 0000000..c55385f --- /dev/null +++ b/energy-management/frontend/src/Devices.tsx @@ -0,0 +1,247 @@ +import { useEffect, useState, type FormEvent } from "react"; +import { api, type ControlKind, type DeviceConfig } from "./api"; + +const emptyForm = { + entityId: "", + name: "", + kind: "switch" as ControlKind, + priority: 1, + minValue: 0, + maxValue: 1, + dwellSeconds: 300, + manualOverride: false, + onlyWhenSun: false, + fromTime: null as string | null, + toTime: null as string | null, + enableUrl: null as string | null, + disableUrl: null as string | null, +}; + +export function Devices() { + const [devices, setDevices] = useState([]); + const [form, setForm] = useState(emptyForm); + const [error, setError] = useState(null); + + async function refresh() { + setDevices(await api.getDevices()); + } + + useEffect(() => { + refresh(); + }, []); + + async function handleCreate(e: FormEvent) { + e.preventDefault(); + try { + await api.createDevice(form); + setForm(emptyForm); + await refresh(); + setError(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Failed to create device"); + } + } + + async function toggleOverride(d: DeviceConfig) { + await api.updateDevice(d.id, { manualOverride: !d.manualOverride }); + await refresh(); + } + + async function remove(d: DeviceConfig) { + if (!confirm(`Remove ${d.name}?`)) return; + await api.deleteDevice(d.id); + await refresh(); + } + + return ( + <> +
+

Add device

+
+
+ + + + + {form.kind === "number" && ( + <> + + + + )} + + + + + + +
+

+ Set both custom endpoints to control this device over plain HTTP + (POST) instead of Home Assistant. The entity ID above is still used + as the device's unique key. +

+ {error && ( +

+ {error} +

+ )} + +
+
+ +
+

Configured devices

+ {devices.length === 0 ? ( +

+ No devices configured yet. +

+ ) : ( + devices + .slice() + .sort((a, b) => a.priority - b.priority) + .map((d) => ( +
+
+
{d.name}
+
+ {d.entityId} · {d.kind} · priority {d.priority} · dwell{" "} + {d.dwellSeconds}s{d.onlyWhenSun && " · sun only"} + {d.fromTime && d.toTime && ` · ${d.fromTime}–${d.toTime}`} + {d.enableUrl && d.disableUrl && " · custom endpoints"} +
{d.enableUrl}
+
{d.disableUrl}
+
+
+
+ {d.manualOverride && ( + Manual + )} + + +
+
+ )) + )} +
+ + ); +} diff --git a/energy-management/frontend/src/PeakHistoryChart.tsx b/energy-management/frontend/src/PeakHistoryChart.tsx new file mode 100644 index 0000000..0c5fa48 --- /dev/null +++ b/energy-management/frontend/src/PeakHistoryChart.tsx @@ -0,0 +1,114 @@ +import { useMemo, useState } from "react"; +import type { BlockHistoryPoint } from "./api"; + +interface DailyPeak { + dayStartMs: number; + peakW: number; +} + +function toDailyPeaks(points: BlockHistoryPoint[]): DailyPeak[] { + const byDay = new Map(); + for (const p of points) { + const dayStart = new Date(p.blockStartMs); + dayStart.setHours(0, 0, 0, 0); + const key = dayStart.getTime(); + byDay.set(key, Math.max(byDay.get(key) ?? 0, p.averageW)); + } + return [...byDay.entries()] + .map(([dayStartMs, peakW]) => ({ dayStartMs, peakW })) + .sort((a, b) => a.dayStartMs - b.dayStartMs); +} + +const WIDTH = 880; +const HEIGHT = 220; +const PAD = { top: 16, right: 16, bottom: 28, left: 48 }; + +export function PeakHistoryChart({ points, targetW }: { points: BlockHistoryPoint[]; targetW: number }) { + const daily = useMemo(() => toDailyPeaks(points), [points]); + const [hoverIdx, setHoverIdx] = useState(null); + + if (daily.length === 0) { + return

No history yet — check back after the first day of data.

; + } + + const maxW = Math.max(targetW, ...daily.map((d) => d.peakW)) * 1.1; + const innerW = WIDTH - PAD.left - PAD.right; + const innerH = HEIGHT - PAD.top - PAD.bottom; + + const x = (i: number) => PAD.left + (daily.length === 1 ? innerW / 2 : (i / (daily.length - 1)) * innerW); + const y = (w: number) => PAD.top + innerH - (w / maxW) * innerH; + + const linePath = daily.map((d, i) => `${i === 0 ? "M" : "L"}${x(i)},${y(d.peakW)}`).join(" "); + const targetY = y(targetW); + const hovered = hoverIdx !== null ? daily[hoverIdx] : null; + + return ( +
+ + {[0, 0.25, 0.5, 0.75, 1].map((f) => ( + + ))} + + + + target {Math.round(targetW / 1000)} kW + + + + + {daily.map((d, i) => ( + targetW ? "var(--status-critical)" : "var(--series-1)"} + stroke="var(--surface-1)" + strokeWidth={2} + onMouseEnter={() => setHoverIdx(i)} + onMouseLeave={() => setHoverIdx(null)} + style={{ cursor: "pointer" }} + /> + ))} + + {daily.map((d, i) => + i % Math.ceil(daily.length / 8 || 1) === 0 ? ( + + {new Date(d.dayStartMs).toLocaleDateString(undefined, { month: "short", day: "numeric" })} + + ) : null, + )} + + + {hovered && ( +
+
{new Date(hovered.dayStartMs).toLocaleDateString()}
+
peak {(hovered.peakW / 1000).toFixed(2)} kW
+
+ )} +
+ ); +} diff --git a/energy-management/frontend/src/api.ts b/energy-management/frontend/src/api.ts new file mode 100644 index 0000000..a7132b8 --- /dev/null +++ b/energy-management/frontend/src/api.ts @@ -0,0 +1,71 @@ +export type ControlKind = "switch" | "number"; + +export interface DeviceConfig { + id: number; + entityId: string; + name: string; + kind: ControlKind; + priority: number; + minValue: number; + maxValue: number; + dwellSeconds: number; + manualOverride: boolean; + onlyWhenSun: boolean; + fromTime: string | null; + toTime: string | null; + enableUrl: string | null; + disableUrl: string | null; +} + +export interface DeviceStatus extends DeviceConfig { + currentValue: number | null; + shed: boolean; + lastChangedAt: string | null; + available: boolean; +} + +export interface QuarterHourStatus { + blockStartMs: number; + blockEndMs: number; + elapsedFractionOfBlock: number; + runningAverageW: number; + projectedAverageW: number; + targetW: number; + overTarget: boolean; +} + +export interface ControlAction { + timestampMs: number; + entityId: string; + action: "shed" | "restore"; + reason: string; +} + +export interface BlockHistoryPoint { + blockStartMs: number; + averageW: number; +} + +async function request(path: string, init?: RequestInit): Promise { + const res = await fetch(path, { + ...init, + headers: { ...(init?.body ? { "Content-Type": "application/json" } : {}), ...init?.headers }, + }); + if (!res.ok) throw new Error(`${init?.method ?? "GET"} ${path} failed: ${res.status}`); + return res.json() as Promise; +} + +export const api = { + getStatus: () => request<{ quarterHour: QuarterHourStatus; devices: DeviceStatus[] }>("/api/status"), + getHistory: (sinceMs: number) => request(`/api/history?sinceMs=${sinceMs}`), + getActions: (limit = 50) => request(`/api/actions?limit=${limit}`), + getDevices: () => request("/api/devices"), + createDevice: (device: Omit) => + request("/api/devices", { method: "POST", body: JSON.stringify(device) }), + updateDevice: (id: number, patch: Partial>) => + request<{ ok: true }>(`/api/devices/${id}`, { method: "PATCH", body: JSON.stringify(patch) }), + deleteDevice: (id: number) => request<{ ok: true }>(`/api/devices/${id}`, { method: "DELETE" }), + getTarget: () => request<{ targetW: number }>("/api/settings/target"), + setTargetKw: (targetKw: number) => + request<{ ok: true }>("/api/settings/target", { method: "PUT", body: JSON.stringify({ targetKw }) }), +}; diff --git a/energy-management/frontend/src/main.tsx b/energy-management/frontend/src/main.tsx new file mode 100644 index 0000000..8d02487 --- /dev/null +++ b/energy-management/frontend/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { App } from "./App"; +import "./theme.css"; + +createRoot(document.getElementById("root")!).render( + + + , +); diff --git a/energy-management/frontend/src/theme.css b/energy-management/frontend/src/theme.css new file mode 100644 index 0000000..fb835e4 --- /dev/null +++ b/energy-management/frontend/src/theme.css @@ -0,0 +1,260 @@ +:root { + color-scheme: light; + --surface-1: #fcfcfb; + --page-plane: #f9f9f7; + --text-primary: #0b0b0b; + --text-secondary: #52514e; + --text-muted: #898781; + --gridline: #e1e0d9; + --baseline: #c3c2b7; + --border: rgba(11, 11, 11, 0.1); + --series-1: #2a78d6; + --seq-300: #6da7ec; + --seq-500: #256abf; + --status-good: #0ca30c; + --status-warning: #fab219; + --status-critical: #d03b3b; +} + +@media (prefers-color-scheme: dark) { + :root:where(:not([data-theme="light"])) { + color-scheme: dark; + --surface-1: #1a1a19; + --page-plane: #0d0d0d; + --text-primary: #ffffff; + --text-secondary: #c3c2b7; + --text-muted: #898781; + --gridline: #2c2c2a; + --baseline: #383835; + --border: rgba(255, 255, 255, 0.1); + --series-1: #3987e5; + --seq-300: #5598e7; + --seq-500: #1c5cab; + --status-good: #0ca30c; + --status-warning: #fab219; + --status-critical: #d03b3b; + } +} + +:root[data-theme="dark"] { + color-scheme: dark; + --surface-1: #1a1a19; + --page-plane: #0d0d0d; + --text-primary: #ffffff; + --text-secondary: #c3c2b7; + --text-muted: #898781; + --gridline: #2c2c2a; + --baseline: #383835; + --border: rgba(255, 255, 255, 0.1); + --series-1: #3987e5; + --seq-300: #5598e7; + --seq-500: #1c5cab; + --status-good: #0ca30c; + --status-warning: #fab219; + --status-critical: #d03b3b; +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + background: var(--page-plane); + color: var(--text-primary); + font-family: system-ui, -apple-system, "Segoe UI", sans-serif; +} + +.app { + max-width: 960px; + margin: 0 auto; + padding: 24px 20px 64px; +} + +nav.tabs { + display: flex; + gap: 4px; + margin-bottom: 20px; + border-bottom: 1px solid var(--border); +} + +nav.tabs button { + background: none; + border: none; + font: inherit; + font-size: 14px; + font-weight: 600; + color: var(--text-secondary); + padding: 10px 14px; + cursor: pointer; + border-bottom: 2px solid transparent; +} + +nav.tabs button.active { + color: var(--text-primary); + border-bottom-color: var(--series-1); +} + +.card { + background: var(--surface-1); + border: 1px solid var(--border); + border-radius: 12px; + padding: 20px; + margin-bottom: 16px; +} + +.card h2 { + margin: 0 0 12px; + font-size: 13px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.03em; + color: var(--text-secondary); +} + +.hero-figure { + font-size: 40px; + font-weight: 600; + line-height: 1.1; +} + +.hero-sub { + font-size: 13px; + color: var(--text-muted); + margin-top: 4px; +} + +.status-pill { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + padding: 4px 10px; + border-radius: 999px; +} + +.status-pill.good { + color: var(--status-good); + background: color-mix(in srgb, var(--status-good) 14%, transparent); +} + +.status-pill.critical { + color: var(--status-critical); + background: color-mix(in srgb, var(--status-critical) 14%, transparent); +} + +.device-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 0; + border-bottom: 1px solid var(--gridline); + gap: 12px; +} + +.device-row:last-child { + border-bottom: none; +} + +.device-name { + font-weight: 600; + font-size: 14px; +} + +.device-meta { + font-size: 12px; + color: var(--text-muted); +} + +.badge { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 3px 8px; + border-radius: 6px; +} + +.badge.shed { + color: var(--status-critical); + background: color-mix(in srgb, var(--status-critical) 14%, transparent); +} + +.badge.active { + color: var(--status-good); + background: color-mix(in srgb, var(--status-good) 14%, transparent); +} + +.badge.override { + color: var(--status-warning); + background: color-mix(in srgb, var(--status-warning) 20%, transparent); +} + +input, +select { + font: inherit; + padding: 6px 8px; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--surface-1); + color: var(--text-primary); +} + +button.primary { + font: inherit; + font-weight: 600; + padding: 8px 14px; + border-radius: 8px; + border: none; + background: var(--series-1); + color: white; + cursor: pointer; +} + +button.secondary { + font: inherit; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid var(--border); + background: transparent; + color: var(--text-primary); + cursor: pointer; +} + +table { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +th { + text-align: left; + color: var(--text-muted); + font-weight: 600; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.03em; + padding: 6px 8px; + border-bottom: 1px solid var(--gridline); +} + +td { + padding: 8px; + border-bottom: 1px solid var(--gridline); +} + +.form-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 10px; + margin-bottom: 12px; +} + +.form-grid label { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 12px; + color: var(--text-secondary); +} diff --git a/energy-management/frontend/tsconfig.json b/energy-management/frontend/tsconfig.json new file mode 100644 index 0000000..b05eea9 --- /dev/null +++ b/energy-management/frontend/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "Bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true + }, + "include": ["src"] +} diff --git a/energy-management/frontend/tsconfig.tsbuildinfo b/energy-management/frontend/tsconfig.tsbuildinfo new file mode 100644 index 0000000..32bf746 --- /dev/null +++ b/energy-management/frontend/tsconfig.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/App.tsx","./src/Dashboard.tsx","./src/Devices.tsx","./src/PeakHistoryChart.tsx","./src/api.ts","./src/main.tsx"],"version":"5.9.3"} \ No newline at end of file diff --git a/energy-management/frontend/vite.config.ts b/energy-management/frontend/vite.config.ts new file mode 100644 index 0000000..d444349 --- /dev/null +++ b/energy-management/frontend/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + proxy: { + "/api": "http://localhost:3000", + }, + }, +}); diff --git a/energy-management/package-lock.json b/energy-management/package-lock.json new file mode 100644 index 0000000..22ec347 --- /dev/null +++ b/energy-management/package-lock.json @@ -0,0 +1,3872 @@ +{ + "name": "energy-management", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "energy-management", + "workspaces": [ + "backend", + "frontend" + ] + }, + "backend": { + "dependencies": { + "@fastify/cors": "^10.0.1", + "@fastify/static": "^10.1.3", + "@influxdata/influxdb-client": "^1.35.0", + "better-sqlite3": "^11.8.1", + "dotenv": "^17.4.2", + "fastify": "^5.2.1", + "pino": "^9.6.0", + "zod": "^3.24.1" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/node": "^22.10.7", + "pino-pretty": "^13.0.0", + "tsx": "^4.19.2", + "typescript": "^5.7.3", + "vitest": "^3.0.4" + } + }, + "frontend": { + "dependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/react": "^19.0.7", + "@types/react-dom": "^19.0.3", + "@vitejs/plugin-react": "^4.3.4", + "typescript": "^5.7.3", + "vite": "^7.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.29.7.tgz", + "integrity": "sha512-TL0hMc9xzy86VD31nUiwzd5otRAcyEPcsegCxolO0PvcXuH1v0kECe/UIznYFihpkvU5wg/jk4v0TTEFfm53fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.29.7.tgz", + "integrity": "sha512-06IyK09H3wi4cGbhDBwp5gUGo0IKtnYa8tyTiephirPCK6fbobVGiXMMI5zLQ4aKEYP3wZ3ArU44o+8KMrSG/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "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" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/accept-negotiator": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/accept-negotiator/-/accept-negotiator-2.1.0.tgz", + "integrity": "sha512-F3EVbzWt+xcnVaOHmWyIlpuFtbxOln7HDZQsh09MtMmMm/CipMayNt8hnIL8VQi54u2ZociDbf+iluGYkf7B1A==", + "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.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", + "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/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "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.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "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.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "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.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "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.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "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.1", + "resolved": "https://registry.npmjs.org/@fastify/send/-/send-4.1.1.tgz", + "integrity": "sha512-BYo+EiaKwlxH+WetGk6hAs1d39iP0y1gqB8lGF/qwkJ9ZZ/cBY1vx5NvExb9Sc3yRMFjD5X4Eyh4e4+TzRkzdw==", + "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.3", + "resolved": "https://registry.npmjs.org/@fastify/static/-/static-10.1.3.tgz", + "integrity": "sha512-W6jqajYS974XjPjB5hQWoxPM8NKM4+p8YmQT6G5IbCa4uhdWSVadZUv75siy1wEA/3ty8RYdpBydfWeu9AqAqQ==", + "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": "^2.0.1", + "fastify-plugin": "^6.0.0", + "fastq": "^1.17.1", + "glob": "^13.0.0" + } + }, + "node_modules/@fastify/static/node_modules/fastify-plugin": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-6.0.0.tgz", + "integrity": "sha512-fZOty7z3O7vOliF6d8bHE3wiEh1KcNnKEQensSgTk9C1DvN6nRLS++XVd86v33Hw/8u9Un8A1zDrQ8ujcQDHEg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@influxdata/influxdb-client": { + "version": "1.35.0", + "resolved": "https://registry.npmjs.org/@influxdata/influxdb-client/-/influxdb-client-1.35.0.tgz", + "integrity": "sha512-woWMi8PDpPQpvTsRaUw4Ig+nOGS/CWwAwS66Fa1Vr/EkW+NEwxI8YfPBsdBMn33jK2Y86/qMiiuX/ROHIkJLTw==", + "license": "MIT" + }, + "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.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "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/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "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/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/better-sqlite3": { + "version": "7.6.13", + "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz", + "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "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/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/ajv/node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "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.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "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/backend": { + "resolved": "backend", + "link": true + }, + "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/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.11.15", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.15.tgz", + "integrity": "sha512-FwMjJJ7HnyZpWe+oWxegG0fezZyBZUagI5LZEoO3GCbtbKNwRfMH9Ue5d5v01PNePBy1QSfPSDTTeVL0Hb9EzA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-sqlite3": { + "version": "11.10.0", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz", + "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", + "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "license": "MIT", + "engines": { + "node": ">=4.0.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==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.4.2", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.2.tgz", + "integrity": "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.409", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.409.tgz", + "integrity": "sha512-ChI4N44d0B4A6C8prnNjMOaGgE59fUyEVYcRYm2XEXIjMbbvF5i9UL1cblDbpGqiU0uS8FE8UcKxqZqTXdmzbQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "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/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "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/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/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, + "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-copy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-4.0.4.tgz", + "integrity": "sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==", + "dev": true, + "license": "MIT" + }, + "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.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "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-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-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz", + "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==", + "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.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.0.tgz", + "integrity": "sha512-A3RNEaDIHWaxFW8n8rNJaW1wQ+XAXuoU71llfUQJjuh5WaYLmKfRhhanaJBOx8m2EBPQkR6sDBovYdHNe9F6rA==", + "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": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "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/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT" + }, + "node_modules/find-my-way": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.8.0.tgz", + "integrity": "sha512-JtyUgATO7qxRp2zKhrmWof74Mqxc1ikbwpwMY97p8ipuTj2QtreA4gK2JNAF6SOqqHnYYkwMUvsgQVi2AJxIyw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/frontend": { + "resolved": "frontend", + "link": true + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, + "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/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, + "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/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true, + "license": "MIT" + }, + "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/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "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/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "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/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "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/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "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/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/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "license": "MIT", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, + "node_modules/node-abi": { + "version": "3.94.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.94.0.tgz", + "integrity": "sha512-W5ZNO5KRPB5TkYmGVD9F6YqhsglXJzE6etpbmT+f6EQElhiX/UTG551cnsRGvLG3fyZEg9HwaDmNmj5nwJ4z9g==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==", + "license": "MIT" + }, + "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/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "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/path-scurry/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-9.14.0.tgz", + "integrity": "sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-pretty": { + "version": "13.1.3", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.3.tgz", + "integrity": "sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^4.0.0", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/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==", + "dev": true, + "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.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "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/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "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/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "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/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "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": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "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/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "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/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "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": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tar-fs": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.5.tgz", + "integrity": "sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "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.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "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/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "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/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "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", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.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 + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "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/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/energy-management/package.json b/energy-management/package.json new file mode 100644 index 0000000..973d0a9 --- /dev/null +++ b/energy-management/package.json @@ -0,0 +1,14 @@ +{ + "name": "energy-management", + "private": true, + "workspaces": [ + "backend", + "frontend" + ], + "scripts": { + "dev:backend": "npm run dev -w backend", + "dev:frontend": "npm run dev -w frontend", + "build": "npm run build -w backend && npm run build -w frontend", + "test": "npm run test -w backend" + } +}