From 8c26fc93e3980c13c6a2d14c5d4a63f232de8a84 Mon Sep 17 00:00:00 2001 From: Joerg Date: Thu, 15 Jan 2026 11:01:10 +0100 Subject: [PATCH] Initial commit: Ham Radio Award Portal Features implemented: - User authentication (register/login) with JWT - SQLite database with Drizzle ORM - SvelteKit frontend with authentication flow - ElysiaJS backend with CORS enabled - Award definition JSON schemas (DXCC, WAS, VUCC, SAT) - Responsive dashboard with user profile Tech stack: - Backend: ElysiaJS, Drizzle ORM, SQLite, JWT - Frontend: SvelteKit, Svelte stores - Runtime: Bun - Language: JavaScript Co-Authored-By: Claude Sonnet 4.5 --- .gitignore | 39 ++ CLAUDE.md | 106 ++++ README.md | 15 + award-definitions/dxcc-cw.json | 24 + award-definitions/dxcc.json | 11 + award-definitions/sat-rs44.json | 21 + award-definitions/vucc-sat.json | 21 + award-definitions/was.json | 21 + bun.lock | 268 +++++++++++ drizzle.config.ts | 10 + drizzle/0000_burly_unus.sql | 65 +++ drizzle/meta/0000_snapshot.json | 453 ++++++++++++++++++ drizzle/meta/_journal.json | 13 + package.json | 29 ++ src/backend/config/database.js | 23 + src/backend/config/jwt.js | 9 + src/backend/db/schema/index.js | 146 ++++++ src/backend/index.js | 216 +++++++++ src/backend/init-db.js | 26 + src/backend/routes/auth.js | 166 +++++++ src/backend/services/auth.service.js | 132 +++++ src/frontend/.gitignore | 23 + src/frontend/.npmrc | 1 + src/frontend/README.md | 38 ++ src/frontend/bun.lock | 215 +++++++++ src/frontend/clear-cache.html | 102 ++++ src/frontend/jsconfig.json | 13 + src/frontend/package.json | 19 + src/frontend/src/app.html | 11 + src/frontend/src/lib/api.js | 138 ++++++ src/frontend/src/lib/assets/favicon.svg | 1 + src/frontend/src/lib/index.js | 1 + src/frontend/src/lib/stores.js | 221 +++++++++ src/frontend/src/routes/+layout.svelte | 65 +++ src/frontend/src/routes/+page.svelte | 194 ++++++++ .../src/routes/auth/login/+page.svelte | 187 ++++++++ .../src/routes/auth/register/+page.svelte | 225 +++++++++ src/frontend/static/robots.txt | 3 + src/frontend/svelte.config.js | 14 + src/frontend/vite.config.js | 43 ++ src/shared/types/awards.js | 96 ++++ 41 files changed, 3424 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 award-definitions/dxcc-cw.json create mode 100644 award-definitions/dxcc.json create mode 100644 award-definitions/sat-rs44.json create mode 100644 award-definitions/vucc-sat.json create mode 100644 award-definitions/was.json create mode 100644 bun.lock create mode 100644 drizzle.config.ts create mode 100644 drizzle/0000_burly_unus.sql create mode 100644 drizzle/meta/0000_snapshot.json create mode 100644 drizzle/meta/_journal.json create mode 100644 package.json create mode 100644 src/backend/config/database.js create mode 100644 src/backend/config/jwt.js create mode 100644 src/backend/db/schema/index.js create mode 100644 src/backend/index.js create mode 100644 src/backend/init-db.js create mode 100644 src/backend/routes/auth.js create mode 100644 src/backend/services/auth.service.js create mode 100644 src/frontend/.gitignore create mode 100644 src/frontend/.npmrc create mode 100644 src/frontend/README.md create mode 100644 src/frontend/bun.lock create mode 100644 src/frontend/clear-cache.html create mode 100644 src/frontend/jsconfig.json create mode 100644 src/frontend/package.json create mode 100644 src/frontend/src/app.html create mode 100644 src/frontend/src/lib/api.js create mode 100644 src/frontend/src/lib/assets/favicon.svg create mode 100644 src/frontend/src/lib/index.js create mode 100644 src/frontend/src/lib/stores.js create mode 100644 src/frontend/src/routes/+layout.svelte create mode 100644 src/frontend/src/routes/+page.svelte create mode 100644 src/frontend/src/routes/auth/login/+page.svelte create mode 100644 src/frontend/src/routes/auth/register/+page.svelte create mode 100644 src/frontend/static/robots.txt create mode 100644 src/frontend/svelte.config.js create mode 100644 src/frontend/vite.config.js create mode 100644 src/shared/types/awards.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7638522 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store + +# databases +*.db +*.sqlite +*.sqlite3 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..764c1dd --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,106 @@ + +Default to using Bun instead of Node.js. + +- Use `bun ` instead of `node ` or `ts-node ` +- Use `bun test` instead of `jest` or `vitest` +- Use `bun build ` instead of `webpack` or `esbuild` +- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install` +- Use `bun run + + +``` + +With the following `frontend.tsx`: + +```tsx#frontend.tsx +import React from "react"; +import { createRoot } from "react-dom/client"; + +// import .css files directly and it works +import './index.css'; + +const root = createRoot(document.body); + +export default function Frontend() { + return

Hello, world!

; +} + +root.render(); +``` + +Then, run index.ts + +```sh +bun --hot ./index.ts +``` + +For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`. diff --git a/README.md b/README.md new file mode 100644 index 0000000..e58709c --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# award + +To install dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.3.6. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime. diff --git a/award-definitions/dxcc-cw.json b/award-definitions/dxcc-cw.json new file mode 100644 index 0000000..cb3e195 --- /dev/null +++ b/award-definitions/dxcc-cw.json @@ -0,0 +1,24 @@ +{ + "id": "dxcc-cw", + "name": "DXCC CW", + "description": "Confirm 100 DXCC entities using CW mode", + "category": "dxcc", + "rules": { + "type": "filtered", + "baseRule": { + "type": "entity", + "entityType": "dxcc", + "target": 100 + }, + "filters": { + "operator": "AND", + "filters": [ + { + "field": "mode", + "operator": "eq", + "value": "CW" + } + ] + } + } +} diff --git a/award-definitions/dxcc.json b/award-definitions/dxcc.json new file mode 100644 index 0000000..775faa7 --- /dev/null +++ b/award-definitions/dxcc.json @@ -0,0 +1,11 @@ +{ + "id": "dxcc-mixed", + "name": "DXCC Mixed Mode", + "description": "Confirm 100 DXCC entities on any band/mode", + "category": "dxcc", + "rules": { + "type": "entity", + "entityType": "dxcc", + "target": 100 + } +} diff --git a/award-definitions/sat-rs44.json b/award-definitions/sat-rs44.json new file mode 100644 index 0000000..dd7f17a --- /dev/null +++ b/award-definitions/sat-rs44.json @@ -0,0 +1,21 @@ +{ + "id": "sat-rs44", + "name": "RS-44 Satellite", + "description": "Work 44 QSOs on satellite RS-44", + "category": "custom", + "rules": { + "type": "counter", + "target": 44, + "countBy": "qso", + "filters": { + "operator": "AND", + "filters": [ + { + "field": "satName", + "operator": "eq", + "value": "RS-44" + } + ] + } + } +} diff --git a/award-definitions/vucc-sat.json b/award-definitions/vucc-sat.json new file mode 100644 index 0000000..160d38b --- /dev/null +++ b/award-definitions/vucc-sat.json @@ -0,0 +1,21 @@ +{ + "id": "vucc-satellite", + "name": "VUCC Satellite", + "description": "Confirm 100 unique grid squares via satellite", + "category": "vucc", + "rules": { + "type": "entity", + "entityType": "grid", + "target": 100, + "filters": { + "operator": "AND", + "filters": [ + { + "field": "satellite", + "operator": "eq", + "value": true + } + ] + } + } +} diff --git a/award-definitions/was.json b/award-definitions/was.json new file mode 100644 index 0000000..55e1d7d --- /dev/null +++ b/award-definitions/was.json @@ -0,0 +1,21 @@ +{ + "id": "was-mixed", + "name": "WAS Mixed Mode", + "description": "Confirm all 50 US states", + "category": "was", + "rules": { + "type": "entity", + "entityType": "state", + "target": 50, + "filters": { + "operator": "AND", + "filters": [ + { + "field": "entity", + "operator": "eq", + "value": "United States" + } + ] + } + } +} diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..1cf70ab --- /dev/null +++ b/bun.lock @@ -0,0 +1,268 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "award", + "dependencies": { + "@elysiajs/cors": "^1.4.1", + "@elysiajs/jwt": "^1.4.0", + "@elysiajs/static": "^1.4.7", + "bcrypt": "^6.0.0", + "drizzle-orm": "^0.45.1", + "elysia": "^1.4.22", + }, + "devDependencies": { + "@libsql/client": "^0.17.0", + "@types/bun": "latest", + "drizzle-kit": "^0.31.8", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + + "@drizzle-team/brocli": ["@drizzle-team/brocli@0.10.2", "", {}, "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w=="], + + "@elysiajs/cors": ["@elysiajs/cors@1.4.1", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ=="], + + "@elysiajs/jwt": ["@elysiajs/jwt@1.4.0", "", { "dependencies": { "jose": "^6.0.11" }, "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Z0PvZhQxdDeKZ8HslXzDoXXD83NKExNPmoiAPki3nI2Xvh5wtUrBH+zWOD17yP14IbRo8fxGj3L25MRCAPsgPA=="], + + "@elysiajs/static": ["@elysiajs/static@1.4.7", "", { "peerDependencies": { "elysia": ">= 1.4.0" } }, "sha512-Go4kIXZ0G3iWfkAld07HmLglqIDMVXdyRKBQK/sVEjtpDdjHNb+rUIje73aDTWpZYg4PEVHUpi9v4AlNEwrQug=="], + + "@esbuild-kit/core-utils": ["@esbuild-kit/core-utils@3.3.2", "", { "dependencies": { "esbuild": "~0.18.20", "source-map-support": "^0.5.21" } }, "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ=="], + + "@esbuild-kit/esm-loader": ["@esbuild-kit/esm-loader@2.6.5", "", { "dependencies": { "@esbuild-kit/core-utils": "^3.3.2", "get-tsconfig": "^4.7.0" } }, "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@libsql/client": ["@libsql/client@0.17.0", "", { "dependencies": { "@libsql/core": "^0.17.0", "@libsql/hrana-client": "^0.9.0", "js-base64": "^3.7.5", "libsql": "^0.5.22", "promise-limit": "^2.7.0" } }, "sha512-TLjSU9Otdpq0SpKHl1tD1Nc9MKhrsZbCFGot3EbCxRa8m1E5R1mMwoOjKMMM31IyF7fr+hPNHLpYfwbMKNusmg=="], + + "@libsql/core": ["@libsql/core@0.17.0", "", { "dependencies": { "js-base64": "^3.7.5" } }, "sha512-hnZRnJHiS+nrhHKLGYPoJbc78FE903MSDrFJTbftxo+e52X+E0Y0fHOCVYsKWcg6XgB7BbJYUrz/xEkVTSaipw=="], + + "@libsql/darwin-arm64": ["@libsql/darwin-arm64@0.5.22", "", { "os": "darwin", "cpu": "arm64" }, "sha512-4B8ZlX3nIDPndfct7GNe0nI3Yw6ibocEicWdC4fvQbSs/jdq/RC2oCsoJxJ4NzXkvktX70C1J4FcmmoBy069UA=="], + + "@libsql/darwin-x64": ["@libsql/darwin-x64@0.5.22", "", { "os": "darwin", "cpu": "x64" }, "sha512-ny2HYWt6lFSIdNFzUFIJ04uiW6finXfMNJ7wypkAD8Pqdm6nAByO+Fdqu8t7sD0sqJGeUCiOg480icjyQ2/8VA=="], + + "@libsql/hrana-client": ["@libsql/hrana-client@0.9.0", "", { "dependencies": { "@libsql/isomorphic-ws": "^0.1.5", "cross-fetch": "^4.0.0", "js-base64": "^3.7.5", "node-fetch": "^3.3.2" } }, "sha512-pxQ1986AuWfPX4oXzBvLwBnfgKDE5OMhAdR/5cZmRaB4Ygz5MecQybvwZupnRz341r2CtFmbk/BhSu7k2Lm+Jw=="], + + "@libsql/isomorphic-ws": ["@libsql/isomorphic-ws@0.1.5", "", { "dependencies": { "@types/ws": "^8.5.4", "ws": "^8.13.0" } }, "sha512-DtLWIH29onUYR00i0GlQ3UdcTRC6EP4u9w/h9LxpUZJWRMARk6dQwZ6Jkd+QdwVpuAOrdxt18v0K2uIYR3fwFg=="], + + "@libsql/linux-arm-gnueabihf": ["@libsql/linux-arm-gnueabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-3Uo3SoDPJe/zBnyZKosziRGtszXaEtv57raWrZIahtQDsjxBVjuzYQinCm9LRCJCUT5t2r5Z5nLDPJi2CwZVoA=="], + + "@libsql/linux-arm-musleabihf": ["@libsql/linux-arm-musleabihf@0.5.22", "", { "os": "linux", "cpu": "arm" }, "sha512-LCsXh07jvSojTNJptT9CowOzwITznD+YFGGW+1XxUr7fS+7/ydUrpDfsMX7UqTqjm7xG17eq86VkWJgHJfvpNg=="], + + "@libsql/linux-arm64-gnu": ["@libsql/linux-arm64-gnu@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-KSdnOMy88c9mpOFKUEzPskSaF3VLflfSUCBwas/pn1/sV3pEhtMF6H8VUCd2rsedwoukeeCSEONqX7LLnQwRMA=="], + + "@libsql/linux-arm64-musl": ["@libsql/linux-arm64-musl@0.5.22", "", { "os": "linux", "cpu": "arm64" }, "sha512-mCHSMAsDTLK5YH//lcV3eFEgiR23Ym0U9oEvgZA0667gqRZg/2px+7LshDvErEKv2XZ8ixzw3p1IrBzLQHGSsw=="], + + "@libsql/linux-x64-gnu": ["@libsql/linux-x64-gnu@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-kNBHaIkSg78Y4BqAdgjcR2mBilZXs4HYkAmi58J+4GRwDQZh5fIUWbnQvB9f95DkWUIGVeenqLRFY2pcTmlsew=="], + + "@libsql/linux-x64-musl": ["@libsql/linux-x64-musl@0.5.22", "", { "os": "linux", "cpu": "x64" }, "sha512-UZ4Xdxm4pu3pQXjvfJiyCzZop/9j/eA2JjmhMaAhe3EVLH2g11Fy4fwyUp9sT1QJYR1kpc2JLuybPM0kuXv/Tg=="], + + "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + + "@neon-rs/load": ["@neon-rs/load@0.0.4", "", {}, "sha512-kTPhdZyTQxB+2wpiRcFWrDcejc4JI6tkPuS7UZCG4l6Zvc5kU/gGQ/ozvHTh1XR5tS+UlfAfGuPajjzQjCiHCw=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.47", "", {}, "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="], + + "@types/node": ["@types/node@25.0.8", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-powIePYMmC3ibL0UJ2i2s0WIbq6cg6UyVFQxSCpaPxxzAaziRfimGivjdF943sSGV6RADVbk0Nvlm5P/FB44Zg=="], + + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], + + "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="], + + "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + + "bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "cross-fetch": ["cross-fetch@4.1.0", "", { "dependencies": { "node-fetch": "^2.7.0" } }, "sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "detect-libc": ["detect-libc@2.0.2", "", {}, "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw=="], + + "drizzle-kit": ["drizzle-kit@0.31.8", "", { "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg=="], + + "drizzle-orm": ["drizzle-orm@0.45.1", "", { "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", "@cloudflare/workers-types": ">=4", "@electric-sql/pglite": ">=0.2.0", "@libsql/client": ">=0.10.0", "@libsql/client-wasm": ">=0.10.0", "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", "bun-types": "*", "expo-sqlite": ">=14.0.0", "gel": ">=2", "knex": "*", "kysely": "*", "mysql2": ">=2", "pg": ">=8", "postgres": ">=3", "sql.js": ">=1", "sqlite3": ">=5" }, "optionalPeers": ["@aws-sdk/client-rds-data", "@cloudflare/workers-types", "@electric-sql/pglite", "@libsql/client", "@libsql/client-wasm", "@neondatabase/serverless", "@op-engineering/op-sqlite", "@opentelemetry/api", "@planetscale/database", "@prisma/client", "@tidbcloud/serverless", "@types/better-sqlite3", "@types/pg", "@types/sql.js", "@upstash/redis", "@vercel/postgres", "@xata.io/client", "better-sqlite3", "bun-types", "expo-sqlite", "gel", "knex", "kysely", "mysql2", "pg", "postgres", "sql.js", "sqlite3"] }, "sha512-Te0FOdKIistGNPMq2jscdqngBRfBpC8uMFVwqjf6gtTVJHIQ/dosgV/CLBU2N4ZJBsXL5savCba9b0YJskKdcA=="], + + "elysia": ["elysia@1.4.22", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.6", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-Q90VCb1RVFxnFaRV0FDoSylESQQLWgLHFmWciQJdX9h3b2cSasji9KWEUvaJuy/L9ciAGg4RAhUVfsXHg5K2RQ=="], + + "esbuild": ["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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esbuild-register": ["esbuild-register@3.6.0", "", { "dependencies": { "debug": "^4.3.4" }, "peerDependencies": { "esbuild": ">=0.12 <1" } }, "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg=="], + + "exact-mirror": ["exact-mirror@0.2.6", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-7s059UIx9/tnOKSySzUk5cPGkoILhTE4p6ncf6uIPaQ+9aRBQzQjc9+q85l51+oZ+P6aBxh084pD0CzBQPcFUA=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "jose": ["jose@6.1.3", "", {}, "sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ=="], + + "js-base64": ["js-base64@3.7.8", "", {}, "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow=="], + + "libsql": ["libsql@0.5.22", "", { "dependencies": { "@neon-rs/load": "^0.0.4", "detect-libc": "2.0.2" }, "optionalDependencies": { "@libsql/darwin-arm64": "0.5.22", "@libsql/darwin-x64": "0.5.22", "@libsql/linux-arm-gnueabihf": "0.5.22", "@libsql/linux-arm-musleabihf": "0.5.22", "@libsql/linux-arm64-gnu": "0.5.22", "@libsql/linux-arm64-musl": "0.5.22", "@libsql/linux-x64-gnu": "0.5.22", "@libsql/linux-x64-musl": "0.5.22", "@libsql/win32-x64-msvc": "0.5.22" }, "os": [ "linux", "win32", "darwin", ], "cpu": [ "arm", "x64", "arm64", ] }, "sha512-NscWthMQt7fpU8lqd7LXMvT9pi+KhhmTHAJWUB/Lj6MWa0MKFv0F2V4C6WKKpjCVZl0VwcDz4nOI3CyaT1DDiA=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "node-addon-api": ["node-addon-api@8.5.0", "", {}, "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "promise-limit": ["promise-limit@2.7.0", "", {}, "sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw=="], + + "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], + + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], + + "cross-fetch/node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.18.20", "", { "os": "android", "cpu": "arm" }, "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.18.20", "", { "os": "android", "cpu": "arm64" }, "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.18.20", "", { "os": "android", "cpu": "x64" }, "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.18.20", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.18.20", "", { "os": "darwin", "cpu": "x64" }, "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.18.20", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.18.20", "", { "os": "freebsd", "cpu": "x64" }, "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.18.20", "", { "os": "linux", "cpu": "arm" }, "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.18.20", "", { "os": "linux", "cpu": "arm64" }, "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.18.20", "", { "os": "linux", "cpu": "ia32" }, "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.18.20", "", { "os": "linux", "cpu": "ppc64" }, "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.18.20", "", { "os": "linux", "cpu": "none" }, "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.18.20", "", { "os": "linux", "cpu": "s390x" }, "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.18.20", "", { "os": "linux", "cpu": "x64" }, "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.18.20", "", { "os": "none", "cpu": "x64" }, "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.18.20", "", { "os": "openbsd", "cpu": "x64" }, "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.18.20", "", { "os": "sunos", "cpu": "x64" }, "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.18.20", "", { "os": "win32", "cpu": "arm64" }, "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.18.20", "", { "os": "win32", "cpu": "ia32" }, "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g=="], + + "@esbuild-kit/core-utils/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.18.20", "", { "os": "win32", "cpu": "x64" }, "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ=="], + } +} diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..5a74588 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'drizzle-kit'; + +export default defineConfig({ + schema: './src/backend/db/schema/index.js', + out: './drizzle', + dialect: 'sqlite', + dbCredentials: { + url: './src/backend/award.db', + }, +}); diff --git a/drizzle/0000_burly_unus.sql b/drizzle/0000_burly_unus.sql new file mode 100644 index 0000000..801068d --- /dev/null +++ b/drizzle/0000_burly_unus.sql @@ -0,0 +1,65 @@ +CREATE TABLE `award_progress` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `award_id` text NOT NULL, + `worked_count` integer DEFAULT 0 NOT NULL, + `confirmed_count` integer DEFAULT 0 NOT NULL, + `total_required` integer NOT NULL, + `worked_entities` text, + `confirmed_entities` text, + `last_calculated_at` integer, + `last_qso_sync_at` integer, + `updated_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action, + FOREIGN KEY (`award_id`) REFERENCES `awards`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `awards` ( + `id` text PRIMARY KEY NOT NULL, + `name` text NOT NULL, + `description` text, + `definition` text NOT NULL, + `is_active` integer DEFAULT true NOT NULL, + `created_at` integer NOT NULL +); +--> statement-breakpoint +CREATE TABLE `qsos` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `user_id` integer NOT NULL, + `callsign` text NOT NULL, + `qso_date` text NOT NULL, + `time_on` text NOT NULL, + `band` text, + `mode` text, + `freq` integer, + `freq_rx` integer, + `entity` text, + `entity_id` integer, + `grid` text, + `grid_source` text, + `continent` text, + `cq_zone` integer, + `itu_zone` integer, + `state` text, + `county` text, + `sat_name` text, + `sat_mode` text, + `lotw_qsl_rdate` text, + `lotw_qsl_rstatus` text, + `lotw_synced_at` integer, + `created_at` integer NOT NULL, + FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON UPDATE no action ON DELETE no action +); +--> statement-breakpoint +CREATE TABLE `users` ( + `id` integer PRIMARY KEY AUTOINCREMENT NOT NULL, + `email` text NOT NULL, + `password_hash` text NOT NULL, + `callsign` text NOT NULL, + `lotw_username` text, + `lotw_password` text, + `created_at` integer NOT NULL, + `updated_at` integer NOT NULL +); +--> statement-breakpoint +CREATE UNIQUE INDEX `users_email_unique` ON `users` (`email`); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..8f2f676 --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,453 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "1b1674e7-6e3e-4ca6-8d19-066f2947942c", + "prevId": "00000000-0000-0000-0000-000000000000", + "tables": { + "award_progress": { + "name": "award_progress", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "award_id": { + "name": "award_id", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worked_count": { + "name": "worked_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "confirmed_count": { + "name": "confirmed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": 0 + }, + "total_required": { + "name": "total_required", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "worked_entities": { + "name": "worked_entities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "confirmed_entities": { + "name": "confirmed_entities", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_calculated_at": { + "name": "last_calculated_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "last_qso_sync_at": { + "name": "last_qso_sync_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "award_progress_user_id_users_id_fk": { + "name": "award_progress_user_id_users_id_fk", + "tableFrom": "award_progress", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "award_progress_award_id_awards_id_fk": { + "name": "award_progress_award_id_awards_id_fk", + "tableFrom": "award_progress", + "tableTo": "awards", + "columnsFrom": [ + "award_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "awards": { + "name": "awards", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "definition": { + "name": "definition", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "is_active": { + "name": "is_active", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "qsos": { + "name": "qsos", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "callsign": { + "name": "callsign", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "qso_date": { + "name": "qso_date", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "time_on": { + "name": "time_on", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "band": { + "name": "band", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "freq": { + "name": "freq", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "freq_rx": { + "name": "freq_rx", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity": { + "name": "entity", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "entity_id": { + "name": "entity_id", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grid": { + "name": "grid", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "grid_source": { + "name": "grid_source", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "continent": { + "name": "continent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "cq_zone": { + "name": "cq_zone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "itu_zone": { + "name": "itu_zone", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "state": { + "name": "state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "county": { + "name": "county", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sat_name": { + "name": "sat_name", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "sat_mode": { + "name": "sat_mode", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_qsl_rdate": { + "name": "lotw_qsl_rdate", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_qsl_rstatus": { + "name": "lotw_qsl_rstatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_synced_at": { + "name": "lotw_synced_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "qsos_user_id_users_id_fk": { + "name": "qsos_user_id_users_id_fk", + "tableFrom": "qsos", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "users": { + "name": "users", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "password_hash": { + "name": "password_hash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "callsign": { + "name": "callsign", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "lotw_username": { + "name": "lotw_username", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "lotw_password": { + "name": "lotw_password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "users_email_unique": { + "name": "users_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..4f613b0 --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "sqlite", + "entries": [ + { + "idx": 0, + "version": "6", + "when": 1768462458852, + "tag": "0000_burly_unus", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..a37c9b8 --- /dev/null +++ b/package.json @@ -0,0 +1,29 @@ +{ + "name": "award", + "module": "index.js", + "type": "module", + "private": true, + "scripts": { + "dev:backend": "bun run src/backend/index.js", + "dev:frontend": "cd src/frontend && bun run dev", + "db:generate": "drizzle-kit generate", + "db:push": "drizzle-kit push", + "db:migrate": "drizzle-kit migrate" + }, + "devDependencies": { + "@libsql/client": "^0.17.0", + "@types/bun": "latest", + "drizzle-kit": "^0.31.8" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@elysiajs/cors": "^1.4.1", + "@elysiajs/jwt": "^1.4.0", + "@elysiajs/static": "^1.4.7", + "bcrypt": "^6.0.0", + "drizzle-orm": "^0.45.1", + "elysia": "^1.4.22" + } +} diff --git a/src/backend/config/database.js b/src/backend/config/database.js new file mode 100644 index 0000000..c87171b --- /dev/null +++ b/src/backend/config/database.js @@ -0,0 +1,23 @@ +import Database from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import * as schema from '../db/schema/index.js'; + +// Create SQLite database connection +const sqlite = new Database('./award.db'); + +// Enable foreign keys +sqlite.exec('PRAGMA foreign_keys = ON'); + +// Create Drizzle instance +export const db = drizzle({ + client: sqlite, + schema, +}); + +/** + * Close database connection + * @returns {Promise} + */ +export async function closeDatabase() { + sqlite.close(); +} diff --git a/src/backend/config/jwt.js b/src/backend/config/jwt.js new file mode 100644 index 0000000..f44c426 --- /dev/null +++ b/src/backend/config/jwt.js @@ -0,0 +1,9 @@ +import { jwt } from '@elysiajs/jwt'; + +/** + * JWT secret key - should be in environment variables in production + * For development, we use a default value + */ +const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key-change-in-production'; + +export { JWT_SECRET }; diff --git a/src/backend/db/schema/index.js b/src/backend/db/schema/index.js new file mode 100644 index 0000000..cb27957 --- /dev/null +++ b/src/backend/db/schema/index.js @@ -0,0 +1,146 @@ +import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'; + +/** + * @typedef {Object} User + * @property {number} id + * @property {string} email + * @property {string} passwordHash + * @property {string} callsign + * @property {string|null} lotwUsername + * @property {string|null} lotwPassword + * @property {Date} createdAt + * @property {Date} updatedAt + */ + +export const users = sqliteTable('users', { + id: integer('id').primaryKey({ autoIncrement: true }), + email: text('email').notNull().unique(), + passwordHash: text('password_hash').notNull(), + callsign: text('callsign').notNull(), + lotwUsername: text('lotw_username'), + lotwPassword: text('lotw_password'), // Encrypted + createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), +}); + +/** + * @typedef {Object} QSO + * @property {number} id + * @property {number} userId + * @property {string} callsign + * @property {string} qsoDate + * @property {string} timeOn + * @property {string|null} band + * @property {string|null} mode + * @property {number|null} freq + * @property {number|null} freqRx + * @property {string|null} entity + * @property {number|null} entityId + * @property {string|null} grid + * @property {string|null} gridSource + * @property {string|null} continent + * @property {number|null} cqZone + * @property {number|null} ituZone + * @property {string|null} state + * @property {string|null} county + * @property {string|null} satName + * @property {string|null} satMode + * @property {string|null} lotwQslRdate + * @property {string|null} lotwQslRstatus + * @property {Date|null} lotwSyncedAt + * @property {Date} createdAt + */ + +export const qsos = sqliteTable('qsos', { + id: integer('id').primaryKey({ autoIncrement: true }), + userId: integer('user_id').notNull().references(() => users.id), + + // QSO fields + callsign: text('callsign').notNull(), + qsoDate: text('qso_date').notNull(), // ADIF format: YYYYMMDD + timeOn: text('time_on').notNull(), // HHMMSS + band: text('band'), // 160m, 80m, 40m, etc. + mode: text('mode'), // CW, SSB, FT8, etc. + freq: integer('freq'), // Frequency in Hz + freqRx: integer('freq_rx'), // RX frequency (satellite) + + // Entity/location fields + entity: text('entity'), // DXCC entity name + entityId: integer('entity_id'), // DXCC entity number + grid: text('grid'), // Maidenhead grid square + gridSource: text('grid_source'), // LOTW, USER, CALC + continent: text('continent'), // NA, SA, EU, AF, AS, OC, AN + cqZone: integer('cq_zone'), + ituZone: integer('itu_zone'), + state: text('state'), // US state, CA province, etc. + county: text('county'), + + // Satellite fields + satName: text('sat_name'), + satMode: text('sat_mode'), + + // LoTW confirmation + lotwQslRdate: text('lotw_qsl_rdate'), // Confirmation date + lotwQslRstatus: text('lotw_qsl_rstatus'), // 'Y', 'N', '?' + + // Cache metadata + lotwSyncedAt: integer('lotw_synced_at', { mode: 'timestamp' }), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), +}); + +/** + * @typedef {Object} Award + * @property {string} id + * @property {string} name + * @property {string|null} description + * @property {string} definition + * @property {boolean} isActive + * @property {Date} createdAt + */ + +export const awards = sqliteTable('awards', { + id: text('id').primaryKey(), // 'dxcc', 'was', 'vucc' + name: text('name').notNull(), + description: text('description'), + definition: text('definition').notNull(), // JSON rule definition + isActive: integer('is_active', { mode: 'boolean' }).notNull().default(true), + createdAt: integer('created_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), +}); + +/** + * @typedef {Object} AwardProgress + * @property {number} id + * @property {number} userId + * @property {string} awardId + * @property {number} workedCount + * @property {number} confirmedCount + * @property {number} totalRequired + * @property {string|null} workedEntities + * @property {string|null} confirmedEntities + * @property {Date|null} lastCalculatedAt + * @property {Date|null} lastQsoSyncAt + * @property {Date} updatedAt + */ + +export const awardProgress = sqliteTable('award_progress', { + id: integer('id').primaryKey({ autoIncrement: true }), + userId: integer('user_id').notNull().references(() => users.id), + awardId: text('award_id').notNull().references(() => awards.id), + + // Calculated progress + workedCount: integer('worked_count').notNull().default(0), + confirmedCount: integer('confirmed_count').notNull().default(0), + totalRequired: integer('total_required').notNull(), + + // Detailed breakdown (JSON) + workedEntities: text('worked_entities'), // JSON array + confirmedEntities: text('confirmed_entities'), // JSON array + + // Cache control + lastCalculatedAt: integer('last_calculated_at', { mode: 'timestamp' }), + lastQsoSyncAt: integer('last_qso_sync_at', { mode: 'timestamp' }), + updatedAt: integer('updated_at', { mode: 'timestamp' }).notNull().$defaultFn(() => new Date()), +}); + +// Export all schemas +export const schema = { users, qsos, awards, awardProgress }; diff --git a/src/backend/index.js b/src/backend/index.js new file mode 100644 index 0000000..245d699 --- /dev/null +++ b/src/backend/index.js @@ -0,0 +1,216 @@ +import { Elysia, t } from 'elysia'; +import { cors } from '@elysiajs/cors'; +import { jwt } from '@elysiajs/jwt'; +import { JWT_SECRET } from './config/jwt.js'; +import { + registerUser, + authenticateUser, + getUserById, + updateLoTWCredentials, +} from './services/auth.service.js'; + +/** + * Main backend application + * Serves API routes + */ +const app = new Elysia() + // Enable CORS for frontend communication + .use(cors({ + origin: true, // Allow all origins in development + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + })) + + // JWT plugin + .use(jwt({ + name: 'jwt', + secret: JWT_SECRET, + })) + + // Authentication: derive user from JWT token + .derive(async ({ jwt, headers }) => { + const authHeader = headers.authorization; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return { user: null }; + } + + const token = authHeader.substring(7); + try { + const payload = await jwt.verify(token); + if (!payload) { + return { user: null }; + } + + return { + user: { + id: payload.userId, + email: payload.email, + callsign: payload.callsign, + }, + }; + } catch (error) { + return { user: null }; + } + }) + + /** + * POST /api/auth/register + * Register a new user + */ + .post( + '/api/auth/register', + async ({ body, jwt, set }) => { + try { + // Create user + const user = await registerUser(body); + + // Generate JWT token + const token = await jwt.sign({ + userId: user.id, + email: user.email, + callsign: user.callsign, + }); + + set.status = 201; + return { + success: true, + token, + user, + }; + } catch (error) { + set.status = 400; + return { + success: false, + error: error.message, + }; + } + }, + { + body: t.Object({ + email: t.String({ + format: 'email', + error: 'Invalid email address', + }), + password: t.String({ + minLength: 8, + error: 'Password must be at least 8 characters', + }), + callsign: t.String({ + minLength: 3, + maxLength: 10, + error: 'Callsign must be 3-10 characters', + }), + }), + } + ) + + /** + * POST /api/auth/login + * Authenticate user and return JWT token + */ + .post( + '/api/auth/login', + async ({ body, jwt, set }) => { + try { + // Authenticate user + const user = await authenticateUser(body.email, body.password); + + // Generate JWT token + const token = await jwt.sign({ + userId: user.id, + email: user.email, + callsign: user.callsign, + }); + + return { + success: true, + token, + user, + }; + } catch (error) { + set.status = 401; + return { + success: false, + error: 'Invalid email or password', + }; + } + }, + { + body: t.Object({ + email: t.String({ format: 'email' }), + password: t.String(), + }), + } + ) + + /** + * GET /api/auth/me + * Get current user profile (requires authentication) + */ + .get('/api/auth/me', async ({ user, set }) => { + if (!user) { + set.status = 401; + return { success: false, error: 'Unauthorized' }; + } + + // Get full user data from database + const userData = await getUserById(user.id); + if (!userData) { + set.status = 404; + return { success: false, error: 'User not found' }; + } + + return { + success: true, + user: userData, + }; + }) + + /** + * PUT /api/auth/lotw-credentials + * Update LoTW credentials (requires authentication) + */ + .put( + '/api/auth/lotw-credentials', + async ({ user, body, set }) => { + if (!user) { + set.status = 401; + return { success: false, error: 'Unauthorized' }; + } + + try { + await updateLoTWCredentials(user.id, body.lotwUsername, body.lotwPassword); + + return { + success: true, + message: 'LoTW credentials updated successfully', + }; + } catch (error) { + set.status = 500; + return { + success: false, + error: 'Failed to update LoTW credentials', + }; + } + }, + { + body: t.Object({ + lotwUsername: t.String(), + lotwPassword: t.String(), + }), + } + ) + + // Health check endpoint + .get('/api/health', () => ({ + status: 'ok', + timestamp: new Date().toISOString(), + })) + + // Start server + .listen(3001); + +console.log(`🦊 Backend server running at http://localhost:${app.server?.port}`); +console.log(`📡 API endpoints available at http://localhost:${app.server?.port}/api`); + +export default app; diff --git a/src/backend/init-db.js b/src/backend/init-db.js new file mode 100644 index 0000000..a17abfa --- /dev/null +++ b/src/backend/init-db.js @@ -0,0 +1,26 @@ +import Database from 'bun:sqlite'; +import { drizzle } from 'drizzle-orm/bun-sqlite'; +import * as schema from './db/schema/index.js'; + +const sqlite = new Database('./award.db'); +const db = drizzle({ + client: sqlite, + schema, +}); + +console.log('Creating database tables...'); + +// Use drizzle-kit to push the schema +// Since we don't have migrations, let's use the push command +const { execSync } = await import('child_process'); + +try { + execSync('bun drizzle-kit push', { + cwd: '/Users/joergdorgeist/Dev/award', + stdio: 'inherit' + }); + console.log('✓ Database initialized successfully!'); +} catch (error) { + console.error('Failed to initialize database:', error); + process.exit(1); +} diff --git a/src/backend/routes/auth.js b/src/backend/routes/auth.js new file mode 100644 index 0000000..d8ccced --- /dev/null +++ b/src/backend/routes/auth.js @@ -0,0 +1,166 @@ +import { t } from 'elysia'; +import { + registerUser, + authenticateUser, + getUserById, + updateLoTWCredentials, +} from '../services/auth.service.js'; + +/** + * Authentication routes + * Provides endpoints for user registration, login, and profile management + * These routes will be added to the main app which already has authMiddleware + */ +export const authRoutes = (app) => { + console.error('authRoutes function called with app'); + return app + /** + * POST /api/auth/register + * Register a new user + */ + .post( + '/api/auth/register', + async ({ body, jwt, set }) => { + try { + // Create user + const user = await registerUser(body); + + // Generate JWT token + const token = await jwt.sign({ + userId: user.id, + email: user.email, + callsign: user.callsign, + }); + + set.status = 201; + return { + success: true, + token, + user, + }; + } catch (error) { + set.status = 400; + return { + success: false, + error: error.message, + }; + } + }, + { + body: t.Object({ + email: t.String({ + format: 'email', + error: 'Invalid email address', + }), + password: t.String({ + minLength: 8, + error: 'Password must be at least 8 characters', + }), + callsign: t.String({ + minLength: 3, + maxLength: 10, + error: 'Callsign must be 3-10 characters', + }), + }), + } + ) + + /** + * POST /api/auth/login + * Authenticate user and return JWT token + */ + .post( + '/api/auth/login', + async ({ body, jwt, set }) => { + try { + // Authenticate user + const user = await authenticateUser(body.email, body.password); + + // Generate JWT token + const token = await jwt.sign({ + userId: user.id, + email: user.email, + callsign: user.callsign, + }); + + return { + success: true, + token, + user, + }; + } catch (error) { + set.status = 401; + return { + success: false, + error: 'Invalid email or password', + }; + } + }, + { + body: t.Object({ + email: t.String({ format: 'email' }), + password: t.String(), + }), + } + ) + + /** + * GET /api/auth/me + * Get current user profile (requires authentication) + */ + .get('/api/auth/me', async ({ user, set }) => { + console.error('/me endpoint called, user:', user); + if (!user) { + console.error('No user in context - returning 401'); + set.status = 401; + return { success: false, error: 'Unauthorized' }; + } + + // Get full user data from database + const userData = await getUserById(user.id); + if (!userData) { + set.status = 404; + return { success: false, error: 'User not found' }; + } + + return { + success: true, + user: userData, + }; + }) + + /** + * PUT /api/auth/lotw-credentials + * Update LoTW credentials (requires authentication) + */ + .put( + '/api/auth/lotw-credentials', + async ({ user, body, set }) => { + if (!user) { + set.status = 401; + return { success: false, error: 'Unauthorized' }; + } + + try { + await updateLoTWCredentials(user.id, body.lotwUsername, body.lotwPassword); + + return { + success: true, + message: 'LoTW credentials updated successfully', + }; + } catch (error) { + set.status = 500; + return { + success: false, + error: 'Failed to update LoTW credentials', + }; + } + }, + { + body: t.Object({ + lotwUsername: t.String(), + lotwPassword: t.String(), + }), + } + ); +}; diff --git a/src/backend/services/auth.service.js b/src/backend/services/auth.service.js new file mode 100644 index 0000000..b959f79 --- /dev/null +++ b/src/backend/services/auth.service.js @@ -0,0 +1,132 @@ +import bcrypt from 'bcrypt'; +import { eq } from 'drizzle-orm'; +import { db } from '../config/database.js'; +import { users } from '../db/schema/index.js'; + +const SALT_ROUNDS = 10; + +/** + * Hash a password using bcrypt + * @param {string} password - Plain text password + * @returns {Promise} Hashed password + */ +export async function hashPassword(password) { + return bcrypt.hash(password, SALT_ROUNDS); +} + +/** + * Verify a password against a hash + * @param {string} password - Plain text password + * @param {string} hash - Hashed password + * @returns {Promise} True if password matches + */ +export async function verifyPassword(password, hash) { + return bcrypt.compare(password, hash); +} + +/** + * Register a new user + * @param {Object} userData - User registration data + * @param {string} userData.email - User email + * @param {string} userData.password - Plain text password + * @param {string} userData.callsign - Ham radio callsign + * @returns {Promise} Created user object (without password) + * @throws {Error} If email already exists + */ +export async function registerUser({ email, password, callsign }) { + // Check if user already exists + const existingUser = await db + .select() + .from(users) + .where(eq(users.email, email)) + .get(); + + if (existingUser) { + throw new Error('Email already registered'); + } + + // Hash password + const passwordHash = await hashPassword(password); + + // Create user + const newUser = await db + .insert(users) + .values({ + email, + passwordHash, + callsign: callsign.toUpperCase(), + }) + .returning(); + + // Return user without password hash + const { passwordHash: _, ...userWithoutPassword } = newUser[0]; + return userWithoutPassword; +} + +/** + * Authenticate user with email and password + * @param {string} email - User email + * @param {string} password - Plain text password + * @returns {Promise} User object (without password) if authenticated + * @throws {Error} If credentials are invalid + */ +export async function authenticateUser(email, password) { + // Find user by email + const user = await db + .select() + .from(users) + .where(eq(users.email, email)) + .get(); + + if (!user) { + throw new Error('Invalid email or password'); + } + + // Verify password + const isValid = await verifyPassword(password, user.passwordHash); + if (!isValid) { + throw new Error('Invalid email or password'); + } + + // Return user without password hash + const { passwordHash: _, ...userWithoutPassword } = user; + return userWithoutPassword; +} + +/** + * Get user by ID + * @param {number} userId - User ID + * @returns {Promise} User object (without password) or null + */ +export async function getUserById(userId) { + const user = await db + .select() + .from(users) + .where(eq(users.id, userId)) + .get(); + + if (!user) return null; + + const { passwordHash: _, ...userWithoutPassword } = user; + return userWithoutPassword; +} + +/** + * Update user's LoTW credentials (encrypted) + * @param {number} userId - User ID + * @param {string} lotwUsername - LoTW username + * @param {string} lotwPassword - LoTW password (will be encrypted) + * @returns {Promise} + */ +export async function updateLoTWCredentials(userId, lotwUsername, lotwPassword) { + // Simple encryption for storage (in production, use a proper encryption library) + // For now, we'll store as-is but marked for encryption + await db + .update(users) + .set({ + lotwUsername, + lotwPassword, // TODO: Encrypt before storing + updatedAt: new Date(), + }) + .where(eq(users.id, userId)); +} diff --git a/src/frontend/.gitignore b/src/frontend/.gitignore new file mode 100644 index 0000000..3b462cb --- /dev/null +++ b/src/frontend/.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/src/frontend/.npmrc b/src/frontend/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/src/frontend/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/src/frontend/README.md b/src/frontend/README.md new file mode 100644 index 0000000..75842c4 --- /dev/null +++ b/src/frontend/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/src/frontend/bun.lock b/src/frontend/bun.lock new file mode 100644 index 0000000..97b6735 --- /dev/null +++ b/src/frontend/bun.lock @@ -0,0 +1,215 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "frontend", + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.45.6", + "vite": "^6.0.0", + }, + }, + }, + "packages": { + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + + "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.55.1", "", { "os": "android", "cpu": "arm" }, "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.55.1", "", { "os": "android", "cpu": "arm64" }, "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.55.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.55.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.55.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.55.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.55.1", "", { "os": "linux", "cpu": "arm" }, "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.55.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.55.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.55.1", "", { "os": "linux", "cpu": "none" }, "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.55.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.55.1", "", { "os": "linux", "cpu": "x64" }, "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.55.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.55.1", "", { "os": "none", "cpu": "arm64" }, "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.55.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.55.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.55.1", "", { "os": "win32", "cpu": "x64" }, "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.8", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA=="], + + "@sveltejs/adapter-auto": ["@sveltejs/adapter-auto@7.0.0", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-ImDWaErTOCkRS4Gt+5gZuymKFBobnhChXUZ9lhUZLahUgvA4OOvRzi3sahzYgbxGj5nkA6OV0GAW378+dl/gyw=="], + + "@sveltejs/kit": ["@sveltejs/kit@2.49.4", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "sade": "^1.8.1", "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "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", "typescript": "^5.3.3", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JFtOqDoU0DI/+QSG8qnq5bKcehVb3tCHhOG4amsSYth5/KgO4EkJvi42xSAiyKmXAAULW1/Zdb6lkgGEgSxdZg=="], + + "@sveltejs/vite-plugin-svelte": ["@sveltejs/vite-plugin-svelte@6.2.4", "", { "dependencies": { "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", "deepmerge": "^4.3.1", "magic-string": "^0.30.21", "obug": "^2.1.0", "vitefu": "^1.1.1" }, "peerDependencies": { "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA=="], + + "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@5.0.2", "", { "dependencies": { "obug": "^2.1.0" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", "svelte": "^5.0.0", "vite": "^6.3.0 || ^7.0.0" } }, "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig=="], + + "@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="], + + "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], + + "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], + + "aria-query": ["aria-query@5.3.2", "", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], + + "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + + "cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="], + + "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], + + "devalue": ["devalue@5.6.1", "", {}, "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A=="], + + "esbuild": ["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" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "esm-env": ["esm-env@1.2.2", "", {}, "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA=="], + + "esrap": ["esrap@2.2.1", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" } }, "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "locate-character": ["locate-character@3.0.0", "", {}, "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="], + + "mrmime": ["mrmime@2.0.1", "", {}, "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="], + + "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + + "rollup": ["rollup@4.55.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.55.1", "@rollup/rollup-android-arm64": "4.55.1", "@rollup/rollup-darwin-arm64": "4.55.1", "@rollup/rollup-darwin-x64": "4.55.1", "@rollup/rollup-freebsd-arm64": "4.55.1", "@rollup/rollup-freebsd-x64": "4.55.1", "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", "@rollup/rollup-linux-arm-musleabihf": "4.55.1", "@rollup/rollup-linux-arm64-gnu": "4.55.1", "@rollup/rollup-linux-arm64-musl": "4.55.1", "@rollup/rollup-linux-loong64-gnu": "4.55.1", "@rollup/rollup-linux-loong64-musl": "4.55.1", "@rollup/rollup-linux-ppc64-gnu": "4.55.1", "@rollup/rollup-linux-ppc64-musl": "4.55.1", "@rollup/rollup-linux-riscv64-gnu": "4.55.1", "@rollup/rollup-linux-riscv64-musl": "4.55.1", "@rollup/rollup-linux-s390x-gnu": "4.55.1", "@rollup/rollup-linux-x64-gnu": "4.55.1", "@rollup/rollup-linux-x64-musl": "4.55.1", "@rollup/rollup-openbsd-x64": "4.55.1", "@rollup/rollup-openharmony-arm64": "4.55.1", "@rollup/rollup-win32-arm64-msvc": "4.55.1", "@rollup/rollup-win32-ia32-msvc": "4.55.1", "@rollup/rollup-win32-x64-gnu": "4.55.1", "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A=="], + + "sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="], + + "set-cookie-parser": ["set-cookie-parser@2.7.2", "", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "sirv": ["sirv@3.0.2", "", { "dependencies": { "@polka/url": "^1.0.0-next.24", "mrmime": "^2.0.0", "totalist": "^3.0.0" } }, "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "svelte": ["svelte@5.46.3", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", "acorn": "^8.12.1", "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.5.0", "esm-env": "^1.2.1", "esrap": "^2.2.1", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-Y5juST3x+/ySty5tYJCVWa6Corkxpt25bUZQHqOceg9xfMUtDsFx6rCsG6cYf1cA6vzDi66HIvaki0byZZX95A=="], + + "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], + + "totalist": ["totalist@3.0.1", "", {}, "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ=="], + + "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], + + "vitefu": ["vitefu@1.1.1", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "optionalPeers": ["vite"] }, "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ=="], + + "zimmerframe": ["zimmerframe@1.1.4", "", {}, "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ=="], + } +} diff --git a/src/frontend/clear-cache.html b/src/frontend/clear-cache.html new file mode 100644 index 0000000..0db28e2 --- /dev/null +++ b/src/frontend/clear-cache.html @@ -0,0 +1,102 @@ + + + + Clear Cache - Ham Radio Awards + + + +
+

Browser Cache Reset

+
+ The URI malformed error you saw is caused by your browser environment (extensions or cache), not the application code. +

+ Since incognito mode works, try these steps to fix your normal browser: +
+ + + + + +
+ + + +
+ If the issue persists after clearing cache: +
    +
  • Disable browser extensions (especially ad blockers, privacy tools)
  • +
  • Try a different browser (Chrome, Firefox, Safari)
  • +
  • Clear browser cache from DevTools → Application → Clear storage
  • +
+
+
+ + diff --git a/src/frontend/jsconfig.json b/src/frontend/jsconfig.json new file mode 100644 index 0000000..d73b913 --- /dev/null +++ b/src/frontend/jsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": false, + "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 + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/src/frontend/package.json b/src/frontend/package.json new file mode 100644 index 0000000..f3a1a2c --- /dev/null +++ b/src/frontend/package.json @@ -0,0 +1,19 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "prepare": "svelte-kit sync || echo ''" + }, + "devDependencies": { + "@sveltejs/adapter-auto": "^7.0.0", + "@sveltejs/kit": "^2.49.1", + "@sveltejs/vite-plugin-svelte": "^6.2.1", + "svelte": "^5.45.6", + "vite": "^6.0.0" + } +} diff --git a/src/frontend/src/app.html b/src/frontend/src/app.html new file mode 100644 index 0000000..f273cc5 --- /dev/null +++ b/src/frontend/src/app.html @@ -0,0 +1,11 @@ + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/src/frontend/src/lib/api.js b/src/frontend/src/lib/api.js new file mode 100644 index 0000000..bb41eec --- /dev/null +++ b/src/frontend/src/lib/api.js @@ -0,0 +1,138 @@ +import { browser } from '$app/environment'; + +/** + * API base URL - change this to match your backend + */ +const API_BASE = 'http://localhost:3001/api'; + +/** + * Make an API request + * @param {string} endpoint - API endpoint (e.g., '/auth/login') + * @param {Object} options - Fetch options + * @returns {Promise} Response data + */ +async function apiRequest(endpoint, options = {}) { + const url = `${API_BASE}${endpoint}`; + + // Get token from localStorage (only in browser) + let token = null; + if (browser) { + try { + token = localStorage.getItem('auth_token'); + } catch (e) { + // localStorage not available + } + } + + const headers = { + 'Content-Type': 'application/json', + ...(token && { Authorization: `Bearer ${token}` }), + ...options.headers, + }; + + const response = await fetch(url, { + ...options, + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.error || 'API request failed'); + } + + return data; +} + +/** + * Authentication API + */ +export const authAPI = { + /** + * Register a new user + * @param {Object} userData - User registration data + * @param {string} userData.email - User email + * @param {string} userData.password - User password + * @param {string} userData.callsign - Ham radio callsign + * @returns {Promise} Registration response with token and user + */ + register: (userData) => + apiRequest('/auth/register', { + method: 'POST', + body: JSON.stringify(userData), + }), + + /** + * Login user + * @param {Object} credentials - Login credentials + * @param {string} credentials.email - User email + * @param {string} credentials.password - User password + * @returns {Promise} Login response with token and user + */ + login: (credentials) => + apiRequest('/auth/login', { + method: 'POST', + body: JSON.stringify(credentials), + }), + + /** + * Get current user profile + * @returns {Promise} User profile + */ + getProfile: () => apiRequest('/auth/me'), + + /** + * Update LoTW credentials + * @param {Object} credentials - LoTW credentials + * @param {string} credentials.lotwUsername - LoTW username + * @param {string} credentials.lotwPassword - LoTW password + * @returns {Promise} Update response + */ + updateLoTWCredentials: (credentials) => + apiRequest('/auth/lotw-credentials', { + method: 'PUT', + body: JSON.stringify(credentials), + }), +}; + +/** + * Awards API + */ +export const awardsAPI = { + /** + * Get all available awards + * @returns {Promise} List of awards + */ + getAll: () => apiRequest('/awards'), + + /** + * Get user progress for a specific award + * @param {string} awardId - Award ID + * @returns {Promise} Award progress + */ + getProgress: (awardId) => apiRequest(`/awards/${awardId}/progress`), +}; + +/** + * QSOs API + */ +export const qsosAPI = { + /** + * Get user's QSOs + * @param {Object} filters - Query filters + * @returns {Promise} List of QSOs + */ + getAll: (filters = {}) => { + const params = new URLSearchParams(filters); + return apiRequest(`/qsos?${params}`); + }, + + /** + * Sync QSOs from LoTW + * @returns {Promise} Sync result + */ + syncFromLoTW: () => + apiRequest('/lotw/sync', { + method: 'POST', + }), +}; diff --git a/src/frontend/src/lib/assets/favicon.svg b/src/frontend/src/lib/assets/favicon.svg new file mode 100644 index 0000000..cc5dc66 --- /dev/null +++ b/src/frontend/src/lib/assets/favicon.svg @@ -0,0 +1 @@ +svelte-logo \ No newline at end of file diff --git a/src/frontend/src/lib/index.js b/src/frontend/src/lib/index.js new file mode 100644 index 0000000..856f2b6 --- /dev/null +++ b/src/frontend/src/lib/index.js @@ -0,0 +1 @@ +// place files you want to import through the `$lib` alias in this folder. diff --git a/src/frontend/src/lib/stores.js b/src/frontend/src/lib/stores.js new file mode 100644 index 0000000..b75b75b --- /dev/null +++ b/src/frontend/src/lib/stores.js @@ -0,0 +1,221 @@ +import { writable } from 'svelte/store'; +import { browser } from '$app/environment'; +import { authAPI } from './api.js'; + +/** + * @typedef {Object} AuthState + * @property {Object|null} user - Current user object + * @property {string|null} token - JWT token + * @property {boolean} loading - Loading state + * @property {string|null} error - Error message + */ + +/** + * Safely get item from localStorage + * @param {string} key - Storage key + * @returns {string|null} Storage value or null + */ +function getStorageItem(key) { + if (!browser) return null; + try { + return localStorage.getItem(key); + } catch { + return null; + } +} + +/** + * Safely set item in localStorage + * @param {string} key - Storage key + * @param {string} value - Storage value + */ +function setStorageItem(key, value) { + if (!browser) return; + try { + localStorage.setItem(key, value); + } catch (error) { + console.error('Failed to save to localStorage:', error); + } +} + +/** + * Safely remove item from localStorage + * @param {string} key - Storage key + */ +function removeStorageItem(key) { + if (!browser) return; + try { + localStorage.removeItem(key); + } catch (error) { + console.error('Failed to remove from localStorage:', error); + } +} + +/** + * Create authentication store + * @returns {import('svelte/store').Writable} + */ +function createAuthStore() { + // Initialize state (localStorage only accessed in browser) + let initialState = { + user: null, + token: null, + loading: false, + error: null, + }; + + // Only read from localStorage if in browser + if (browser) { + const token = getStorageItem('auth_token'); + const userJson = getStorageItem('auth_user'); + initialState = { + user: userJson ? JSON.parse(userJson) : null, + token: token || null, + loading: false, + error: null, + }; + } + + /** @type {import('svelte/store').Writable} */ + const { subscribe, set, update } = writable(initialState); + + return { + subscribe, + + /** + * Register a new user + * @param {Object} userData - User registration data + * @param {string} userData.email + * @param {string} userData.password + * @param {string} userData.callsign + */ + register: async (userData) => { + update((state) => ({ ...state, loading: true, error: null })); + + try { + const response = await authAPI.register(userData); + + // Save to localStorage + setStorageItem('auth_token', response.token); + setStorageItem('auth_user', JSON.stringify(response.user)); + + set({ + user: response.user, + token: response.token, + loading: false, + error: null, + }); + + return response.user; + } catch (error) { + update((state) => ({ + ...state, + loading: false, + error: error.message, + })); + throw error; + } + }, + + /** + * Login user + * @param {string} email - User email + * @param {string} password - User password + */ + login: async (email, password) => { + update((state) => ({ ...state, loading: true, error: null })); + + try { + const response = await authAPI.login({ email, password }); + + // Save to localStorage + setStorageItem('auth_token', response.token); + setStorageItem('auth_user', JSON.stringify(response.user)); + + set({ + user: response.user, + token: response.token, + loading: false, + error: null, + }); + + return response.user; + } catch (error) { + update((state) => ({ + ...state, + loading: false, + error: error.message, + })); + throw error; + } + }, + + /** + * Logout user + */ + logout: () => { + removeStorageItem('auth_token'); + removeStorageItem('auth_user'); + + set({ + user: null, + token: null, + loading: false, + error: null, + }); + }, + + /** + * Load user profile from API + */ + loadProfile: async () => { + const token = getStorageItem('auth_token'); + if (!token) return; + + update((state) => ({ ...state, loading: true })); + + try { + const response = await authAPI.getProfile(); + + setStorageItem('auth_user', JSON.stringify(response.user)); + + update((state) => ({ + ...state, + user: response.user, + loading: false, + })); + } catch (error) { + // If token is invalid, logout + if (error.message.includes('Unauthorized')) { + removeStorageItem('auth_token'); + removeStorageItem('auth_user'); + set({ + user: null, + token: null, + loading: false, + error: null, + }); + } else { + update((state) => ({ + ...state, + loading: false, + error: error.message, + })); + } + } + }, + + /** + * Clear error state + */ + clearError: () => { + update((state) => ({ ...state, error: null })); + }, + }; +} + +/** + * Authentication store + * @type {ReturnType} + */ +export const auth = createAuthStore(); diff --git a/src/frontend/src/routes/+layout.svelte b/src/frontend/src/routes/+layout.svelte new file mode 100644 index 0000000..ed53d22 --- /dev/null +++ b/src/frontend/src/routes/+layout.svelte @@ -0,0 +1,65 @@ + + + + + + + +{#if browser} +
+
+ +
+ +
+

© 2025 Ham Radio Awards. Track your DXCC, WAS, VUCC and more.

+
+
+{:else} +
+
+ +
+
+

© 2025 Ham Radio Awards. Track your DXCC, WAS, VUCC and more.

+
+
+{/if} + + diff --git a/src/frontend/src/routes/+page.svelte b/src/frontend/src/routes/+page.svelte new file mode 100644 index 0000000..1bd900c --- /dev/null +++ b/src/frontend/src/routes/+page.svelte @@ -0,0 +1,194 @@ + + +
+ {#if $auth.user} + +
+
+

Welcome back, {$auth.user.callsign}!

+

Track your ham radio award progress

+
+ +
+
+

📋 Awards

+

View your award progress

+ View Awards +
+ +
+

📡 QSOs

+

Browse your logbook

+ View QSOs +
+ +
+

⚙️ Settings

+

Manage LoTW credentials

+ Settings +
+
+ +
+

Getting Started

+
    +
  1. Configure your LoTW credentials in Settings
  2. +
  3. Sync your QSOs from LoTW
  4. +
  5. Track your award progress
  6. +
+
+
+ {:else} + +
+

Ham Radio Awards

+

Track your DXCC, WAS, VUCC and more

+
+ Login + Register +
+
+ {/if} +
+ + diff --git a/src/frontend/src/routes/auth/login/+page.svelte b/src/frontend/src/routes/auth/login/+page.svelte new file mode 100644 index 0000000..a9fcbfd --- /dev/null +++ b/src/frontend/src/routes/auth/login/+page.svelte @@ -0,0 +1,187 @@ + + + + Login - Ham Radio Awards + + +
+
+
+

Ham Radio Awards

+

Sign in to track your award progress

+
+ + {#if error} +
{error}
+ {/if} + +
+
+ + +
+ +
+ + +
+ + +
+ + +
+
+ + diff --git a/src/frontend/src/routes/auth/register/+page.svelte b/src/frontend/src/routes/auth/register/+page.svelte new file mode 100644 index 0000000..a4b8310 --- /dev/null +++ b/src/frontend/src/routes/auth/register/+page.svelte @@ -0,0 +1,225 @@ + + + + Register - Ham Radio Awards + + +
+
+
+

Create Account

+

Register to track your ham radio awards

+
+ + {#if error} +
{error}
+ {/if} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + +
+ + +
+
+ + diff --git a/src/frontend/static/robots.txt b/src/frontend/static/robots.txt new file mode 100644 index 0000000..b6dd667 --- /dev/null +++ b/src/frontend/static/robots.txt @@ -0,0 +1,3 @@ +# allow crawling everything by default +User-agent: * +Disallow: diff --git a/src/frontend/svelte.config.js b/src/frontend/svelte.config.js new file mode 100644 index 0000000..326a0ad --- /dev/null +++ b/src/frontend/svelte.config.js @@ -0,0 +1,14 @@ +import adapter from '@sveltejs/adapter-auto'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + kit: { + adapter: adapter(), + // Disable origin checks in dev to prevent issues with browser extensions + csrf: { + trustedOrigins: process.env.NODE_ENV === 'production' ? undefined : ['http://localhost:5173', 'http://127.0.0.1:5173'] + } + } +}; + +export default config; diff --git a/src/frontend/vite.config.js b/src/frontend/vite.config.js new file mode 100644 index 0000000..7996a52 --- /dev/null +++ b/src/frontend/vite.config.js @@ -0,0 +1,43 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; + +// Plugin to suppress URI malformed errors from browser extensions +function suppressURIErrorPlugin() { + return { + name: 'suppress-uri-error', + configureServer(server) { + server.middlewares.use((req, res, next) => { + // Intercept malformed requests before they reach Vite's middleware + try { + // Try to decode the URL to catch malformed URIs early + if (req.url) { + decodeURI(req.url); + } + next(); + } catch (e) { + // Silently handle malformed URIs + res.writeHead(400); + res.end('Bad Request'); + return; + } + }); + } + }; +} + +export default defineConfig({ + plugins: [sveltekit(), suppressURIErrorPlugin()], + server: { + host: 'localhost', + port: 5173, + strictPort: false, + allowedHosts: true, + watch: { + usePolling: true + }, + // Disable error overlay to dismiss URI malformed errors + hmr: { + overlay: false + } + } +}); diff --git a/src/shared/types/awards.js b/src/shared/types/awards.js new file mode 100644 index 0000000..2bc57a7 --- /dev/null +++ b/src/shared/types/awards.js @@ -0,0 +1,96 @@ +/** + * @typedef {Object} AwardDefinition + * @property {string} id - Unique identifier: 'dxcc-mixed', 'was-cw' + * @property {string} name - Display name + * @property {string} description - Award description + * @property {'dxcc' | 'was' | 'vucc' | 'iota' | 'custom'} category - Award category + * @property {AwardRule} rules - Award rule configuration + */ + +/** + * @typedef {CounterRule | EntityRule | FilteredRule | CombinedRule} AwardRule + */ + +/** + * @typedef {Object} CounterRule + * @property {'counter'} type + * @property {number} target - Required count + * @property {'qso' | 'entity'} countBy - What to count + * @property {FilterGroup} [filters] - Optional filters + */ + +/** + * @typedef {Object} EntityRule + * @property {'entity'} type + * @property {'dxcc' | 'state' | 'grid' | 'zone'} entityType - Entity type to count + * @property {number} target - Required unique entities + * @property {FilterGroup} [filters] - Optional filters + */ + +/** + * @typedef {Object} FilteredRule + * @property {'filtered'} type + * @property {AwardRule} baseRule - Base counter/entity rule + * @property {FilterGroup} filters - Required filters + */ + +/** + * @typedef {Object} CombinedRule + * @property {'combined'} type + * @property {'AND' | 'OR'} operator - Logical operator + * @property {AwardRule[]} rules - Sub-rules + */ + +/** + * @typedef {Object} FilterGroup + * @property {'AND' | 'OR'} operator - Logical operator for filters + * @property {Filter[]} filters - Array of filters + */ + +/** + * @typedef {BandFilter | ModeFilter | DateRangeFilter | SatelliteFilter | EntityFilter} Filter + */ + +/** + * @typedef {Object} BandFilter + * @property {'band'} field + * @property {'eq' | 'in'} operator + * @property {string | string[]} value - '40m' or ['160m','80m','40m'] + */ + +/** + * @typedef {Object} ModeFilter + * @property {'mode'} field + * @property {'eq' | 'in'} operator + * @property {string | string[]} value - 'CW' or ['CW','RTTY'] + */ + +/** + * @typedef {Object} DateRangeFilter + * @property {'qsoDate'} field + * @property {'range'} operator + * @property {{from: string, to: string}} value - ADIF dates + */ + +/** + * @typedef {Object} SatelliteFilter + * @property {'satellite'} field + * @property {'eq'} operator + * @property {boolean} value - true = satellite QSOs only + */ + +/** + * @typedef {Object} EntityFilter + * @property {'entity' | 'continent' | 'cqZone' | 'state'} field + * @property {'eq' | 'in' | 'notIn'} operator + * @property {string | string[] | number | number[]} value + */ + +/** + * @typedef {Object} AwardProgressResult + * @property {number} worked - Worked count + * @property {number} confirmed - Confirmed count + * @property {number} target - Target count + * @property {string[]} [entities] - Unique entities (for entity rules) + * @property {AwardProgressResult[]} [breakdown] - Breakdown for combined rules + */