From d9d9743d2c7dec6166792184b5850f04524da840 Mon Sep 17 00:00:00 2001 From: yandrik Date: Sat, 6 Dec 2025 12:38:49 +0100 Subject: [PATCH] feat: dryer counter initial commot --- docker-compose.yml | 21 + dryer-counter/.gitignore | 23 + dryer-counter/.npmrc | 1 + dryer-counter/Dockerfile | 47 + dryer-counter/README.md | 38 + dryer-counter/data/dryer.db | Bin 0 -> 24576 bytes dryer-counter/package.json | 32 + dryer-counter/pnpm-lock.yaml | 1551 +++++++++++++++++ dryer-counter/pnpm-workspace.yaml | 3 + dryer-counter/src/app.css | 432 +++++ dryer-counter/src/app.d.ts | 13 + dryer-counter/src/app.html | 11 + dryer-counter/src/lib/assets/favicon.svg | 1 + .../src/lib/components/Calculator.svelte | 541 ++++++ .../src/lib/components/Counter.svelte | 145 ++ .../lib/components/DateRangeSelector.svelte | 418 +++++ .../src/lib/components/DryChart.svelte | 313 ++++ .../src/lib/components/RecentEntries.svelte | 506 ++++++ dryer-counter/src/lib/index.ts | 1 + dryer-counter/src/lib/server/db.ts | 297 ++++ dryer-counter/src/lib/server/sse.ts | 48 + dryer-counter/src/routes/+layout.svelte | 29 + dryer-counter/src/routes/+page.server.ts | 23 + dryer-counter/src/routes/+page.svelte | 416 +++++ .../src/routes/api/calculate-costs/+server.ts | 31 + .../src/routes/api/cost-settings/+server.ts | 57 + dryer-counter/src/routes/api/dry/+server.ts | 92 + .../src/routes/api/dry/entries/+server.ts | 48 + .../src/routes/api/dry/stats/+server.ts | 43 + .../src/routes/api/dry/stream/+server.ts | 30 + dryer-counter/static/robots.txt | 3 + dryer-counter/svelte.config.js | 20 + dryer-counter/tsconfig.json | 20 + dryer-counter/vite.config.ts | 6 + pocketbase/Dockerfile | 22 + 35 files changed, 5282 insertions(+) create mode 100644 docker-compose.yml create mode 100644 dryer-counter/.gitignore create mode 100644 dryer-counter/.npmrc create mode 100644 dryer-counter/Dockerfile create mode 100644 dryer-counter/README.md create mode 100644 dryer-counter/data/dryer.db create mode 100644 dryer-counter/package.json create mode 100644 dryer-counter/pnpm-lock.yaml create mode 100644 dryer-counter/pnpm-workspace.yaml create mode 100644 dryer-counter/src/app.css create mode 100644 dryer-counter/src/app.d.ts create mode 100644 dryer-counter/src/app.html create mode 100644 dryer-counter/src/lib/assets/favicon.svg create mode 100644 dryer-counter/src/lib/components/Calculator.svelte create mode 100644 dryer-counter/src/lib/components/Counter.svelte create mode 100644 dryer-counter/src/lib/components/DateRangeSelector.svelte create mode 100644 dryer-counter/src/lib/components/DryChart.svelte create mode 100644 dryer-counter/src/lib/components/RecentEntries.svelte create mode 100644 dryer-counter/src/lib/index.ts create mode 100644 dryer-counter/src/lib/server/db.ts create mode 100644 dryer-counter/src/lib/server/sse.ts create mode 100644 dryer-counter/src/routes/+layout.svelte create mode 100644 dryer-counter/src/routes/+page.server.ts create mode 100644 dryer-counter/src/routes/+page.svelte create mode 100644 dryer-counter/src/routes/api/calculate-costs/+server.ts create mode 100644 dryer-counter/src/routes/api/cost-settings/+server.ts create mode 100644 dryer-counter/src/routes/api/dry/+server.ts create mode 100644 dryer-counter/src/routes/api/dry/entries/+server.ts create mode 100644 dryer-counter/src/routes/api/dry/stats/+server.ts create mode 100644 dryer-counter/src/routes/api/dry/stream/+server.ts create mode 100644 dryer-counter/static/robots.txt create mode 100644 dryer-counter/svelte.config.js create mode 100644 dryer-counter/tsconfig.json create mode 100644 dryer-counter/vite.config.ts create mode 100644 pocketbase/Dockerfile diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a082e02 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +services: + dryer-counter: + build: ./dryer-counter + ports: + - "3000:3000" + volumes: + - dryer-data:/app/data + environment: + - NODE_ENV=production + - PORT=3000 + restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + +volumes: + dryer-data: + driver: local diff --git a/dryer-counter/.gitignore b/dryer-counter/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/dryer-counter/.gitignore @@ -0,0 +1,23 @@ +node_modules + +# Output +.output +.vercel +.netlify +.wrangler +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/dryer-counter/.npmrc b/dryer-counter/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/dryer-counter/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/dryer-counter/Dockerfile b/dryer-counter/Dockerfile new file mode 100644 index 0000000..c7247b9 --- /dev/null +++ b/dryer-counter/Dockerfile @@ -0,0 +1,47 @@ +# Build stage +FROM node:20-alpine AS builder + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY . . + +# Build the application +RUN pnpm build + +# Production stage +FROM node:20-alpine AS runner + +WORKDIR /app + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files and install production dependencies +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ +RUN pnpm install --prod --frozen-lockfile + +# Copy built application +COPY --from=builder /app/build ./build + +# Create data directory for SQLite +RUN mkdir -p /app/data + +# Set environment variables +ENV NODE_ENV=production +ENV PORT=3000 + +# Expose port +EXPOSE 3000 + +# Start the application +CMD ["node", "build"] \ No newline at end of file diff --git a/dryer-counter/README.md b/dryer-counter/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/dryer-counter/README.md @@ -0,0 +1,38 @@ +# sv + +Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli). + +## Creating a project + +If you're seeing this, you've probably already done this step. Congrats! + +```sh +# create a new project in the current directory +npx sv create + +# create a new project in my-app +npx sv create my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: + +```sh +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +To create a production version of your app: + +```sh +npm run build +``` + +You can preview the production build with `npm run preview`. + +> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment. diff --git a/dryer-counter/data/dryer.db b/dryer-counter/data/dryer.db new file mode 100644 index 0000000000000000000000000000000000000000..5f271e1135380e9823c323c87620bab38b734e2e GIT binary patch literal 24576 zcmeI4Uu;uV7{KrC|G)Pl2t!bJb$spS#=QU9Zq+%;C@}YztrMnb8SRBtwhp=?fe=lQ z2NFWUgD*a4LWnQEXo!g>zWAUCQG6gIMq*-!FFt5O3@;iq@w8=UU9;PLz8UyV*7V+< zU%zwC@7&w=X5YD!`-Tf;OF6Ponx82vhOkZ$MPY}c2!c>z4-b1R6$jgJE$y&pv2NRL zvm|t1`YX(8o#AbQbW?hVJy9V8WPl8i0Wv@a$N(8217v^<{BI4MJK*%TM5E&Qh4Red zIV-oYSk5k5<#M5TbaBOwYhWUs%A}P{YUglTSy8352mF42A+HRMX3~4o6Uz9+;7Dp> zS{X`DD+7De14BwXtL;;?j;;!6oh)UKTczx=Q?m*SI;@P2Wt7pW;bCQ0dUtASIHRcD zMwMhlc5!y0RLkNYPt37vQKt69NvoK%8g`v4#d4`&E!J&1R>Z2SXgW=)sm$2mD0}M&8%K4# z<L!AN4~dx`+cg<@BPL5rT0DWymz;^)$^z4rsreNyPkudm`8H| z>i)|8zPscea&L3}>YHntiS`P`hY>ap#bzFmzgnw~TgNuvi2W3U*^U@#L7 zag5JUL!Amm<21~%}IP4(D?1aRVnhviu$F!soW9r~l95#)?bU1*(^cd`4 z6H}8~54?iIrf^sWgC$^+W2;U*^Tzs?fC(J74~LC&Y}GlfH5!L895#xDh{N{cuss;efZaGOjl*`~umK#llVeSe5FJt&OoJV3VVahV^*}$zY$mIQ zy|fmlvX5v3`qsqQCsGVvA;B>lZMf4gZAOH-pF%E zu49?AccS$|JED8+yE2NyA{=YdXaWF(#bFx?i?l5d)lQv}j2V$~H0meWMK9g28&=2@IBi%@`~WEgWky z1QRwPFf|SvF_;OD6`{ZL^v}60R+ar%l#a)Z_XX{tNd?Xlm+w%lUH{(_ z@_l^{ zI?D-8BsH^g{hwor^YV}Kr}72L|8KfXfb#$Ox(UkvXX|qs&ZlZ(Kjr^FSXonBbPxl| z|6d`*0LuU8LMZ>g;WMO`*iZTY{0ONC(Sh>+>%MrI61Gl&^8f3VQ2u}8uMyQ?M+xQs zFRk;{l1zw7`TzAN4O^Q)`TxsH!Pz0gR)g#CsO0~%(dvoH^?y*B6y)3THTj~Pm;2;R z(p~AgbVWMDUZz3@$N(8217v^=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.12': + resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.12': + resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.12': + resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.12': + resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.12': + resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.12': + resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.12': + resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.12': + resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.12': + resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.12': + resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.12': + resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.12': + resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.12': + resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.12': + resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.12': + resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.12': + resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.12': + resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.12': + resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.12': + resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.12': + resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.12': + resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.12': + resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.12': + resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.12': + resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.12': + resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@rollup/plugin-commonjs@28.0.9': + resolution: {integrity: sha512-PIR4/OHZ79romx0BVVll/PkwWpJ7e5lsqFa3gFfcrFPWwLXLV39JVUzQV9RKjWerE7B845Hqjj9VYlQeieZ2dA==} + engines: {node: '>=16.0.0 || 14 >= 14.17'} + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-json@6.1.0': + resolution: {integrity: sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/plugin-node-resolve@16.0.3': + resolution: {integrity: sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^2.78.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.53.3': + resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.53.3': + resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.53.3': + resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.53.3': + resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.53.3': + resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.53.3': + resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.53.3': + resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.53.3': + resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.53.3': + resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.53.3': + resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.53.3': + resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.53.3': + resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==} + cpu: [x64] + os: [win32] + + '@standard-schema/spec@1.0.0': + resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} + + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} + peerDependencies: + acorn: ^8.9.0 + + '@sveltejs/adapter-auto@7.0.0': + resolution: {integrity: sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw==} + peerDependencies: + '@sveltejs/kit': ^2.0.0 + + '@sveltejs/adapter-node@5.4.0': + resolution: {integrity: sha512-NMsrwGVPEn+J73zH83Uhss/hYYZN6zT3u31R3IHAn3MiKC3h8fjmIAhLfTSOeNHr5wPYfjjMg8E+1gyFgyrEcQ==} + peerDependencies: + '@sveltejs/kit': ^2.4.0 + + '@sveltejs/kit@2.49.1': + resolution: {integrity: sha512-vByReCTTdlNM80vva8alAQC80HcOiHLkd8XAxIiKghKSHcqeNfyhp3VsYAV8VSiPKu4Jc8wWCfsZNAIvd1uCqA==} + engines: {node: '>=18.13'} + hasBin: true + peerDependencies: + '@opentelemetry/api': ^1.0.0 + '@sveltejs/vite-plugin-svelte': ^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 + svelte: ^4.0.0 || ^5.0.0-next.0 + vite: ^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1': + resolution: {integrity: sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + '@sveltejs/vite-plugin-svelte': ^6.0.0-next.0 + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@sveltejs/vite-plugin-svelte@6.2.1': + resolution: {integrity: sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==} + engines: {node: ^20.19 || ^22.12 || >=24} + peerDependencies: + svelte: ^5.0.0 + vite: ^6.3.0 || ^7.0.0 + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/cookie@0.6.0': + resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/node@24.10.1': + resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==} + + '@types/resolve@1.20.2': + resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + better-sqlite3@12.5.0: + resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + commondir@1.0.1: + resolution: {integrity: sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==} + + cookie@0.6.0: + resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} + engines: {node: '>= 0.6'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + devalue@5.5.0: + resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + esbuild@0.25.12: + resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==} + engines: {node: '>=18'} + hasBin: true + + esm-env@1.2.2: + resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==} + + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-module@1.0.0: + resolution: {integrity: sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==} + + is-reference@1.2.1: + resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} + + is-reference@3.0.3: + resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} + + kleur@4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + + locate-character@3.0.0: + resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdsvex@0.12.6: + resolution: {integrity: sha512-pupx2gzWh3hDtm/iDW4WuCpljmyHbHi34r7ktOqpPGvyiM4MyfNgdJ3qMizXdgCErmvYC9Nn/qyjePy+4ss9Wg==} + peerDependencies: + svelte: ^3.56.0 || ^4.0.0 || ^5.0.0-next.120 + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.85.0: + resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==} + engines: {node: '>=10'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + hasBin: true + + prism-svelte@0.4.7: + resolution: {integrity: sha512-yABh19CYbM24V7aS7TuPYRNMqthxwbvx6FF/Rw920YbyBWO3tnyPIqRMgHuSVsLmuHkkBS1Akyof463FVdkeDQ==} + + prismjs@1.30.0: + resolution: {integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==} + engines: {node: '>=6'} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + + rollup@4.53.3: + resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + sade@1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + svelte-chartjs@3.1.5: + resolution: {integrity: sha512-ka2zh7v5FiwfAX1oMflZ0HkNkgjHjFqANgRyC+vNYXfxtx2ku68Zo+2KgbKeBH2nS1ThDqkIACPzGxy4T0UaoA==} + peerDependencies: + chart.js: ^3.5.0 || ^4.0.0 + svelte: ^4.0.0 + + svelte-check@4.3.4: + resolution: {integrity: sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==} + engines: {node: '>= 18.0.0'} + hasBin: true + peerDependencies: + svelte: ^4.0.0 || ^5.0.0-next.0 + typescript: '>=5.0.0' + + svelte@5.45.6: + resolution: {integrity: sha512-V3aVXthzPyPt1UB1wLEoXnEXpwPsvs7NHrR0xkCor8c11v71VqBj477MClqPZYyrcXrAH21sNGhOj9FJvSwXfQ==} + engines: {node: '>=18'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unist-util-is@4.1.0: + resolution: {integrity: sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==} + + unist-util-stringify-position@2.0.3: + resolution: {integrity: sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==} + + unist-util-visit-parents@3.1.1: + resolution: {integrity: sha512-1KROIZWo6bcMrZEwiH2UrXDyalAa0uqzWCxCJj6lPOvTve2WkfgCytoDTPaMnodXh1WrXOq0haVYHj99ynJlsg==} + + unist-util-visit@2.0.3: + resolution: {integrity: sha512-iJ4/RczbJMkD0712mGktuGpm/U4By4FfDonL7N/9tATGIF4imikjOuagyMY53tnZq3NP6BcmlrHhEKAfGWjh7Q==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vfile-message@2.0.4: + resolution: {integrity: sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==} + + vite@7.2.6: + resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + 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 + + vitefu@1.1.1: + resolution: {integrity: sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==} + peerDependencies: + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + peerDependenciesMeta: + vite: + optional: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + zimmerframe@1.1.4: + resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} + +snapshots: + + '@esbuild/aix-ppc64@0.25.12': + optional: true + + '@esbuild/android-arm64@0.25.12': + optional: true + + '@esbuild/android-arm@0.25.12': + optional: true + + '@esbuild/android-x64@0.25.12': + optional: true + + '@esbuild/darwin-arm64@0.25.12': + optional: true + + '@esbuild/darwin-x64@0.25.12': + optional: true + + '@esbuild/freebsd-arm64@0.25.12': + optional: true + + '@esbuild/freebsd-x64@0.25.12': + optional: true + + '@esbuild/linux-arm64@0.25.12': + optional: true + + '@esbuild/linux-arm@0.25.12': + optional: true + + '@esbuild/linux-ia32@0.25.12': + optional: true + + '@esbuild/linux-loong64@0.25.12': + optional: true + + '@esbuild/linux-mips64el@0.25.12': + optional: true + + '@esbuild/linux-ppc64@0.25.12': + optional: true + + '@esbuild/linux-riscv64@0.25.12': + optional: true + + '@esbuild/linux-s390x@0.25.12': + optional: true + + '@esbuild/linux-x64@0.25.12': + optional: true + + '@esbuild/netbsd-arm64@0.25.12': + optional: true + + '@esbuild/netbsd-x64@0.25.12': + optional: true + + '@esbuild/openbsd-arm64@0.25.12': + optional: true + + '@esbuild/openbsd-x64@0.25.12': + optional: true + + '@esbuild/openharmony-arm64@0.25.12': + optional: true + + '@esbuild/sunos-x64@0.25.12': + optional: true + + '@esbuild/win32-arm64@0.25.12': + optional: true + + '@esbuild/win32-ia32@0.25.12': + optional: true + + '@esbuild/win32-x64@0.25.12': + optional: true + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@kurkle/color@0.3.4': {} + + '@polka/url@1.0.0-next.29': {} + + '@rollup/plugin-commonjs@28.0.9(rollup@4.53.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + commondir: 1.0.1 + estree-walker: 2.0.2 + fdir: 6.5.0(picomatch@4.0.3) + is-reference: 1.2.1 + magic-string: 0.30.21 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.53.3 + + '@rollup/plugin-json@6.1.0(rollup@4.53.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + optionalDependencies: + rollup: 4.53.3 + + '@rollup/plugin-node-resolve@16.0.3(rollup@4.53.3)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + '@types/resolve': 1.20.2 + deepmerge: 4.3.1 + is-module: 1.0.0 + resolve: 1.22.11 + optionalDependencies: + rollup: 4.53.3 + + '@rollup/pluginutils@5.3.0(rollup@4.53.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.3 + optionalDependencies: + rollup: 4.53.3 + + '@rollup/rollup-android-arm-eabi@4.53.3': + optional: true + + '@rollup/rollup-android-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.53.3': + optional: true + + '@rollup/rollup-darwin-x64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.53.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.53.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.53.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.53.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.53.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.53.3': + optional: true + + '@standard-schema/spec@1.0.0': {} + + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': + dependencies: + acorn: 8.15.0 + + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))': + dependencies: + '@sveltejs/kit': 2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)) + + '@sveltejs/adapter-node@5.4.0(@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))': + dependencies: + '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3) + '@rollup/plugin-json': 6.1.0(rollup@4.53.3) + '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3) + '@sveltejs/kit': 2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)) + rollup: 4.53.3 + + '@sveltejs/kit@2.49.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1))': + dependencies: + '@standard-schema/spec': 1.0.0 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)) + '@types/cookie': 0.6.0 + acorn: 8.15.0 + cookie: 0.6.0 + devalue: 5.5.0 + esm-env: 1.2.2 + kleur: 4.1.5 + magic-string: 0.30.21 + mrmime: 2.0.1 + sade: 1.8.1 + set-cookie-parser: 2.7.2 + sirv: 3.0.2 + svelte: 5.45.6 + vite: 7.2.6(@types/node@24.10.1) + + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1))': + dependencies: + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)) + debug: 4.4.3 + svelte: 5.45.6 + vite: 7.2.6(@types/node@24.10.1) + transitivePeerDependencies: + - supports-color + + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1))': + dependencies: + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)))(svelte@5.45.6)(vite@7.2.6(@types/node@24.10.1)) + debug: 4.4.3 + deepmerge: 4.3.1 + magic-string: 0.30.21 + svelte: 5.45.6 + vite: 7.2.6(@types/node@24.10.1) + vitefu: 1.1.1(vite@7.2.6(@types/node@24.10.1)) + transitivePeerDependencies: + - supports-color + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 24.10.1 + + '@types/cookie@0.6.0': {} + + '@types/estree@1.0.8': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 2.0.11 + + '@types/node@24.10.1': + dependencies: + undici-types: 7.16.0 + + '@types/resolve@1.20.2': {} + + '@types/unist@2.0.11': {} + + acorn@8.15.0: {} + + aria-query@5.3.2: {} + + axobject-query@4.1.0: {} + + base64-js@1.5.1: {} + + better-sqlite3@12.5.0: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@1.1.4: {} + + clsx@2.1.1: {} + + commondir@1.0.1: {} + + cookie@0.6.0: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + deepmerge@4.3.1: {} + + detect-libc@2.1.2: {} + + devalue@5.5.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + esbuild@0.25.12: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.12 + '@esbuild/android-arm': 0.25.12 + '@esbuild/android-arm64': 0.25.12 + '@esbuild/android-x64': 0.25.12 + '@esbuild/darwin-arm64': 0.25.12 + '@esbuild/darwin-x64': 0.25.12 + '@esbuild/freebsd-arm64': 0.25.12 + '@esbuild/freebsd-x64': 0.25.12 + '@esbuild/linux-arm': 0.25.12 + '@esbuild/linux-arm64': 0.25.12 + '@esbuild/linux-ia32': 0.25.12 + '@esbuild/linux-loong64': 0.25.12 + '@esbuild/linux-mips64el': 0.25.12 + '@esbuild/linux-ppc64': 0.25.12 + '@esbuild/linux-riscv64': 0.25.12 + '@esbuild/linux-s390x': 0.25.12 + '@esbuild/linux-x64': 0.25.12 + '@esbuild/netbsd-arm64': 0.25.12 + '@esbuild/netbsd-x64': 0.25.12 + '@esbuild/openbsd-arm64': 0.25.12 + '@esbuild/openbsd-x64': 0.25.12 + '@esbuild/openharmony-arm64': 0.25.12 + '@esbuild/sunos-x64': 0.25.12 + '@esbuild/win32-arm64': 0.25.12 + '@esbuild/win32-ia32': 0.25.12 + '@esbuild/win32-x64': 0.25.12 + + esm-env@1.2.2: {} + + esrap@2.2.1: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + estree-walker@2.0.2: {} + + expand-template@2.0.3: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + file-uri-to-path@1.0.0: {} + + fs-constants@1.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + github-from-package@0.0.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ini@1.3.8: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-module@1.0.0: {} + + is-reference@1.2.1: + dependencies: + '@types/estree': 1.0.8 + + is-reference@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + kleur@4.1.5: {} + + locate-character@3.0.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdsvex@0.12.6(svelte@5.45.6): + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 2.0.11 + prism-svelte: 0.4.7 + prismjs: 1.30.0 + svelte: 5.45.6 + unist-util-visit: 2.0.3 + vfile-message: 2.0.4 + + mimic-response@3.1.0: {} + + minimist@1.2.8: {} + + mkdirp-classic@0.5.3: {} + + mri@1.2.0: {} + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + napi-build-utils@2.0.0: {} + + node-abi@3.85.0: + dependencies: + semver: 7.7.3 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + path-parse@1.0.7: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.85.0 + pump: 3.0.3 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + + prism-svelte@0.4.7: {} + + prismjs@1.30.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@4.1.2: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rollup@4.53.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.53.3 + '@rollup/rollup-android-arm64': 4.53.3 + '@rollup/rollup-darwin-arm64': 4.53.3 + '@rollup/rollup-darwin-x64': 4.53.3 + '@rollup/rollup-freebsd-arm64': 4.53.3 + '@rollup/rollup-freebsd-x64': 4.53.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.53.3 + '@rollup/rollup-linux-arm-musleabihf': 4.53.3 + '@rollup/rollup-linux-arm64-gnu': 4.53.3 + '@rollup/rollup-linux-arm64-musl': 4.53.3 + '@rollup/rollup-linux-loong64-gnu': 4.53.3 + '@rollup/rollup-linux-ppc64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-gnu': 4.53.3 + '@rollup/rollup-linux-riscv64-musl': 4.53.3 + '@rollup/rollup-linux-s390x-gnu': 4.53.3 + '@rollup/rollup-linux-x64-gnu': 4.53.3 + '@rollup/rollup-linux-x64-musl': 4.53.3 + '@rollup/rollup-openharmony-arm64': 4.53.3 + '@rollup/rollup-win32-arm64-msvc': 4.53.3 + '@rollup/rollup-win32-ia32-msvc': 4.53.3 + '@rollup/rollup-win32-x64-gnu': 4.53.3 + '@rollup/rollup-win32-x64-msvc': 4.53.3 + fsevents: 2.3.3 + + sade@1.8.1: + dependencies: + mri: 1.2.0 + + safe-buffer@5.2.1: {} + + semver@7.7.3: {} + + set-cookie-parser@2.7.2: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + source-map-js@1.2.1: {} + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-json-comments@2.0.1: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + svelte-chartjs@3.1.5(chart.js@4.5.1)(svelte@5.45.6): + dependencies: + chart.js: 4.5.1 + svelte: 5.45.6 + + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.45.6)(typescript@5.9.3): + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + chokidar: 4.0.3 + fdir: 6.5.0(picomatch@4.0.3) + picocolors: 1.1.1 + sade: 1.8.1 + svelte: 5.45.6 + typescript: 5.9.3 + transitivePeerDependencies: + - picomatch + + svelte@5.45.6: + dependencies: + '@jridgewell/remapping': 2.3.5 + '@jridgewell/sourcemap-codec': 1.5.5 + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@types/estree': 1.0.8 + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + clsx: 2.1.1 + devalue: 5.5.0 + esm-env: 1.2.2 + esrap: 2.2.1 + is-reference: 3.0.3 + locate-character: 3.0.0 + magic-string: 0.30.21 + zimmerframe: 1.1.4 + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.3 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + totalist@3.0.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + unist-util-is@4.1.0: {} + + unist-util-stringify-position@2.0.3: + dependencies: + '@types/unist': 2.0.11 + + unist-util-visit-parents@3.1.1: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + + unist-util-visit@2.0.3: + dependencies: + '@types/unist': 2.0.11 + unist-util-is: 4.1.0 + unist-util-visit-parents: 3.1.1 + + util-deprecate@1.0.2: {} + + vfile-message@2.0.4: + dependencies: + '@types/unist': 2.0.11 + unist-util-stringify-position: 2.0.3 + + vite@7.2.6(@types/node@24.10.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.10.1 + fsevents: 2.3.3 + + vitefu@1.1.1(vite@7.2.6(@types/node@24.10.1)): + optionalDependencies: + vite: 7.2.6(@types/node@24.10.1) + + wrappy@1.0.2: {} + + zimmerframe@1.1.4: {} diff --git a/dryer-counter/pnpm-workspace.yaml b/dryer-counter/pnpm-workspace.yaml new file mode 100644 index 0000000..f2721cf --- /dev/null +++ b/dryer-counter/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +onlyBuiltDependencies: + - better-sqlite3 + - esbuild diff --git a/dryer-counter/src/app.css b/dryer-counter/src/app.css new file mode 100644 index 0000000..1e6784b --- /dev/null +++ b/dryer-counter/src/app.css @@ -0,0 +1,432 @@ +/* Bold Pastel Papercut Theme for Dryer Counter App */ + +:root { + /* Bold Pastel Colors - Solid, vibrant */ + --pastel-pink: #FFB5C5; + --pastel-pink-light: #FFCDD8; + --pastel-coral: #FFA07A; + --pastel-blue: #87CEEB; + --pastel-blue-light: #B0E0E6; + --pastel-green: #98D8AA; + --pastel-mint: #7DDBA3; + --pastel-yellow: #FFE66D; + --pastel-orange: #FFB347; + --pastel-lavender: #C9B1FF; + --pastel-purple: #DDA0DD; + + /* Background Colors - Solid */ + --bg-cream: #FFF5E6; + --bg-card: #FFFFFF; + --bg-mint: #E8F5E9; + + /* Text Colors */ + --text-dark: #2D2D2D; + --text-medium: #4A4A4A; + --text-light: #6B6B6B; + + /* Papercut specific */ + --border-dark: #2D2D2D; + --border-width: 3px; + --paper-shadow: 6px 6px 0px var(--border-dark); + --paper-shadow-sm: 4px 4px 0px var(--border-dark); + --paper-shadow-hover: 8px 8px 0px var(--border-dark); + + /* Border Radius - Slightly rounded for paper feel */ + --radius-sm: 4px; + --radius-md: 8px; + --radius-lg: 12px; + + /* Spacing */ + --space-xs: 4px; + --space-sm: 8px; + --space-md: 16px; + --space-lg: 24px; + --space-xl: 32px; + --space-2xl: 48px; + + /* Transitions */ + --transition-fast: 100ms ease; + --transition-normal: 200ms ease; +} + +/* Reset and Base Styles */ +*, *::before, *::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-family: 'Comic Sans MS', 'Chalkboard', 'Marker Felt', sans-serif; + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +body { + background: var(--bg-cream); + color: var(--text-dark); + min-height: 100vh; + /* Paper texture effect with dots */ + background-image: + radial-gradient(circle, var(--pastel-lavender) 1px, transparent 1px), + radial-gradient(circle, var(--pastel-pink-light) 1px, transparent 1px); + background-size: 40px 40px, 60px 60px; + background-position: 0 0, 20px 20px; +} + +/* Typography */ +h1, h2, h3, h4, h5, h6 { + font-weight: 700; + line-height: 1.3; + color: var(--text-dark); +} + +h1 { + font-size: 2.5rem; +} + +h2 { + font-size: 1.5rem; +} + +h3 { + font-size: 1.25rem; +} + +/* Card Component - Papercut style */ +.card { + background: var(--bg-card); + border: var(--border-width) solid var(--border-dark); + border-radius: var(--radius-md); + box-shadow: var(--paper-shadow); + padding: var(--space-lg); + transition: transform var(--transition-fast), box-shadow var(--transition-fast); + position: relative; +} + +.card:hover { + transform: translate(-2px, -2px); + box-shadow: var(--paper-shadow-hover); +} + +/* Counter Card Styles - Bold solid colors */ +.counter-card { + text-align: center; + padding: var(--space-xl); + position: relative; + overflow: hidden; +} + +.counter-card.short { + background: var(--pastel-pink); +} + +.counter-card.long { + background: var(--pastel-blue); +} + +/* Papercut decoration effect */ +.counter-card::before { + content: ''; + position: absolute; + top: 10px; + left: 10px; + right: 10px; + bottom: 10px; + border: 2px dashed var(--border-dark); + border-radius: var(--radius-sm); + pointer-events: none; + opacity: 0.3; +} + +.counter-number { + font-size: 5rem; + font-weight: 900; + line-height: 1; + margin: var(--space-md) 0; + color: var(--text-dark); + text-shadow: 3px 3px 0px rgba(0,0,0,0.1); +} + +.counter-label { + font-size: 1.1rem; + color: var(--text-dark); + text-transform: uppercase; + letter-spacing: 2px; + font-weight: 700; +} + +/* Button Styles - Papercut */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: var(--space-sm); + padding: var(--space-sm) var(--space-lg); + border: var(--border-width) solid var(--border-dark); + border-radius: var(--radius-sm); + font-size: 1rem; + font-weight: 700; + cursor: pointer; + transition: all var(--transition-fast); + box-shadow: var(--paper-shadow-sm); + text-transform: uppercase; +} + +.btn:hover { + transform: translate(-2px, -2px); + box-shadow: var(--paper-shadow); +} + +.btn:active { + transform: translate(2px, 2px); + box-shadow: 2px 2px 0px var(--border-dark); +} + +.btn-primary { + background: var(--pastel-yellow); + color: var(--text-dark); +} + +.btn-secondary { + background: var(--pastel-mint); + color: var(--text-dark); +} + +/* Grid Layout */ +.grid { + display: grid; + gap: var(--space-lg); +} + +.grid-2 { + grid-template-columns: repeat(2, 1fr); +} + +.grid-3 { + grid-template-columns: repeat(3, 1fr); +} + +@media (max-width: 768px) { + .grid-2, .grid-3 { + grid-template-columns: 1fr; + } +} + +/* Container */ +.container { + max-width: 1000px; + margin: 0 auto; + padding: var(--space-lg); +} + +/* Header - Papercut style */ +.header { + background: var(--pastel-yellow); + border-bottom: var(--border-width) solid var(--border-dark); + padding: var(--space-md) var(--space-lg); + margin-bottom: var(--space-xl); + position: relative; +} + +/* Zigzag border effect for header */ +.header::after { + content: ''; + position: absolute; + bottom: -15px; + left: 0; + right: 0; + height: 15px; + background: + linear-gradient(135deg, var(--pastel-yellow) 25%, transparent 25%) -12px 0, + linear-gradient(225deg, var(--pastel-yellow) 25%, transparent 25%) -12px 0, + linear-gradient(315deg, var(--pastel-yellow) 25%, transparent 25%), + linear-gradient(45deg, var(--pastel-yellow) 25%, transparent 25%); + background-size: 24px 15px; + background-color: transparent; +} + +.header-content { + max-width: 1000px; + margin: 0 auto; + display: flex; + align-items: center; + justify-content: space-between; +} + +.logo { + display: flex; + align-items: center; + gap: var(--space-sm); + font-size: 1.75rem; + font-weight: 900; + color: var(--text-dark); + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Entry List - Papercut style */ +.entry-list { + list-style: none; +} + +.entry-item { + display: flex; + align-items: center; + gap: var(--space-md); + padding: var(--space-md); + border-bottom: 2px dashed var(--border-dark); + transition: background var(--transition-fast); +} + +.entry-item:last-child { + border-bottom: none; +} + +.entry-item:hover { + background: var(--bg-cream); +} + +.entry-icon { + width: 48px; + height: 48px; + border: var(--border-width) solid var(--border-dark); + border-radius: var(--radius-sm); + display: flex; + align-items: center; + justify-content: center; + font-size: 1.5rem; + box-shadow: 3px 3px 0px var(--border-dark); +} + +.entry-icon.short { + background: var(--pastel-pink); +} + +.entry-icon.long { + background: var(--pastel-blue); +} + +.entry-details { + flex: 1; +} + +.entry-type { + font-weight: 700; + color: var(--text-dark); + font-size: 1.1rem; +} + +.entry-time { + font-size: 0.9rem; + color: var(--text-medium); + font-weight: 500; +} + +/* Chart Container */ +.chart-container { + position: relative; + height: 300px; + width: 100%; + background: var(--bg-cream); + border: 2px dashed var(--border-dark); + border-radius: var(--radius-sm); + padding: var(--space-sm); +} + +/* Status Indicator - Papercut style */ +.status-dot { + width: 12px; + height: 12px; + border: 2px solid var(--border-dark); + border-radius: var(--radius-sm); + display: inline-block; + margin-right: var(--space-xs); +} + +.status-dot.connected { + background: var(--pastel-green); +} + +.status-dot.disconnected { + background: var(--pastel-coral); +} + +/* Loading State */ +.loading { + display: flex; + align-items: center; + justify-content: center; + padding: var(--space-xl); +} + +.spinner { + width: 40px; + height: 40px; + border: var(--border-width) solid var(--border-dark); + border-top-color: var(--pastel-yellow); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Empty State */ +.empty-state { + text-align: center; + padding: var(--space-2xl); + color: var(--text-medium); +} + +.empty-state-icon { + font-size: 4rem; + margin-bottom: var(--space-md); +} + +/* Section Title - Papercut style */ +.section-title { + display: flex; + align-items: center; + gap: var(--space-sm); + margin-bottom: var(--space-lg); + padding-bottom: var(--space-sm); + border-bottom: 3px solid var(--border-dark); +} + +.section-title span { + font-size: 1.5rem; +} + +.section-title h2 { + margin: 0; + text-transform: uppercase; + letter-spacing: 1px; +} + +/* Badge/Tag style */ +.tag { + display: inline-block; + padding: var(--space-xs) var(--space-sm); + background: var(--pastel-lavender); + border: 2px solid var(--border-dark); + border-radius: var(--radius-sm); + font-size: 0.85rem; + font-weight: 700; + text-transform: uppercase; +} + +/* Code blocks - Papercut style */ +code { + font-family: 'Courier New', monospace; + background: var(--pastel-yellow); + border: 2px solid var(--border-dark); + padding: 2px 6px; + border-radius: var(--radius-sm); + font-weight: 600; +} \ No newline at end of file diff --git a/dryer-counter/src/app.d.ts b/dryer-counter/src/app.d.ts new file mode 100644 index 0000000..da08e6d --- /dev/null +++ b/dryer-counter/src/app.d.ts @@ -0,0 +1,13 @@ +// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/dryer-counter/src/app.html b/dryer-counter/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/dryer-counter/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/dryer-counter/src/lib/assets/favicon.svg b/dryer-counter/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/dryer-counter/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/dryer-counter/src/lib/components/Calculator.svelte b/dryer-counter/src/lib/components/Calculator.svelte new file mode 100644 index 0000000..2d739e1 --- /dev/null +++ b/dryer-counter/src/lib/components/Calculator.svelte @@ -0,0 +1,541 @@ + + +
+
+
+ 💰 +

Kostenrechner

+
+ {#if !isEditing} + + {/if} +
+ + {#if isEditing} +
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ {:else if costs} +
+
+
+ Kosten Kurzzyklus: + {formatCurrency(costs.shortCostPerCycle)} +
+
+ Kosten Langzyklus: + {formatCurrency(costs.longCostPerCycle)} +
+
+
+ Gesamtkosten: + {formatCurrency(costs.totalCost)} +
+
+ +
+
+ + {costs.shortCount || shortCount} Kurzzyklen: + + + {formatCurrency(costs.totalShortCost)} + +
+
+ + {costs.longCount || longCount} Langzyklen: + + + {formatCurrency(costs.totalLongCost)} + +
+
+ + {#if costs.dateRange} +
+ 📅 Kosten fĂŒr Zeitraum: {getFormattedDateRange(costs.dateRange.start, costs.dateRange.end)} +
+ {:else} +
+ 📅 Kosten fĂŒr den gesamten Zeitraum +
+ {/if} + + {#if costs.settings.updated_at} +
+ Zuletzt aktualisiert: {new Date(costs.settings.updated_at).toLocaleDateString('de-DE')} +
+ {/if} +
+ {/if} +
+ +
+ + \ No newline at end of file diff --git a/dryer-counter/src/lib/components/Counter.svelte b/dryer-counter/src/lib/components/Counter.svelte new file mode 100644 index 0000000..8c45d06 --- /dev/null +++ b/dryer-counter/src/lib/components/Counter.svelte @@ -0,0 +1,145 @@ + + +
+
{type === 'short' ? 'QUICK' : 'FULL'}
+
{icons[type]}
+
{count}
+
{labels[type]}
+ +
+ + +
+ +
+
+ + \ No newline at end of file diff --git a/dryer-counter/src/lib/components/DateRangeSelector.svelte b/dryer-counter/src/lib/components/DateRangeSelector.svelte new file mode 100644 index 0000000..4114ae0 --- /dev/null +++ b/dryer-counter/src/lib/components/DateRangeSelector.svelte @@ -0,0 +1,418 @@ + + +
+
+
+ 📅 +

Zeitraumauswahl

+
+ +
+ +
+ +
+
+ + +
+ +
—
+ +
+ + +
+
+ + +
+
Schnellauswahl:
+
+ {#each quickPeriods as period} + + {/each} +
+
+ + +
+ Aktueller Zeitraum: + + {getDisplayDate(selectedStartDate)} bis {getDisplayDate(selectedEndDate)} + +
+
+
+ + \ No newline at end of file diff --git a/dryer-counter/src/lib/components/DryChart.svelte b/dryer-counter/src/lib/components/DryChart.svelte new file mode 100644 index 0000000..5682af4 --- /dev/null +++ b/dryer-counter/src/lib/components/DryChart.svelte @@ -0,0 +1,313 @@ + + +
+
+ 📊 +

+ {title} + {#if showDateRange && dateRange} + + ({getFormattedDateRange(dateRange.start, dateRange.end)}) + + {/if} +

+
+
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/dryer-counter/src/lib/components/RecentEntries.svelte b/dryer-counter/src/lib/components/RecentEntries.svelte new file mode 100644 index 0000000..fa08809 --- /dev/null +++ b/dryer-counter/src/lib/components/RecentEntries.svelte @@ -0,0 +1,506 @@ + + +
+
+ 📋 +

+ Letzte EintrÀge + {#if showDateRange && dateRange} + + ({getFormattedDateRange()}) + + {/if} +

+
+ + {#if entries.length === 0} +
+
đŸ§ș
+ {#if showDateRange && dateRange} +

Keine EintrÀge im gewÀhlten Zeitraum gefunden

+ {:else} +

Noch keine TrockengÀnge aufgezeichnet

+ {/if} +
+
+ {:else} +
+
    + {#each entries as entry, index (entry.id)} +
  • +
    + {typeIcons[entry.type]} +
    +
    +
    + {getEntryLabel(entry)} + + {actionIcons[entry.action]} + +
    +
    {formatTime(entry.created_at)}
    +
    +
    + {entry.action === 'increment' ? '✓' : '✗'} +
    +
  • + {/each} +
+ + {#if hasMore} +
+ {#if loading} +
🔄 Lade mehr EintrĂ€ge...
+ {:else} +
âŹ‡ïž Scrollen fĂŒr mehr EintrĂ€ge
+ {/if} +
+ {:else if entries.length > 20} +
+
✅
+

Alle EintrÀge geladen ({entries.length} insgesamt)

+
+ {/if} +
+ {/if} +
+ + \ No newline at end of file diff --git a/dryer-counter/src/lib/index.ts b/dryer-counter/src/lib/index.ts new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/dryer-counter/src/lib/index.ts @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/dryer-counter/src/lib/server/db.ts b/dryer-counter/src/lib/server/db.ts new file mode 100644 index 0000000..e8b27a3 --- /dev/null +++ b/dryer-counter/src/lib/server/db.ts @@ -0,0 +1,297 @@ +import Database from 'better-sqlite3'; +import { join } from 'path'; +import { existsSync, mkdirSync } from 'fs'; + +// Ensure the data directory exists +const dataDir = join(process.cwd(), 'data'); +if (!existsSync(dataDir)) { + mkdirSync(dataDir, { recursive: true }); +} + +// Create database connection +const dbPath = join(dataDir, 'dryer.db'); +export const db = new Database(dbPath); + +// Initialize database tables +export function initDatabase() { + // Create dry_entries table if it doesn't exist + const createTableQuery = ` + CREATE TABLE IF NOT EXISTS dry_entries ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL CHECK(type IN ('short', 'long')), + action TEXT NOT NULL DEFAULT 'increment' CHECK(action IN ('increment', 'decrement')), + created_at DATETIME DEFAULT (DATETIME('now', 'localtime')) + ) + `; + + // Create cost_settings table if it doesn't exist + const createCostSettingsTable = ` + CREATE TABLE IF NOT EXISTS cost_settings ( + id INTEGER PRIMARY KEY CHECK (id = 1), + eur_per_kwh REAL NOT NULL DEFAULT 0.3, + kwh_short_cycle REAL NOT NULL DEFAULT 1.0, + kwh_long_cycle REAL NOT NULL DEFAULT 1.6, + eur_short_cycle REAL, + eur_long_cycle REAL, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `; + + db.exec(createTableQuery); + db.exec(createCostSettingsTable); + + // Insert default cost settings if they don't exist + const insertDefaultSettings = ` + INSERT OR IGNORE INTO cost_settings (id, eur_per_kwh, kwh_short_cycle, kwh_long_cycle) + VALUES (1, 0.3, 1.0, 1.6) + `; + + db.exec(insertDefaultSettings); + console.log('Database initialized successfully'); +} + +// Insert a new dry entry (for backward compatibility, defaults to increment) +export function insertDryEntry(type: 'short' | 'long') { + const stmt = db.prepare('INSERT INTO dry_entries (type, action, created_at) VALUES (?, ?, DATETIME(\'now\', \'localtime\'))'); + const result = stmt.run(type, 'increment'); + return result; +} + +// Get count of dry entries by type (increments minus decrements) with optional date range +export function getDryCount(type?: 'short' | 'long', startDate?: string, endDate?: string) { + let query = ` + SELECT + SUM(CASE WHEN action = 'increment' THEN 1 ELSE -1 END) as count + FROM dry_entries + WHERE 1=1 + `; + const params: any[] = []; + + if (type) { + query += ' AND type = ?'; + params.push(type); + } + + if (startDate) { + query += ' AND DATE(created_at, \'localtime\') >= DATE(?, \'localtime\')'; + params.push(startDate); + } + + if (endDate) { + query += ' AND DATE(created_at, \'localtime\') <= DATE(?, \'localtime\')'; + params.push(endDate); + } + + const stmt = db.prepare(query); + const result = stmt.get(...params) as { count: number }; + return { count: Math.max(0, result.count || 0) }; // Ensure count is never negative +} + +// Get recent dry entries with optional date range filtering +export function getRecentEntries(limit = 10, offset = 0, startDate?: string, endDate?: string) { + let query = ` + SELECT id, type, action, created_at + FROM dry_entries + WHERE 1=1 + `; + const params: any[] = []; + + if (startDate) { + query += ' AND DATE(created_at, \'localtime\') >= DATE(?, \'localtime\')'; + params.push(startDate); + } + + if (endDate) { + query += ' AND DATE(created_at, \'localtime\') <= DATE(?, \'localtime\')'; + params.push(endDate); + } + + query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?'; + params.push(limit, offset); + + const stmt = db.prepare(query); + return stmt.all(...params) as Array<{ + id: number; + type: 'short' | 'long'; + action: 'increment' | 'decrement'; + created_at: string; + }>; +} + +// Get total count of entries for pagination with optional date range +export function getTotalEntriesCount(startDate?: string, endDate?: string) { + let query = 'SELECT COUNT(*) as count FROM dry_entries WHERE 1=1'; + const params: any[] = []; + + if (startDate) { + query += ' AND DATE(created_at, \'localtime\') >= DATE(?, \'localtime\')'; + params.push(startDate); + } + + if (endDate) { + query += ' AND DATE(created_at, \'localtime\') <= DATE(?, \'localtime\')'; + params.push(endDate); + } + + const stmt = db.prepare(query); + return stmt.get(...params) as { count: number }; +} + +// Get statistics for charts (grouped by day) with optional date range +export function getDailyStats(days = 30, startDate?: string, endDate?: string) { + let query = ` + SELECT + DATE(created_at, 'localtime') as date, + type, + SUM(CASE WHEN action = 'increment' THEN 1 ELSE -1 END) as count + FROM dry_entries + WHERE 1=1 + `; + const params: any[] = []; + + if (startDate && endDate) { + query += ' AND DATE(created_at, \'localtime\') >= DATE(?, \'localtime\') AND DATE(created_at, \'localtime\') <= DATE(?, \'localtime\')'; + params.push(startDate, endDate); + } else { + // Fallback to original behavior for backward compatibility - use local time + query += ` AND created_at >= DATETIME('now', 'localtime', '-${days} days')`; + } + + query += ' GROUP BY DATE(created_at, \'localtime\'), type ORDER BY date DESC'; + + const stmt = db.prepare(query); + const results = stmt.all(...params) as Array<{ + date: string; + type: 'short' | 'long'; + count: number; + }>; + + // Ensure counts are never negative + return results.map(result => ({ + ...result, + count: Math.max(0, result.count || 0) + })); +} + +// Get weekly statistics with optional date range +export function getWeeklyStats(weeks = 12, startDate?: string, endDate?: string) { + let query = ` + SELECT + strftime('%Y-W%W', created_at, 'localtime') as week, + type, + SUM(CASE WHEN action = 'increment' THEN 1 ELSE -1 END) as count + FROM dry_entries + WHERE 1=1 + `; + const params: any[] = []; + + if (startDate && endDate) { + query += ' AND DATE(created_at, \'localtime\') >= DATE(?, \'localtime\') AND DATE(created_at, \'localtime\') <= DATE(?, \'localtime\')'; + params.push(startDate, endDate); + } else { + // Fallback to original behavior for backward compatibility - use local time + query += ` AND created_at >= DATETIME('now', 'localtime', '-${weeks * 7} days')`; + } + + query += ' GROUP BY strftime(\'%Y-W%W\', created_at, \'localtime\'), type ORDER BY week DESC'; + + const stmt = db.prepare(query); + const results = stmt.all(...params) as Array<{ + week: string; + type: 'short' | 'long'; + count: number; + }>; + + // Ensure counts are never negative + return results.map(result => ({ + ...result, + count: Math.max(0, result.count || 0) + })); +} + +// Increment dryer count (add normal dryer entry) +export function incrementDryCount(type: 'short' | 'long') { + return insertDryEntry(type); +} + +// Decrement dryer count (add decrement entry) +export function decrementDryCount(type: 'short' | 'long') { + // Check current count before allowing decrement + const currentCount = getDryCount(type); + if (currentCount.count <= 0) { + throw new Error(`Cannot decrement ${type} cycle count: current count is 0`); + } + + const stmt = db.prepare('INSERT INTO dry_entries (type, action, created_at) VALUES (?, ?, DATETIME(\'now\', \'localtime\'))'); + const result = stmt.run(type, 'decrement'); + return result; +} + +// Cost settings functions +export interface CostSettings { + eur_per_kwh: number; + kwh_short_cycle: number; + kwh_long_cycle: number; + eur_short_cycle?: number; + eur_long_cycle?: number; + updated_at?: string; +} + +// Get cost settings +export function getCostSettings(): CostSettings { + const stmt = db.prepare('SELECT eur_per_kwh, kwh_short_cycle, kwh_long_cycle, eur_short_cycle, eur_long_cycle, updated_at FROM cost_settings WHERE id = 1'); + const result = stmt.get() as CostSettings | undefined; + + return result || { + eur_per_kwh: 0.3, + kwh_short_cycle: 1.0, + kwh_long_cycle: 1.6, + eur_short_cycle: undefined, + eur_long_cycle: undefined, + }; +} + +// Update cost settings +export function updateCostSettings(settings: Partial) { + const current = getCostSettings(); + const merged = { ...current, ...settings }; + + const stmt = db.prepare(` + UPDATE cost_settings + SET eur_per_kwh = ?, kwh_short_cycle = ?, kwh_long_cycle = ?, + eur_short_cycle = ?, eur_long_cycle = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = 1 + `); + + return stmt.run( + merged.eur_per_kwh, + merged.kwh_short_cycle, + merged.kwh_long_cycle, + merged.eur_short_cycle || null, + merged.eur_long_cycle || null + ); +} + +// Calculate costs for given counts +export function calculateCosts(shortCount: number, longCount: number) { + const settings = getCostSettings(); + + const shortCostPerCycle = settings.eur_short_cycle || (settings.kwh_short_cycle * settings.eur_per_kwh); + const longCostPerCycle = settings.eur_long_cycle || (settings.kwh_long_cycle * settings.eur_per_kwh); + + const totalShortCost = shortCount * shortCostPerCycle; + const totalLongCost = longCount * longCostPerCycle; + const totalCost = totalShortCost + totalLongCost; + + return { + shortCostPerCycle, + longCostPerCycle, + totalShortCost, + totalLongCost, + totalCost, + settings + }; +} + +// Initialize the database when the module is imported +initDatabase(); \ No newline at end of file diff --git a/dryer-counter/src/lib/server/sse.ts b/dryer-counter/src/lib/server/sse.ts new file mode 100644 index 0000000..052b7aa --- /dev/null +++ b/dryer-counter/src/lib/server/sse.ts @@ -0,0 +1,48 @@ +// Server-Sent Events management for real-time updates + +type SSEClient = { + id: string; + controller: ReadableStreamDefaultController; +}; + +// Store connected clients +const clients: Map = new Map(); + +// Generate a unique client ID +function generateClientId(): string { + return `client_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +// Add a new client +export function addClient(controller: ReadableStreamDefaultController): string { + const id = generateClientId(); + clients.set(id, { id, controller }); + console.log(`SSE Client connected: ${id}. Total clients: ${clients.size}`); + return id; +} + +// Remove a client +export function removeClient(id: string): void { + clients.delete(id); + console.log(`SSE Client disconnected: ${id}. Total clients: ${clients.size}`); +} + +// Broadcast an update to all connected clients +export function broadcastUpdate(event: { type: string; data: unknown }): void { + const message = `event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`; + + clients.forEach((client) => { + try { + client.controller.enqueue(new TextEncoder().encode(message)); + } catch (error) { + console.error(`Error sending to client ${client.id}:`, error); + // Remove disconnected clients + removeClient(client.id); + } + }); +} + +// Get the number of connected clients +export function getClientCount(): number { + return clients.size; +} \ No newline at end of file diff --git a/dryer-counter/src/routes/+layout.svelte b/dryer-counter/src/routes/+layout.svelte new file mode 100644 index 0000000..75d144c --- /dev/null +++ b/dryer-counter/src/routes/+layout.svelte @@ -0,0 +1,29 @@ + + + + + đŸ§ș Dryer Counter + + + +
+
+ +
+ + Live +
+
+
+ +
+ {@render children()} +
diff --git a/dryer-counter/src/routes/+page.server.ts b/dryer-counter/src/routes/+page.server.ts new file mode 100644 index 0000000..0910041 --- /dev/null +++ b/dryer-counter/src/routes/+page.server.ts @@ -0,0 +1,23 @@ +import type { PageServerLoad } from './$types'; +import { getDryCount, getRecentEntries, getDailyStats, getWeeklyStats } from '$lib/server/db'; + +export const load: PageServerLoad = async () => { + // Get counts + const shortCount = getDryCount('short').count; + const longCount = getDryCount('long').count; + + // Get recent entries (load 20 initially for better UX) + const recentEntries = getRecentEntries(20); + + // Get stats for charts + const dailyStats = getDailyStats(14); // Last 14 days + const weeklyStats = getWeeklyStats(8); // Last 8 weeks + + return { + shortCount, + longCount, + recentEntries, + dailyStats, + weeklyStats + }; +}; \ No newline at end of file diff --git a/dryer-counter/src/routes/+page.svelte b/dryer-counter/src/routes/+page.svelte new file mode 100644 index 0000000..2fad967 --- /dev/null +++ b/dryer-counter/src/routes/+page.svelte @@ -0,0 +1,416 @@ + + +
+ +
+ +
+ + +
+
+ handleIncrement('short')} + onDecrement={() => handleDecrement('short')} + /> + handleIncrement('long')} + onDecrement={() => handleDecrement('long')} + /> +
+
+ + +
+
+
+ đŸ§ș +
+ {shortCount + longCount} + Total Dry Cycles +
+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+
+ 🔌 +

Home Assistant Integration

+
+
+

Send POST requests to log dry cycles:

+ POST /api/dry +
+
+ Short cycle: + {`{ "type": "short" }`} +
+
+ Long cycle: + {`{ "type": "long" }`} +
+
+
+
+
+
+ + diff --git a/dryer-counter/src/routes/api/calculate-costs/+server.ts b/dryer-counter/src/routes/api/calculate-costs/+server.ts new file mode 100644 index 0000000..fe4b8ed --- /dev/null +++ b/dryer-counter/src/routes/api/calculate-costs/+server.ts @@ -0,0 +1,31 @@ +import { json, error } from '@sveltejs/kit'; +import { calculateCosts, getDryCount } from '$lib/server/db'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ url }) => { + try { + // Get query parameters + const startDate = url.searchParams.get('startDate'); + const endDate = url.searchParams.get('endDate'); + + // Get counts from database with optional date range filtering + const shortResult = getDryCount('short', startDate || undefined, endDate || undefined); + const longResult = getDryCount('long', startDate || undefined, endDate || undefined); + + const actualShortCount = shortResult.count; + const actualLongCount = longResult.count; + + // Calculate costs + const costs = calculateCosts(actualShortCount, actualLongCount); + + return json({ + ...costs, + shortCount: actualShortCount, + longCount: actualLongCount, + dateRange: startDate && endDate ? { start: startDate, end: endDate } : null + }); + } catch (err) { + console.error('Error calculating costs:', err); + return error(500, 'Internal server error'); + } +}; \ No newline at end of file diff --git a/dryer-counter/src/routes/api/cost-settings/+server.ts b/dryer-counter/src/routes/api/cost-settings/+server.ts new file mode 100644 index 0000000..307a778 --- /dev/null +++ b/dryer-counter/src/routes/api/cost-settings/+server.ts @@ -0,0 +1,57 @@ +import { json, error } from '@sveltejs/kit'; +import { getCostSettings, updateCostSettings } from '$lib/server/db'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async () => { + try { + const settings = getCostSettings(); + return json(settings); + } catch (err) { + console.error('Error fetching cost settings:', err); + return error(500, 'Internal server error'); + } +}; + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = await request.json(); + + // Validate required fields + if (body.eur_per_kwh === undefined || body.kwh_short_cycle === undefined || body.kwh_long_cycle === undefined) { + return error(400, 'Missing required fields: eur_per_kwh, kwh_short_cycle, kwh_long_cycle'); + } + + // Validate numeric values + if (isNaN(body.eur_per_kwh) || isNaN(body.kwh_short_cycle) || isNaN(body.kwh_long_cycle)) { + return error(400, 'All values must be numbers'); + } + + if (body.eur_per_kwh < 0 || body.kwh_short_cycle < 0 || body.kwh_long_cycle < 0) { + return error(400, 'All values must be non-negative'); + } + + if (body.eur_short_cycle && isNaN(body.eur_short_cycle)) { + return error(400, 'eur_short_cycle must be a number'); + } + + if (body.eur_long_cycle && isNaN(body.eur_long_cycle)) { + return error(400, 'eur_long_cycle must be a number'); + } + + // Update cost settings + const settings = { + eur_per_kwh: parseFloat(body.eur_per_kwh), + kwh_short_cycle: parseFloat(body.kwh_short_cycle), + kwh_long_cycle: parseFloat(body.kwh_long_cycle), + eur_short_cycle: body.eur_short_cycle ? parseFloat(body.eur_short_cycle) : undefined, + eur_long_cycle: body.eur_long_cycle ? parseFloat(body.eur_long_cycle) : undefined + }; + + updateCostSettings(settings); + + return json({ success: true, settings }); + } catch (err) { + console.error('Error updating cost settings:', err); + return error(500, 'Internal server error'); + } +}; \ No newline at end of file diff --git a/dryer-counter/src/routes/api/dry/+server.ts b/dryer-counter/src/routes/api/dry/+server.ts new file mode 100644 index 0000000..be8f1b0 --- /dev/null +++ b/dryer-counter/src/routes/api/dry/+server.ts @@ -0,0 +1,92 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { insertDryEntry, incrementDryCount, decrementDryCount, getDryCount, getRecentEntries } from '$lib/server/db'; +import { broadcastUpdate } from '$lib/server/sse'; + +export const POST: RequestHandler = async ({ request }) => { + try { + const body = await request.json(); + const { type, action } = body; + + // Validate type + if (type !== 'short' && type !== 'long') { + return json( + { error: 'Invalid type. Must be "short" or "long".' }, + { status: 400 } + ); + } + + // Validate action + if (!action || !['increment', 'decrement'].includes(action)) { + return json( + { error: 'Invalid action. Must be "increment" or "decrement".' }, + { status: 400 } + ); + } + + // Perform the action + let result; + if (action === 'increment') { + result = incrementDryCount(type); + } else { + result = decrementDryCount(type); + } + + // Get updated counts (always broadcast full data, not filtered by date range) + const shortCount = getDryCount('short').count; + const longCount = getDryCount('long').count; + const recentEntries = getRecentEntries(20); + + // Broadcast update to all connected clients + broadcastUpdate({ + type: 'new_entry', + data: { + shortCount, + longCount, + recentEntries + } + }); + + return json({ + success: true, + action, + type, + id: result.lastInsertRowid, + shortCount, + longCount + }); + } catch (error) { + console.error('Error handling dry entry:', error); + return json( + { error: 'Failed to handle dry entry' }, + { status: 500 } + ); + } +}; + +export const GET: RequestHandler = async ({ url }) => { + try { + const startDate = url.searchParams.get('startDate'); + const endDate = url.searchParams.get('endDate'); + + const shortCount = getDryCount('short', startDate, endDate).count; + const longCount = getDryCount('long', startDate, endDate).count; + const recentEntries = getRecentEntries(20, 0, startDate, endDate); + + return json({ + shortCount, + longCount, + recentEntries, + dateRange: { + startDate, + endDate + } + }); + } catch (error) { + console.error('Error fetching dry stats:', error); + return json( + { error: 'Failed to fetch dry stats' }, + { status: 500 } + ); + } +}; \ No newline at end of file diff --git a/dryer-counter/src/routes/api/dry/entries/+server.ts b/dryer-counter/src/routes/api/dry/entries/+server.ts new file mode 100644 index 0000000..bc02e83 --- /dev/null +++ b/dryer-counter/src/routes/api/dry/entries/+server.ts @@ -0,0 +1,48 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getRecentEntries, getTotalEntriesCount } from '$lib/server/db'; + +export const GET: RequestHandler = async ({ url }) => { + try { + const limit = parseInt(url.searchParams.get('limit') || '20'); + const offset = parseInt(url.searchParams.get('offset') || '0'); + const startDate = url.searchParams.get('startDate'); + const endDate = url.searchParams.get('endDate'); + + // Validate parameters + if (limit < 1 || limit > 100) { + return json( + { error: 'Limit must be between 1 and 100' }, + { status: 400 } + ); + } + + if (offset < 0) { + return json( + { error: 'Offset must be non-negative' }, + { status: 400 } + ); + } + + const entries = getRecentEntries(limit, offset, startDate, endDate); + const totalCount = getTotalEntriesCount(startDate, endDate); + + return json({ + entries, + totalCount: totalCount.count, + hasMore: offset + entries.length < totalCount.count, + currentOffset: offset, + currentLimit: limit, + dateRange: { + startDate, + endDate + } + }); + } catch (error) { + console.error('Error fetching paginated entries:', error); + return json( + { error: 'Failed to fetch entries' }, + { status: 500 } + ); + } +}; \ No newline at end of file diff --git a/dryer-counter/src/routes/api/dry/stats/+server.ts b/dryer-counter/src/routes/api/dry/stats/+server.ts new file mode 100644 index 0000000..ee5efd7 --- /dev/null +++ b/dryer-counter/src/routes/api/dry/stats/+server.ts @@ -0,0 +1,43 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { getDailyStats, getWeeklyStats, getDryCount } from '$lib/server/db'; + +export const GET: RequestHandler = async ({ url }) => { + try { + const period = url.searchParams.get('period') || 'daily'; + const days = parseInt(url.searchParams.get('days') || '30', 10); + const weeks = parseInt(url.searchParams.get('weeks') || '12', 10); + const startDate = url.searchParams.get('startDate'); + const endDate = url.searchParams.get('endDate'); + + let stats; + if (period === 'weekly') { + stats = getWeeklyStats(weeks, startDate, endDate); + } else { + stats = getDailyStats(days, startDate, endDate); + } + + // Get total counts (respect date range if provided) + const totalShort = getDryCount('short', startDate, endDate).count; + const totalLong = getDryCount('long', startDate, endDate).count; + + return json({ + stats, + totals: { + short: totalShort, + long: totalLong, + total: totalShort + totalLong + }, + dateRange: { + startDate, + endDate + } + }); + } catch (error) { + console.error('Error fetching stats:', error); + return json( + { error: 'Failed to fetch stats' }, + { status: 500 } + ); + } +}; \ No newline at end of file diff --git a/dryer-counter/src/routes/api/dry/stream/+server.ts b/dryer-counter/src/routes/api/dry/stream/+server.ts new file mode 100644 index 0000000..4dcd8ed --- /dev/null +++ b/dryer-counter/src/routes/api/dry/stream/+server.ts @@ -0,0 +1,30 @@ +import type { RequestHandler } from './$types'; +import { addClient, removeClient } from '$lib/server/sse'; + +export const GET: RequestHandler = async () => { + let clientId: string; + + const stream = new ReadableStream({ + start(controller) { + clientId = addClient(controller); + + // Send initial connection message + const connectMessage = `event: connected\ndata: ${JSON.stringify({ clientId })}\n\n`; + controller.enqueue(new TextEncoder().encode(connectMessage)); + }, + cancel() { + if (clientId) { + removeClient(clientId); + } + } + }); + + return new Response(stream, { + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + 'X-Accel-Buffering': 'no' // Disable buffering for nginx + } + }); +}; \ No newline at end of file diff --git a/dryer-counter/static/robots.txt b/dryer-counter/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/dryer-counter/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/dryer-counter/svelte.config.js b/dryer-counter/svelte.config.js new file mode 100644 index 0000000..35c8145 --- /dev/null +++ b/dryer-counter/svelte.config.js @@ -0,0 +1,20 @@ +import { mdsvex } from 'mdsvex'; +import adapter from '@sveltejs/adapter-node'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://svelte.dev/docs/kit/integrations + // for more information about preprocessors + preprocess: [vitePreprocess(), mdsvex()], + kit: { + // Using node adapter for SQLite support + adapter: adapter({ + // Optional: specify where to build the app + out: 'build' + }) + }, + extensions: ['.svelte', '.svx'] +}; + +export default config; diff --git a/dryer-counter/tsconfig.json b/dryer-counter/tsconfig.json new file mode 100644 index 0000000..2c2ed3c --- /dev/null +++ b/dryer-counter/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "rewriteRelativeImportExtensions": true, + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler" + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // To make changes to top-level options such as include and exclude, we recommend extending + // the generated config; see https://svelte.dev/docs/kit/configuration#typescript +} diff --git a/dryer-counter/vite.config.ts b/dryer-counter/vite.config.ts new file mode 100644 index 0000000..bbf8c7d --- /dev/null +++ b/dryer-counter/vite.config.ts @@ -0,0 +1,6 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [sveltekit()] +}); diff --git a/pocketbase/Dockerfile b/pocketbase/Dockerfile new file mode 100644 index 0000000..44e1071 --- /dev/null +++ b/pocketbase/Dockerfile @@ -0,0 +1,22 @@ +FROM alpine:latest + +ARG PB_VERSION=0.34.2 + +RUN apk add --no-cache \ + unzip \ + ca-certificates + +# download and unzip PocketBase +ADD https://github.com/pocketbase/pocketbase/releases/download/v${PB_VERSION}/pocketbase_${PB_VERSION}_linux_amd64.zip /tmp/pb.zip +RUN unzip /tmp/pb.zip -d /pb/ + +# uncomment to copy the local pb_migrations dir into the image +# COPY ./pb_migrations /pb/pb_migrations + +# uncomment to copy the local pb_hooks dir into the image +# COPY ./pb_hooks /pb/pb_hooks + +EXPOSE 8080 + +# start PocketBase +CMD ["/pb/pocketbase", "serve", "--http=0.0.0.0:8080"]