Compare commits
6 Commits
0ee9ed0abb
...
staging
| Author | SHA1 | Date | |
|---|---|---|---|
| f94e3dc8ac | |||
| 54924b39f0 | |||
| b52e9188f4 | |||
| ecd49fc410 | |||
| 4ea0ba2d48 | |||
| 0846cf7d77 |
@@ -28,3 +28,10 @@ R2_ACCESS_KEY_ID=
|
||||
R2_SECRET_ACCESS_KEY=
|
||||
R2_BUCKET=
|
||||
R2_PUBLIC_URL=
|
||||
|
||||
# House tracker: shared secret for the home worker (openssl rand -hex 32), and
|
||||
# where alerts go (Discord webhook and/or ntfy topic URL). See docs/house-tracker.md.
|
||||
HOUSE_WORKER_TOKEN=
|
||||
HOUSE_DISCORD_WEBHOOK_URL=
|
||||
HOUSE_NTFY_URL=
|
||||
HOUSE_NTFY_TOKEN=
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -16,6 +16,10 @@ Thumbs.db
|
||||
!.env.example
|
||||
!.env.test
|
||||
|
||||
# Home worker credentials and browser cookies
|
||||
/house.env
|
||||
/.house-worker-profile/
|
||||
|
||||
# Vite
|
||||
vite.config.js.timestamp-*
|
||||
vite.config.ts.timestamp-*
|
||||
|
||||
@@ -18,3 +18,4 @@ data/cod-challenges.json
|
||||
# Generated local image download manifest
|
||||
data/game-images.json
|
||||
data/cod-achievement-art.json
|
||||
src/lib/house-tracker/fixtures/
|
||||
|
||||
@@ -21,4 +21,4 @@ COPY package.json ./
|
||||
EXPOSE 3000
|
||||
ENV NODE_ENV=production
|
||||
ENV BODY_SIZE_LIMIT=6M
|
||||
CMD ["bun", "build/index.js"]
|
||||
CMD ["bun", "./build"]
|
||||
|
||||
12
README.md
12
README.md
@@ -7,10 +7,16 @@ This is my personal website, including:
|
||||
- Blog posts + comments
|
||||
- Projects like Animaldex, CatchEmAll
|
||||
|
||||
## House tracker
|
||||
|
||||
Available at `/projects/house-tracker` (admin only). Listings come from a worker
|
||||
that runs at home; setup, settings and the ban-avoidance rules are in
|
||||
[docs/house-tracker.md](docs/house-tracker.md).
|
||||
|
||||
## osu! Map Finder
|
||||
|
||||
Available at `/projects/spotify-beatmap-finder`. Built with the existing Svelte 5,
|
||||
SvelteKit, Tailwind 4, and adapter-node setup. Uses server-only direct `fetch`
|
||||
SvelteKit 3, Tailwind 4, and adapter-bun setup. Uses server-only direct `fetch`
|
||||
calls; no Spotify/osu! SDK or additional services.
|
||||
|
||||
Set these private variables in your local `.env` or deployment environment:
|
||||
@@ -23,7 +29,7 @@ OSU_CLIENT_SECRET
|
||||
```
|
||||
|
||||
Never prefix these with `PUBLIC_` or commit `.env`. Providers read
|
||||
`$env/dynamic/private`; the browser receives only display metadata and results.
|
||||
`$app/env/private`; the browser receives only display metadata and results.
|
||||
The other projects on this personal site retain their own environment requirements.
|
||||
|
||||
Supported inputs: song text (`Deftones Change`), Spotify track, playlist, and album
|
||||
@@ -64,7 +70,7 @@ up to six active searches globally, and 12 budget units per IP per 10 minutes:
|
||||
a single track costs one, a playlist/album six. Expired records are cleaned on
|
||||
requests; the limiter has a 10,000-record ceiling. It uses `getClientAddress()`,
|
||||
never arbitrary forwarded headers; unsupported adapters share a conservative key.
|
||||
Configure adapter-node's trusted proxy/client-address settings correctly at deployment.
|
||||
Configure adapter-bun's trusted proxy/client-address settings (`ADDRESS_HEADER`, `XFF_DEPTH`) correctly at deployment.
|
||||
|
||||
Requests have a 12-second timeout and at most two retries for network errors,
|
||||
429s or server errors. Backoff includes jitter. `Retry-After` is honored; delays
|
||||
|
||||
112
bun.lock
112
bun.lock
@@ -9,7 +9,6 @@
|
||||
"@aws-sdk/client-s3": "^3.1121.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1121.0",
|
||||
"@maptiler/sdk": "^4.1.0",
|
||||
"@sveltejs/adapter-node": "^5.5.7",
|
||||
"ai": "^7.0.85",
|
||||
"d3-geo": "^3.1.1",
|
||||
"drizzle-orm": "^1.0.0-rc.4",
|
||||
@@ -30,8 +29,9 @@
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^1.48.0",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@sveltejs/adapter-bun": "^1.0.0",
|
||||
"@sveltejs/enhanced-img": "^0.11.0",
|
||||
"@sveltejs/kit": "^2.70.3",
|
||||
"@sveltejs/kit": "^3.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
@@ -54,7 +54,7 @@
|
||||
"prettier": "3.9.6",
|
||||
"prettier-plugin-svelte": "^4.1.1",
|
||||
"shadcn-svelte": "^1.7.0",
|
||||
"svelte": "^5.57.0",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "4.7.6",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwind-variants": "^3.3.1",
|
||||
@@ -325,8 +325,6 @@
|
||||
|
||||
"@melt-ui/svelte": ["@melt-ui/svelte@0.76.2", "", { "dependencies": { "@floating-ui/core": "^1.3.1", "@floating-ui/dom": "^1.4.5", "@internationalized/date": "^3.5.0", "dequal": "^2.0.3", "focus-trap": "^7.5.2", "nanoid": "^5.0.4" }, "peerDependencies": { "svelte": ">=3 <5" } }, "sha512-7SbOa11tXUS95T3fReL+dwDs5FyJtCEqrqG3inRziDws346SYLsxOQ6HmX+4BkIsQh1R8U3XNa+EMmdMt38lMA=="],
|
||||
|
||||
"@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="],
|
||||
|
||||
"@noble/ciphers": ["@noble/ciphers@2.4.0", "", {}, "sha512-AnjFn0Jv92laAkvMrghlFZq4qQCIN/4DxFV/eooqtC2YTjB7kBeLMS2T9KJX4Dn+ZVXLOwK0lSgqDtx9gvxtiw=="],
|
||||
|
||||
"@noble/hashes": ["@noble/hashes@2.4.0", "", {}, "sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA=="],
|
||||
@@ -371,66 +369,8 @@
|
||||
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
|
||||
|
||||
"@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@29.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-ZaOxZceP7SOUW7Lqw5IRVweSQYWaeIPnXIGLiB690EBA3FGJTO40EEr2L5yZplJWsgTCogILRSpcAe7+U0Otdg=="],
|
||||
|
||||
"@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
|
||||
|
||||
"@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-lUYM3UBGuM93CnMPG1YocWu7X802BrNF3jW2zny5gQyLQgRFJhV1Sq0Zi74+dh/6NBx1DxFC4b4GXg9wUCG5Qg=="],
|
||||
|
||||
"@rollup/plugin-replace": ["@rollup/plugin-replace@6.0.3", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "magic-string": "^0.30.3" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-J4RZarRvQAm5IF0/LwUUg+obsm+xZhYnbMXmXROyoSE1ATJe3oXSb9L5MMppdxP2ylNSjv6zFBwKYjcKMucVfA=="],
|
||||
|
||||
"@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.1", "", { "os": "android", "cpu": "arm" }, "sha512-UZ8sUxPTiHWYX9QNdJedb1kDZSpS1t/VPWBWGSgqHNi9w3Cu6IXvu2mzbhiTiPvtrqgTQJ+zqiAq2iPIPilpaQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.1", "", { "os": "android", "cpu": "arm64" }, "sha512-cQ4nFQABN5cDvDpbvJ7bMStCpnaVxynZrRMfUJYgxcIk9Sh54FIO1vtfkg0B69REjER77ioZ/ov+eAApx/KmLQ=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FQNqd1lRy/0QhDk3xeRIkSBiCpXCiDnZO3YLVdcDKN1UBiKToNftCzcXYNLshmPDUMlu2TdeS8tGcsU6f3YF1Q=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-pvD16V939D3CloK0+qikpGaxiPrDUXTe7Y5cWOMkMSy7m1cawa8EGy/kXYi/G/cKAC4HDAbSnzCIk1WmsoOKXg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-pcFGeL2345VwdTnJhA6zLbew+YgWB0qBG2+dMtXjCicf6+rm6kO6cOoh5VnTe0ZMrMRgRyuHmCJxZWrIdzYuOw=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mRJlqSRulVzcKq/LKA6ICSIc3K/l4fzlVn/gePn2nXIHy8seRi5z/eeRE0d/XMBxcMldiXtQTSpRj0tkkC3g8Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-YDUNvVM85TI3g/1OpnqKP1h4NeW/j64DfWMf+G3M809xNk1bJSnpFp4sh83NpmVE5DXnkh8ULor4LTVZKoYLHw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.1", "", { "os": "linux", "cpu": "arm" }, "sha512-7Mcn71p9ZuQFAj+h+dhQXy/yeLePRS2yKRnmW1DijA9thKO5qap0GNOIQK4yQ6iP3SU0Mrb/yWo8h8vgRba8lw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-4YiLQTX6U4CSl0L9cluep9A9W6UmTfqBDc2/CH6wlu54pl4E7Jn3cOD8oxzvBDEGk/JMKgJ47C8g+radF7mwvg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-2ra8F7w8OquwZN9z2/fKFnli69wa8PLwaVzRMIPGb13ByMJwC28Fbp8YcVGoUhlYMTt7j5j9bNgpysrN2UM+vw=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-Sy20ncyhjmBP0Ml+UvQbimjlk6VFgjW5uNP+qqwHB00mTE8Bl2C1TuHTlRwK2YoXeZbee5lP2XevBWVkAQAtSQ=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-noITLp8oNjYliPnGWmLyelIHwULGqbHloQHGw1rtxbWhTuWooRpnZarZQJ1y9EUC4szuCusCc+HEpUtxpIwYvA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-hlxxXd+F1mWiAcaFR7Sv9ZQT6m6UfI8+Vy/kFJzztq2pDMU/0wZ9sish0iszNZvsQDo8Gc0i5yuFEOz5dDf6fA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-EF7OpqQTQ/BvGqLzUi4rEHuagCV9MugAUXSHemwPW5vxZ75RR+jxO/2j95Ph2dalMpFHSVECjRoioHZgA9zOYA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-wQO3JesW9PRkwlabQ27y7sPfVOOTLRG73I4F2UYHG5PXun3J9U3y+b7ezVKSYbsvSKGQ1k1cq8Qlun4C9kLt3w=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.1", "", { "os": "linux", "cpu": "none" }, "sha512-ouAGwhO6wHRXdnOVCOsB0tRFkA7nhNB2Nwax6oECXN0YiN8EYUTBAOudADOB1PI+yDL61TeNx/u7MVCzksNbkQ=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-q2R38Sn+1J8RxhfJ+T54wSWmyKXWec+9jgDfqO2AtArEqHO5R2aeayp5H5OYLr5UYDVGsVaZPEFUooMhYCdz5A=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-gfI5T24WLLuFfSKw7Go/zDXjAAV0fny0swTaDv+WjK7vqcw4cRhFfdsyKL1n+ukI+ooBxn3bVQnyrn06WpI50w=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.1", "", { "os": "linux", "cpu": "x64" }, "sha512-4h6XqthmB4Hspji84wvgk+ElodTsGj+dbZqHJHHtKxj4mYq0ANSEEPX9ys3moJueqsRjwpaJYH7874Itwnj2ow=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-dlfCOa87o1VAYegLQ9EKilx2JCeRofiyPGhTCmqnuXZ6bMPiycO1rq1+sKoulAp7pGLIsTIw+1x5R+zgh5LhhA=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.1", "", { "os": "none", "cpu": "arm64" }, "sha512-cjkLbOlfcm3QGhMM1J5zaZjsw1GggbN6rw9UTSSRrPrR1KkcXnN7Uq9rPw34xImQ9VOY9GN+6u2Zj80B9ptkcw=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-Li1KdUnWGE4N3e1F/B4RTB1ms+nG4WBgjByO46pkeBVX/2UBsY53xf5vK9WygVmnH3RwncIST7lkSdLSY6P9lg=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-t4ZYOSoLTgwhuFMrmTMLx/+i1DQVK7HYqMc6kY46EApwi8X0nIVphzdNoThU3xt6n+N5urG1/gxBdCaKDLavfg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-RgroPfMmKlD1RzSDxvwgcPiy2HNQKoYV7OmwIXDsk73uKW5t6B/V8KIy27SMv/FNXFo/oSBtWc9J0X7t91ezZg=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.1", "", { "os": "win32", "cpu": "x64" }, "sha512-at8QVep6S3h5Y6gSbdGU06bRY5WJkf6WUduM9YtvYMbYhB1MOFfUgc6kehitQXzOtMSaT70q7f9ydPhpqu821w=="],
|
||||
|
||||
"@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="],
|
||||
|
||||
"@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="],
|
||||
@@ -447,11 +387,11 @@
|
||||
|
||||
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.13", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-wgKggnhZVL9Bfx1OaKKTrYY9BFRk6C8UAkQNUcIv1+llzYrIqy+RZm5HPKzn0NpEBvTVhTqB4kQyllZywsRBRQ=="],
|
||||
|
||||
"@sveltejs/adapter-node": ["@sveltejs/adapter-node@5.5.7", "", { "dependencies": { "@rollup/plugin-commonjs": "^29.0.0", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.0", "@rollup/plugin-replace": "^6.0.3", "rollup": "^4.59.0" }, "peerDependencies": { "@sveltejs/kit": "^2.4.0" } }, "sha512-uOfc9eVlI3A37RRSaKcgrheBYPrfJwC9VMqDp8x/O6tlKdcLLvHThSWD0KNIbjQ/d+7bwLGx3vx6aowAcRfd2g=="],
|
||||
"@sveltejs/adapter-bun": ["@sveltejs/adapter-bun@1.0.0", "", { "dependencies": { "bun-types": "^1.3.14" }, "peerDependencies": { "@sveltejs/kit": "^3.0.0-next.0" } }, "sha512-CUfFNxrWrKk/0VqSM1DrXCAwpY3tWocaFwBohuBeyxKx07859shLqPdT9oo00lDti9PvVwhQMDS6bKQylTyv0g=="],
|
||||
|
||||
"@sveltejs/enhanced-img": ["@sveltejs/enhanced-img@0.11.0", "", { "dependencies": { "magic-string": "^0.30.5", "sharp": "^0.34.1", "svelte-parse-markup": "^0.1.5", "vite-imagetools": "^9.0.3", "zimmerframe": "^1.1.2" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^6.0.0 || ^7.0.0", "svelte": "^5.0.0", "vite": "^6.3.0 || >=7.0.0" } }, "sha512-TN7VzGoqwFqvA4Faj1brUPHLtcEkyFa08nrn9bfm98yVrzr0g4bOZHjlBoPYHHX+sZNGE+cxznhTVgarECPQsA=="],
|
||||
|
||||
"@sveltejs/kit": ["@sveltejs/kit@2.70.3", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.9", "@types/cookie": "^0.6.0", "acorn": "^8.16.0", "cookie": "^0.6.0", "devalue": "^5.8.1", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", "set-cookie-parser": "^3.0.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 || ^7.0.0", "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.3.3 || ^6.0.0", "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-UDvEYuZqAMbfB/oXIoqKvbKcb7YczK5zYrzmsGV1zRJk03jntwp8dXiYoIJotxAndsKvcPFtx9H1GRSKFdSHgg=="],
|
||||
"@sveltejs/kit": ["@sveltejs/kit@3.1.0", "", { "dependencies": { "@rolldown/pluginutils": "^1.0.1", "@standard-schema/spec": "^1.1.0", "cookie": "^2.0.1", "devalue": "^5.9.4", "esm-env": "^1.2.2", "magic-string": "^1.2.3", "mrmime": "^2.0.1", "sirv": "^3.0.2" }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", "@sveltejs/vite-plugin-svelte": "^7.0.0", "svelte": "^5.57.1", "typescript": "^6.0.0", "vite": "^8.0.12" }, "optionalPeers": ["@opentelemetry/api", "typescript"], "bin": { "svelte-kit": "svelte-kit.js" } }, "sha512-JTWlq2HQlkqx6vIoxhC0R6sc4WVqbQoRyEmr5YlqKmiYbzY1U3awtSWTmjMLlS056a49AmkQR6rnNANXIE8B0Q=="],
|
||||
|
||||
"@sveltejs/load-config": ["@sveltejs/load-config@0.2.3", "", {}, "sha512-VT3qmUb8pRV2QrZjd8iAmtg8lf4W0TIjZbvXtz5MKei/q96teWZgGJyyidJzOjzZzvdq616eSRVeMYIQChUTAQ=="],
|
||||
|
||||
@@ -493,8 +433,6 @@
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/cookie": ["@types/cookie@0.6.0", "", {}, "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA=="],
|
||||
|
||||
"@types/d3-geo": ["@types/d3-geo@3.1.1", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-65Emv9fQiQQqphLlRkuQ5ypPsOmWPhtBGCMv61JDPEPMvsx+gzhGf74yw1a78xFKPj6zw4AgQICJoQv0vK9M2w=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
@@ -513,8 +451,6 @@
|
||||
|
||||
"@types/pg": ["@types/pg@8.23.1", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-fKVHpikPdg4GKks3JuLEhvwSyvwzF23hnabPy6DD8ljVbC7+6J5dQzdv4arV6jqq57djnMgs1HKBxX4P8aBI3A=="],
|
||||
|
||||
"@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
|
||||
|
||||
"@types/topojson-client": ["@types/topojson-client@3.1.5", "", { "dependencies": { "@types/geojson": "*", "@types/topojson-specification": "*" } }, "sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw=="],
|
||||
|
||||
"@types/topojson-specification": ["@types/topojson-specification@1.0.5", "", { "dependencies": { "@types/geojson": "*" } }, "sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ=="],
|
||||
@@ -631,6 +567,8 @@
|
||||
|
||||
"browserslist": ["browserslist@4.28.8", "", { "dependencies": { "baseline-browser-mapping": "^2.11.12", "caniuse-lite": "^1.0.30001809", "electron-to-chromium": "^1.5.402", "node-releases": "^2.0.53", "update-browserslist-db": "^1.3.0" }, "bin": "cli.js" }, "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.4.3", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-tinRJNGOmUN+tEAsn2y2e942n9JHRHR42iUYzDH75NmhJqVBokGSQkkfSFdNWwsANIdrK6ACH9t01M452YyTwg=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
@@ -645,13 +583,11 @@
|
||||
|
||||
"commander": ["commander@14.0.3", "", {}, "sha512-H+y0Jo/T1RZ9qPP4Eh1pkcQcLRglraJaSLoyOtHxu6AapkjWVCy2Sit1QQ4x3Dng8qDlSsZEet7g5Pq06MvTgw=="],
|
||||
|
||||
"commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
|
||||
|
||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"cookie": ["cookie@0.6.0", "", {}, "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw=="],
|
||||
"cookie": ["cookie@2.0.1", "", {}, "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
@@ -673,7 +609,7 @@
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"devalue": ["devalue@5.9.2", "", {}, "sha512-po4PAY5c53tw5XMocSnf8A/5OHhbbUftpr93aEN6BBoAdntUmK7vu7wOATqvt7cXO7m1Cl4gMVn6p7n6n4mj0w=="],
|
||||
"devalue": ["devalue@5.9.4", "", {}, "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg=="],
|
||||
|
||||
"drizzle-kit": ["drizzle-kit@1.0.0-rc.4", "", { "dependencies": { "@drizzle-team/brocli": "^0.12.0", "@js-temporal/polyfill": "^0.5.1", "esbuild": "^0.25.10", "get-tsconfig": "^4.13.6", "jiti": "^2.6.1" }, "bin": { "drizzle-kit": "bin.cjs" } }, "sha512-KZCpjRyu+oYHLj/UJogfFlOkWhHVkaEI2EOT1U3NDVXUzLoTyPjqwFxwOrlQxsY6jzRyxcz4EacqboGfhEeYrA=="],
|
||||
|
||||
@@ -687,8 +623,6 @@
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.24.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, "sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@2.3.2", "", {}, "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw=="],
|
||||
|
||||
"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": "bin/esbuild" }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="],
|
||||
@@ -753,8 +687,6 @@
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="],
|
||||
|
||||
"gl-matrix": ["gl-matrix@3.4.4", "", {}, "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ=="],
|
||||
@@ -765,8 +697,6 @@
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
|
||||
|
||||
"highlight.js": ["highlight.js@11.12.0", "", {}, "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
@@ -781,14 +711,10 @@
|
||||
|
||||
"internmap": ["internmap@2.0.3", "", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
"is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
|
||||
|
||||
"is-reference": ["is-reference@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.6" } }, "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
@@ -815,8 +741,6 @@
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"known-css-properties": ["known-css-properties@0.37.0", "", {}, "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ=="],
|
||||
|
||||
"kysely": ["kysely@0.29.5", "", {}, "sha512-ooa+eSbBNPTo3MycPEuW5jdrxQdQwdtB3LC3h43FiXQbIry5tR0C5lDG7eealK0E4D7XjrnOP5DIUg/LyjRMYQ=="],
|
||||
@@ -909,8 +833,6 @@
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"pbf": ["pbf@4.0.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": "bin/pbf" }, "sha512-J0ajxARhZfpUEebxYs1vhMGMuLSXtBe1e+fFPDrf2uA2hgo+UshKfNUWOz92HJNz6/NFEXseQPddnHkTreWRqg=="],
|
||||
@@ -971,16 +893,12 @@
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"resolve-protobuf-schema": ["resolve-protobuf-schema@2.1.0", "", { "dependencies": { "protocol-buffers-schema": "^3.3.1" } }, "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ=="],
|
||||
|
||||
"rolldown": ["rolldown@1.2.6", "", { "dependencies": { "@oxc-project/types": "=0.147.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm-eabi": "1.2.6", "@rolldown/binding-android-arm64": "1.2.6", "@rolldown/binding-darwin-arm64": "1.2.6", "@rolldown/binding-darwin-x64": "1.2.6", "@rolldown/binding-freebsd-x64": "1.2.6", "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", "@rolldown/binding-linux-arm64-gnu": "1.2.6", "@rolldown/binding-linux-arm64-musl": "1.2.6", "@rolldown/binding-linux-ppc64-gnu": "1.2.6", "@rolldown/binding-linux-s390x-gnu": "1.2.6", "@rolldown/binding-linux-x64-gnu": "1.2.6", "@rolldown/binding-linux-x64-musl": "1.2.6", "@rolldown/binding-openharmony-arm64": "1.2.6", "@rolldown/binding-win32-arm64-msvc": "1.2.6", "@rolldown/binding-win32-x64-msvc": "1.2.6" }, "bin": "bin/cli.mjs" }, "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA=="],
|
||||
|
||||
"rollup": ["rollup@4.63.1", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.1", "@rollup/rollup-android-arm64": "4.63.1", "@rollup/rollup-darwin-arm64": "4.63.1", "@rollup/rollup-darwin-x64": "4.63.1", "@rollup/rollup-freebsd-arm64": "4.63.1", "@rollup/rollup-freebsd-x64": "4.63.1", "@rollup/rollup-linux-arm-gnueabihf": "4.63.1", "@rollup/rollup-linux-arm-musleabihf": "4.63.1", "@rollup/rollup-linux-arm64-gnu": "4.63.1", "@rollup/rollup-linux-arm64-musl": "4.63.1", "@rollup/rollup-linux-loong64-gnu": "4.63.1", "@rollup/rollup-linux-loong64-musl": "4.63.1", "@rollup/rollup-linux-ppc64-gnu": "4.63.1", "@rollup/rollup-linux-ppc64-musl": "4.63.1", "@rollup/rollup-linux-riscv64-gnu": "4.63.1", "@rollup/rollup-linux-riscv64-musl": "4.63.1", "@rollup/rollup-linux-s390x-gnu": "4.63.1", "@rollup/rollup-linux-x64-gnu": "4.63.1", "@rollup/rollup-linux-x64-musl": "4.63.1", "@rollup/rollup-openbsd-x64": "4.63.1", "@rollup/rollup-openharmony-arm64": "4.63.1", "@rollup/rollup-win32-arm64-msvc": "4.63.1", "@rollup/rollup-win32-ia32-msvc": "4.63.1", "@rollup/rollup-win32-x64-gnu": "4.63.1", "@rollup/rollup-win32-x64-msvc": "4.63.1", "fsevents": "~2.3.2" }, "bin": "dist/bin/rollup" }, "sha512-3Df9jsstwhccuEfmAMi9l8XUh/GOkVObmFTU7CCVBysEbcOZLl84jCtaAZMcPiMz2EGKsATzQcU+Xr3n/wU6cg=="],
|
||||
|
||||
"rou3": ["rou3@0.9.2", "", {}, "sha512-3SOzvaAg8rkHrXtRjpCvCvbyO5to9oOO27Z/XqHEYXfMRVSw/qMIVdmaOk9W2lcRLtR6dlqTjo9hDeJk70QBYQ=="],
|
||||
|
||||
"runed": ["runed@0.35.1", "", { "dependencies": { "dequal": "^2.0.3", "esm-env": "^1.0.0", "lz-string": "^1.5.0" }, "peerDependencies": { "@sveltejs/kit": "^2.21.0", "svelte": "^5.7.0" }, "optionalPeers": ["@sveltejs/kit"] }, "sha512-2F4Q/FZzbeJTFdIS/PuOoPRSm92sA2LhzTnv6FXhCoENb3huf5+fDuNOg1LNvGOouy3u/225qxmuJvcV3IZK5Q=="],
|
||||
@@ -1011,9 +929,7 @@
|
||||
|
||||
"style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"svelte": ["svelte@5.57.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.10", "@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.8.1", "esm-env": "^1.2.1", "esrap": "^2.2.12", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-NdbDn7fl4be1ViUG0oq/lvG6OZy3oENolV2ONjiqqsfVoeAfzaQAKUcEX3MrQod/Bebv1PgwET9rfXhgn9s4Kg=="],
|
||||
"svelte": ["svelte@5.57.2", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "@jridgewell/sourcemap-codec": "^1.6.0", "@sveltejs/acorn-typescript": "^1.0.13", "@types/estree": "^1.0.9", "acorn": "^8.18.0", "aria-query": "5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", "devalue": "^5.9.2", "esm-env": "^1.2.1", "esrap": "^2.3.6", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", "zimmerframe": "^1.1.2" } }, "sha512-1YiPCAXT0Ct6g1vOMmSLa0Nsc3QRMvtDF32ihgglGVrugIuIK38RyDiFNXVDhtQ81TJjSO57NdMw6DyYLqSGDQ=="],
|
||||
|
||||
"svelte-check": ["svelte-check@4.7.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "@sveltejs/load-config": "^0.2.3", "chokidar": "^4.0.1", "fdir": "^6.2.0", "picocolors": "^1.0.0", "sade": "^1.7.4" }, "peerDependencies": { "svelte": "^4.0.0 || ^5.0.0-next.0", "typescript": "^5.0.0 || ^6.0.0" }, "bin": "bin/svelte-check" }, "sha512-t2scM//ZuVbSY/T2w6FSBw1v9s2NEmh/g+sy1lqtosW5ylBV5AF4wFb1Ts9Kf3MbfPDUDJDZ9L436YT0SPTdvw=="],
|
||||
|
||||
@@ -1111,12 +1027,10 @@
|
||||
|
||||
"@maplibre/vt-pbf/pbf": ["pbf@5.1.2", "", { "dependencies": { "resolve-protobuf-schema": "^2.1.0" }, "bin": "bin/pbf" }, "sha512-mnvGdvOrIvJOBGUEdGkrVXjN8E/VkIJCkf2eS1DH2yv82ORUlLttmDt0rWY38yYZmVwciZwBUvHM20qxBZf40w=="],
|
||||
|
||||
"@rollup/plugin-commonjs/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@rollup/plugin-commonjs/is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
|
||||
|
||||
"@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
|
||||
|
||||
"@sveltejs/kit/magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="],
|
||||
|
||||
"@sveltejs/vite-plugin-svelte/magic-string": ["magic-string@1.2.3", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g=="],
|
||||
|
||||
"@tailwindcss/node/lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
@@ -1151,8 +1065,6 @@
|
||||
|
||||
"postcss/nanoid": ["nanoid@3.3.18", "", { "bin": "bin/nanoid.cjs" }, "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w=="],
|
||||
|
||||
"rollup/fsevents": ["fsevents@2.3.2", "", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="],
|
||||
|
||||
"shadcn-svelte/cn": ["cn@0.3.3", "", { "bin": { "cn": "bin/cn.mjs" } }, "sha512-OEiyJkSpYm2kOuNOKQ+c2wmzZRG8orrb9fnObtHTbCGNx/E9KLnGcmcocItOzREKk5KzQN5I2e3BUCj563KEpw=="],
|
||||
|
||||
"svelte-eslint-parser/eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
|
||||
178
docs/house-tracker.md
Normal file
178
docs/house-tracker.md
Normal file
@@ -0,0 +1,178 @@
|
||||
# House tracker
|
||||
|
||||
Personal tool: collects Dutch housing listings into Postgres, shows them on
|
||||
`/projects/house-tracker` (admin only) and sends an alert when a new listing
|
||||
matches a rule. Only Pararius for now; other sources plug in the same way.
|
||||
|
||||
## How it fits together
|
||||
|
||||
```
|
||||
home box (residential IP) stanrunge.dev (Hetzner)
|
||||
┌──────────────────────────┐ POST sync ┌──────────────────────────────┐
|
||||
│ scripts/house-worker.ts │ ──────────────▶ │ /projects/house-tracker/api/ │
|
||||
│ polls pararius.nl │ POST details │ worker/{sync,details,report}│
|
||||
│ parses HTML │ ◀────────────── │ stores, dedups, alerts │
|
||||
└──────────────────────────┘ needDetails └──────────────┬───────────────┘
|
||||
▼
|
||||
Discord webhook / ntfy push
|
||||
```
|
||||
|
||||
The server never talks to the listing sites. Pararius sits behind Cloudflare,
|
||||
which challenges datacenter IPs and plain HTTP clients, so the worker runs at
|
||||
home where traffic looks like a normal visitor.
|
||||
|
||||
Two lanes:
|
||||
|
||||
- **fast**: one filtered search (default `huurwoningen/gouda/1300-1700`), page 1
|
||||
only. Every 3–5 min on weekdays 07:00–18:00 Amsterdam time, every 30–60 min
|
||||
otherwise. Listings near Gouda were all published in office hours on weekdays.
|
||||
Alerts are sent from the search card alone, before the detail page is fetched.
|
||||
- **slow**: full, paginated sweeps of rent and buy listings in Gouda, Rotterdam,
|
||||
Den Haag, Utrecht, Amsterdam and Eindhoven every 12 h. Unfiltered city sweeps
|
||||
also mark listings that disappeared, which gives "days listed" in the stats.
|
||||
|
||||
Safety rules in the worker: one request at a time, at least 8 s apart with
|
||||
jitter, a daily budget (1500 by default, the slow lane may only use 60%), and
|
||||
any challenge/403/429 stops everything and backs off 15 min → 30 → 60 … 6 h.
|
||||
The first poll of any search only seeds the database, so you don't get 40
|
||||
alerts for listings that were already online.
|
||||
|
||||
## Server setup
|
||||
|
||||
Add to the production env file (`/etc/personal-website/prod.env`):
|
||||
|
||||
```text
|
||||
# openssl rand -hex 32
|
||||
HOUSE_WORKER_TOKEN=
|
||||
# one or both
|
||||
HOUSE_DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/...
|
||||
HOUSE_NTFY_URL=https://ntfy.sh/some-long-random-topic
|
||||
HOUSE_NTFY_TOKEN= # only for a protected ntfy topic
|
||||
PUBLIC_MAPTILER_API_KEY= # already set for Animaldex
|
||||
```
|
||||
|
||||
`scripts/migrate-house-tracker.ts` runs on every deploy (idempotent). Use
|
||||
"send test alert" on the page to check the alert channel.
|
||||
|
||||
If Cloudflare Bot Fight Mode / WAF challenges are on for stanrunge.dev, add a
|
||||
WAF skip rule for `URI Path starts with /projects/house-tracker/api/worker/`,
|
||||
otherwise the worker's POSTs get challenged too. The endpoints are protected by
|
||||
the bearer token.
|
||||
|
||||
### Development behind Cloudflare Access
|
||||
|
||||
For `https://dev.stanrunge.dev`, keep Access enabled. In Cloudflare Zero Trust,
|
||||
go to **Access controls → Service credentials → Service Tokens** and create a
|
||||
token named `house-worker`. Save its Client ID and Client Secret. In the Access
|
||||
application protecting the dev hostname, add a **Service Auth** policy with
|
||||
**Include → Service Token → house-worker**. Keep the existing browser login
|
||||
policy. See [Cloudflare's service token documentation](https://developers.cloudflare.com/cloudflare-one/access-controls/service-credentials/service-tokens/).
|
||||
|
||||
Set `HOUSE_WORKER_TOKEN` in the dev website's `.env` and restart its dev server.
|
||||
On the home machine, copy `house.env.example` to `house.env`, fill in the same
|
||||
worker token plus the Cloudflare Client ID and Client Secret, then run:
|
||||
|
||||
```sh
|
||||
chmod 600 house.env
|
||||
bun --env-file=house.env scripts/house-worker.ts --check-api
|
||||
```
|
||||
|
||||
This makes one authenticated GET to the website's worker health endpoint. It
|
||||
does not contact Pararius, sync listings or send alerts. The expected output is
|
||||
`API connection OK`. Access credentials are sent only to the website API; the
|
||||
client refuses redirects and stops immediately on an authentication failure.
|
||||
An HTTP 503 can mean the website's worker token is missing; an HTTP 404 can mean
|
||||
the running website does not yet include the health route.
|
||||
|
||||
## Home worker setup
|
||||
|
||||
Needs a machine that stays on (old laptop, Raspberry Pi, NAS) with Bun 1.4 or
|
||||
newer; older versions cannot read this repository's lockfile. Use `bun upgrade`
|
||||
if needed.
|
||||
|
||||
```sh
|
||||
git clone https://git.stanrunge.dev/stan/personal-website ~/house-worker
|
||||
cd ~/house-worker && bun install --frozen-lockfile
|
||||
cat > house.env <<'EOF'
|
||||
HOUSE_API_URL=https://stanrunge.dev
|
||||
HOUSE_WORKER_TOKEN=same-value-as-the-server
|
||||
EOF
|
||||
chmod 600 house.env
|
||||
|
||||
# 1. check parsing without sending anything
|
||||
bun --env-file=house.env scripts/house-worker.ts --dry-run --once
|
||||
# 2. one real poll
|
||||
bun --env-file=house.env scripts/house-worker.ts --once
|
||||
```
|
||||
|
||||
Then run it as a service, `/etc/systemd/system/house-worker.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=House tracker worker
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
User=stan
|
||||
WorkingDirectory=/home/stan/house-worker
|
||||
ExecStart=/home/stan/.bun/bin/bun --env-file=house.env scripts/house-worker.ts
|
||||
Restart=always
|
||||
RestartSec=60
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
```sh
|
||||
sudo systemctl enable --now house-worker
|
||||
journalctl -u house-worker -f
|
||||
```
|
||||
|
||||
Or with Docker: `docker run -d --restart unless-stopped --name house-worker
|
||||
--env-file house.env -v "$PWD":/app -w /app oven/bun:1.4.0 bun
|
||||
scripts/house-worker.ts`.
|
||||
|
||||
## Worker settings
|
||||
|
||||
| variable | default |
|
||||
| ------------------------ | ------------------------------------------------------------------------ |
|
||||
| `HOUSE_FAST_SEARCHES` | `https://www.pararius.nl/huurwoningen/gouda/1300-1700` (comma separated) |
|
||||
| `HOUSE_SLOW_SEARCHES` | rent + buy for the six cities above |
|
||||
| `HOUSE_SLOW_EVERY_HOURS` | `12` |
|
||||
| `HOUSE_SLOW_MAX_PAGES` | `80` per search |
|
||||
| `HOUSE_MIN_GAP_SECONDS` | `8` between any two requests |
|
||||
| `HOUSE_DAILY_BUDGET` | `1500` requests |
|
||||
| `HOUSE_FETCH_MODE` | `fetch`, or `browser` (real Chromium) |
|
||||
|
||||
Any Pararius search URL works as a fast search: set the filters on the site
|
||||
(price, radius, bedrooms) and paste the URL. Every extra fast search is one more
|
||||
request per poll.
|
||||
|
||||
## When it gets challenged
|
||||
|
||||
You get one "worker was challenged" alert per 6 hours and the page header turns
|
||||
red. In order:
|
||||
|
||||
1. Do nothing for a day. Backoff handles short blocks.
|
||||
2. Slow down: raise `HOUSE_MIN_GAP_SECONDS`, lower `HOUSE_DAILY_BUDGET`, or drop
|
||||
slow searches.
|
||||
3. Save a page manually in your normal browser to investigate parsing offline.
|
||||
Do not switch clients, use proxies or solve challenges to bypass a block.
|
||||
|
||||
The optional `HOUSE_FETCH_MODE=browser` uses headless Chromium with a persistent
|
||||
profile (`npx playwright install chromium` once on the worker machine). It obeys
|
||||
the same stop/backoff rules and is not a workaround for a challenged fetch.
|
||||
|
||||
## Parsers
|
||||
|
||||
`src/lib/house-tracker/pararius.ts`, tested against trimmed real pages in
|
||||
`src/lib/house-tracker/fixtures/`. Listing pages expose an exact publish
|
||||
timestamp (`odp.offered_since`), coordinates, deposit, energy label and agent.
|
||||
`pararius-search-gouda.html` is a trimmed real search page saved on 2026-10-11,
|
||||
with tracking and account data removed. It covers three listings, nested base
|
||||
and total rent, action attributes and a lazy photo template. The synthetic
|
||||
fixture still covers pagination and price on request. Rental `priceEur` is the
|
||||
base/listed rent used by the search filter; a separately shown total can be
|
||||
higher (Raam: €1,475 base, €1,750 total). If Pararius changes markup, the worker
|
||||
reports `parse-error` in the worker log on the page.
|
||||
@@ -7,6 +7,6 @@ export default defineConfig({
|
||||
verbose: true,
|
||||
strict: true,
|
||||
dbCredentials: {
|
||||
url: process.env.DB_URL
|
||||
url: process.env.DB_URL!
|
||||
}
|
||||
});
|
||||
|
||||
7
house.env.example
Normal file
7
house.env.example
Normal file
@@ -0,0 +1,7 @@
|
||||
# Copy to house.env on the HOME machine. That filename is ignored by Git.
|
||||
HOUSE_API_URL=https://dev.stanrunge.dev
|
||||
# Same value as HOUSE_WORKER_TOKEN in the website's .env.
|
||||
HOUSE_WORKER_TOKEN=
|
||||
# Cloudflare Access service token, used only for requests to HOUSE_API_URL.
|
||||
HOUSE_CF_ACCESS_CLIENT_ID=
|
||||
HOUSE_CF_ACCESS_CLIENT_SECRET=
|
||||
14
package.json
14
package.json
@@ -4,7 +4,7 @@
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
"build": "bun --bun vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "npm run test:integration && npm run test:unit",
|
||||
"check": "svelte-kit sync && svelte-check --tsgo --tsconfig ./tsconfig.json",
|
||||
@@ -25,6 +25,8 @@
|
||||
"db:migrate:game-tracker": "bun scripts/migrate-game-tracker.ts",
|
||||
"db:migrate:cs-prices": "bun scripts/migrate-cs-prices.ts",
|
||||
"cs-prices:ingest": "bun scripts/ingest-cs-prices.ts",
|
||||
"db:migrate:house-tracker": "bun scripts/migrate-house-tracker.ts",
|
||||
"house:worker": "bun scripts/house-worker.ts",
|
||||
"auth:role": "bun scripts/set-admin.ts",
|
||||
"catalog:build": "python3 scripts/build-pokemon-catalog.py",
|
||||
"catalog:cod": "python3 scripts/build-cod-catalog.py && prettier --write data/call-of-duty.json",
|
||||
@@ -47,8 +49,9 @@
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@lucide/svelte": "^1.48.0",
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@sveltejs/adapter-bun": "^1.0.0",
|
||||
"@sveltejs/enhanced-img": "^0.11.0",
|
||||
"@sveltejs/kit": "^2.70.3",
|
||||
"@sveltejs/kit": "^3.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
||||
"@tailwindcss/typography": "^0.5.20",
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
@@ -71,7 +74,7 @@
|
||||
"prettier": "3.9.6",
|
||||
"prettier-plugin-svelte": "^4.1.1",
|
||||
"shadcn-svelte": "^1.7.0",
|
||||
"svelte": "^5.57.0",
|
||||
"svelte": "^5.57.1",
|
||||
"svelte-check": "4.7.6",
|
||||
"tailwind-merge": "^3.6.0",
|
||||
"tailwind-variants": "^3.3.1",
|
||||
@@ -89,7 +92,6 @@
|
||||
"@aws-sdk/client-s3": "^3.1121.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.1121.0",
|
||||
"@maptiler/sdk": "^4.1.0",
|
||||
"@sveltejs/adapter-node": "^5.5.7",
|
||||
"ai": "^7.0.85",
|
||||
"d3-geo": "^3.1.1",
|
||||
"drizzle-orm": "^1.0.0-rc.4",
|
||||
@@ -102,5 +104,9 @@
|
||||
"topojson-client": "^3.1.0",
|
||||
"world-atlas": "^2.0.2",
|
||||
"zod": "^4.5.4"
|
||||
},
|
||||
"imports": {
|
||||
"#lib": "./src/lib/index.js",
|
||||
"#lib/*": "./src/lib/*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ run_database_script scripts/migrate-foundation.ts
|
||||
run_database_script scripts/migrate-money-tracker.ts
|
||||
run_database_script scripts/migrate-game-tracker.ts
|
||||
run_database_script scripts/migrate-tracker-journal.ts
|
||||
run_database_script scripts/migrate-house-tracker.ts
|
||||
run_database_script scripts/seed-catalog.ts
|
||||
run_database_script scripts/seed-cod-catalog.ts
|
||||
run_database_script scripts/seed-pokemon-research.ts
|
||||
|
||||
111
scripts/house-tracker-migration.sql
Normal file
111
scripts/house-tracker-migration.sql
Normal file
@@ -0,0 +1,111 @@
|
||||
-- House tracker: listings from Dutch housing sites, collected by a home worker.
|
||||
-- Idempotent; safe to run on every deploy.
|
||||
|
||||
CREATE TABLE IF NOT EXISTS house_listings (
|
||||
id serial PRIMARY KEY,
|
||||
source text NOT NULL,
|
||||
source_id text NOT NULL,
|
||||
external_id text,
|
||||
url text NOT NULL,
|
||||
offer text NOT NULL CHECK (offer IN ('rent', 'buy')),
|
||||
property_type text NOT NULL,
|
||||
title text,
|
||||
street text,
|
||||
postal_code text,
|
||||
city text,
|
||||
city_slug text,
|
||||
neighbourhood text,
|
||||
price_eur integer CHECK (price_eur IS NULL OR price_eur >= 0),
|
||||
area_m2 integer,
|
||||
rooms integer,
|
||||
bedrooms integer,
|
||||
bathrooms integer,
|
||||
construction_year integer,
|
||||
energy_label text,
|
||||
interior text,
|
||||
deposit_eur integer,
|
||||
available_from text,
|
||||
contract_type text,
|
||||
agent text,
|
||||
image_url text,
|
||||
description text,
|
||||
features jsonb NOT NULL DEFAULT '{}'::jsonb,
|
||||
lat double precision,
|
||||
lng double precision,
|
||||
-- When the site says it was published. first_seen_at is when we noticed.
|
||||
offered_at timestamptz,
|
||||
first_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
-- fast, slow, or seed (already listed when its search was first polled).
|
||||
first_seen_lane text NOT NULL DEFAULT 'slow',
|
||||
last_seen_at timestamptz NOT NULL DEFAULT now(),
|
||||
detail_fetched_at timestamptz,
|
||||
removed_at timestamptz,
|
||||
alerted_at timestamptz,
|
||||
-- Personal tracking.
|
||||
status text,
|
||||
notes text
|
||||
);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS house_listings_source_idx ON house_listings (source, source_id);
|
||||
CREATE INDEX IF NOT EXISTS house_listings_active_idx
|
||||
ON house_listings (offer, first_seen_at DESC) WHERE removed_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS house_listings_city_idx ON house_listings (source, offer, city_slug);
|
||||
CREATE INDEX IF NOT EXISTS house_listings_details_idx
|
||||
ON house_listings (first_seen_at DESC) WHERE detail_fetched_at IS NULL;
|
||||
|
||||
-- Every price we have seen, so price drops and market trends survive edits.
|
||||
CREATE TABLE IF NOT EXISTS house_price_history (
|
||||
listing_id integer NOT NULL REFERENCES house_listings(id) ON DELETE CASCADE,
|
||||
observed_at timestamptz NOT NULL DEFAULT now(),
|
||||
price_eur integer NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS house_price_history_listing_idx
|
||||
ON house_price_history (listing_id, observed_at);
|
||||
|
||||
-- One row per polled search URL. The first sync of a search only seeds the
|
||||
-- database: alerting on everything already listed would be noise.
|
||||
CREATE TABLE IF NOT EXISTS house_searches (
|
||||
search_url text PRIMARY KEY,
|
||||
source text NOT NULL,
|
||||
lane text NOT NULL,
|
||||
first_synced_at timestamptz NOT NULL DEFAULT now(),
|
||||
last_synced_at timestamptz NOT NULL DEFAULT now(),
|
||||
last_found integer NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
-- Worker health: challenges, HTTP errors, parse failures.
|
||||
CREATE TABLE IF NOT EXISTS house_worker_runs (
|
||||
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
||||
source text NOT NULL,
|
||||
lane text NOT NULL,
|
||||
url text NOT NULL,
|
||||
status text NOT NULL,
|
||||
http_status integer,
|
||||
found integer,
|
||||
duration_ms integer,
|
||||
message text,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS house_worker_runs_created_idx ON house_worker_runs (created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS house_alert_rules (
|
||||
id serial PRIMARY KEY,
|
||||
name text NOT NULL,
|
||||
enabled boolean NOT NULL DEFAULT true,
|
||||
offer text NOT NULL DEFAULT 'rent' CHECK (offer IN ('rent', 'buy')),
|
||||
min_price integer,
|
||||
max_price integer,
|
||||
min_area integer,
|
||||
min_bedrooms integer,
|
||||
-- Lowercase city names; empty means anywhere.
|
||||
cities text[] NOT NULL DEFAULT '{}',
|
||||
-- Empty means every property type.
|
||||
property_types text[] NOT NULL DEFAULT '{}',
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO house_alert_rules (name, offer, min_price, max_price, cities)
|
||||
SELECT 'Gouda area rentals', 'rent', 1300, 1700, ARRAY[
|
||||
'gouda', 'waddinxveen', 'reeuwijk', 'moordrecht', 'nieuwerkerk aan den ijssel',
|
||||
'zevenhuizen', 'bodegraven', 'boskoop', 'oudewater', 'schoonhoven', 'haastrecht'
|
||||
]
|
||||
WHERE NOT EXISTS (SELECT 1 FROM house_alert_rules);
|
||||
382
scripts/house-worker.ts
Normal file
382
scripts/house-worker.ts
Normal file
@@ -0,0 +1,382 @@
|
||||
/**
|
||||
* House tracker home worker. Runs on a machine at home (residential IP), polls
|
||||
* Pararius politely and POSTs what it finds to the website, which stores it and
|
||||
* sends the alerts. See docs/house-tracker.md.
|
||||
*
|
||||
* HOUSE_API_URL=https://stanrunge.dev HOUSE_WORKER_TOKEN=... bun scripts/house-worker.ts
|
||||
* bun scripts/house-worker.ts --once # one fast-lane poll, then exit
|
||||
* bun scripts/house-worker.ts --dry-run # fetch + parse, print, send nothing
|
||||
* bun scripts/house-worker.ts --check-api # check website auth only, no Pararius requests
|
||||
*
|
||||
* Politeness rules, in order of importance:
|
||||
* - one request at a time, at least HOUSE_MIN_GAP_SECONDS apart (with jitter)
|
||||
* - a daily request budget
|
||||
* - any challenge / 403 / 429 stops everything and backs off exponentially
|
||||
* - the fast lane only polls often on weekdays 07:00–18:00 Amsterdam time
|
||||
*/
|
||||
import {
|
||||
isChallenge,
|
||||
parseDetailPage,
|
||||
parseSearchPage,
|
||||
searchPageUrl
|
||||
} from '../src/lib/house-tracker/pararius';
|
||||
import { backoffMs, nextDelayMs } from '../src/lib/house-tracker/schedule';
|
||||
import { createWorkerApi } from '../src/lib/house-tracker/worker-api';
|
||||
import type {
|
||||
Lane,
|
||||
ListingDetail,
|
||||
ReportRequest,
|
||||
RunStatus,
|
||||
SyncRequest,
|
||||
SyncResponse
|
||||
} from '../src/lib/house-tracker/types';
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const once = args.has('--once');
|
||||
const checkApi = args.has('--check-api');
|
||||
const dryRun = args.has('--dry-run') || process.env.HOUSE_DRY_RUN === '1';
|
||||
|
||||
const list = (value: string | undefined, fallback: string[]) =>
|
||||
value
|
||||
? value
|
||||
.split(/[\s,]+/)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean)
|
||||
: fallback;
|
||||
const number = (value: string | undefined, fallback: number) =>
|
||||
value && Number.isFinite(Number(value)) ? Number(value) : fallback;
|
||||
|
||||
const config = {
|
||||
api: (process.env.HOUSE_API_URL ?? '').replace(/\/$/, ''),
|
||||
token: process.env.HOUSE_WORKER_TOKEN ?? '',
|
||||
fastSearches: list(process.env.HOUSE_FAST_SEARCHES, [
|
||||
'https://www.pararius.nl/huurwoningen/gouda/1300-1700'
|
||||
]),
|
||||
slowSearches: list(
|
||||
process.env.HOUSE_SLOW_SEARCHES,
|
||||
['gouda', 'rotterdam', 'den-haag', 'utrecht', 'amsterdam', 'eindhoven'].flatMap((city) => [
|
||||
`https://www.pararius.nl/huurwoningen/${city}`,
|
||||
`https://www.pararius.nl/koopwoningen/${city}`
|
||||
])
|
||||
),
|
||||
slowEveryHours: number(process.env.HOUSE_SLOW_EVERY_HOURS, 12),
|
||||
slowMaxPages: number(process.env.HOUSE_SLOW_MAX_PAGES, 80),
|
||||
minGapMs: Math.max(8, number(process.env.HOUSE_MIN_GAP_SECONDS, 8)) * 1000,
|
||||
dailyBudget: number(process.env.HOUSE_DAILY_BUDGET, 1500),
|
||||
fetchMode: (process.env.HOUSE_FETCH_MODE ?? 'fetch') as 'fetch' | 'browser',
|
||||
userAgent:
|
||||
process.env.HOUSE_USER_AGENT ??
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36'
|
||||
};
|
||||
|
||||
if (checkApi && dryRun) throw new Error('--check-api cannot be combined with dry-run mode');
|
||||
const requestApi = dryRun
|
||||
? null
|
||||
: createWorkerApi(
|
||||
{
|
||||
url: config.api,
|
||||
token: config.token,
|
||||
accessClientId: process.env.HOUSE_CF_ACCESS_CLIENT_ID,
|
||||
accessClientSecret: process.env.HOUSE_CF_ACCESS_CLIENT_SECRET
|
||||
},
|
||||
checkApi ? 1 : 4
|
||||
);
|
||||
|
||||
const SOURCE = 'pararius';
|
||||
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const log = (...parts: unknown[]) => console.log(new Date().toISOString(), ...parts);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Polite fetching
|
||||
|
||||
class Challenged extends Error {
|
||||
constructor(
|
||||
readonly url: string,
|
||||
readonly status: number
|
||||
) {
|
||||
super(`Challenged (${status}) on ${url}`);
|
||||
}
|
||||
}
|
||||
|
||||
const cookies = new Map<string, string>();
|
||||
let lastRequestAt = 0;
|
||||
let budgetDay = '';
|
||||
let budgetUsed = 0;
|
||||
|
||||
function remember(response: Response) {
|
||||
for (const header of response.headers.getSetCookie?.() ?? []) {
|
||||
const [pair] = header.split(';');
|
||||
const index = pair.indexOf('=');
|
||||
if (index > 0) cookies.set(pair.slice(0, index).trim(), pair.slice(index + 1).trim());
|
||||
}
|
||||
}
|
||||
|
||||
function resetBudget() {
|
||||
const day = new Date().toISOString().slice(0, 10);
|
||||
if (day !== budgetDay) {
|
||||
budgetDay = day;
|
||||
budgetUsed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/** The slow lane may only use part of the budget so the fast lane never runs dry. */
|
||||
function slowBudgetLeft() {
|
||||
resetBudget();
|
||||
return budgetUsed < config.dailyBudget * 0.6;
|
||||
}
|
||||
|
||||
async function waitForTurn() {
|
||||
resetBudget();
|
||||
if (budgetUsed >= config.dailyBudget) throw new Error('Daily request budget used up');
|
||||
const gap = config.minGapMs * (1 + Math.random() * 0.5);
|
||||
const wait = lastRequestAt + gap - Date.now();
|
||||
if (wait > 0) await sleep(wait);
|
||||
lastRequestAt = Date.now();
|
||||
budgetUsed++;
|
||||
}
|
||||
|
||||
type BrowserPage = {
|
||||
goto(url: string, options: object): Promise<{ status(): number } | null>;
|
||||
content(): Promise<string>;
|
||||
};
|
||||
let browserPage: Promise<BrowserPage> | null = null;
|
||||
|
||||
async function openBrowser(): Promise<BrowserPage> {
|
||||
// Real Chromium for when plain requests get challenged. Needs
|
||||
// `npx playwright install chromium` on the worker machine.
|
||||
const { chromium } = await import('@playwright/test');
|
||||
const context = await chromium.launchPersistentContext(
|
||||
process.env.HOUSE_BROWSER_PROFILE ?? '.house-worker-profile',
|
||||
{ headless: true, locale: 'nl-NL', timezoneId: 'Europe/Amsterdam' }
|
||||
);
|
||||
return context.pages()[0] ?? (await context.newPage());
|
||||
}
|
||||
|
||||
async function getHtml(url: string, referer?: string): Promise<{ html: string; status: number }> {
|
||||
await waitForTurn();
|
||||
if (config.fetchMode === 'browser') {
|
||||
browserPage ??= openBrowser();
|
||||
const page = await browserPage;
|
||||
const response = await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
|
||||
const html = await page.content();
|
||||
const status = response?.status() ?? 0;
|
||||
if (isChallenge(html, status)) throw new Challenged(url, status);
|
||||
return { html, status };
|
||||
}
|
||||
const response = await fetch(url, {
|
||||
redirect: 'follow',
|
||||
signal: AbortSignal.timeout(30_000),
|
||||
headers: {
|
||||
'user-agent': config.userAgent,
|
||||
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
'accept-language': 'nl-NL,nl;q=0.9,en-US;q=0.8,en;q=0.7',
|
||||
'cache-control': 'no-cache',
|
||||
...(referer ? { referer } : {}),
|
||||
...(cookies.size
|
||||
? { cookie: [...cookies].map(([key, value]) => `${key}=${value}`).join('; ') }
|
||||
: {})
|
||||
}
|
||||
});
|
||||
remember(response);
|
||||
const html = await response.text();
|
||||
if (isChallenge(html, response.status)) throw new Challenged(url, response.status);
|
||||
return { html, status: response.status };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Website API
|
||||
|
||||
async function api<T>(path: 'sync' | 'details' | 'report', body: unknown): Promise<T | null> {
|
||||
return requestApi ? requestApi<T>(path, body) : null;
|
||||
}
|
||||
|
||||
async function report(
|
||||
lane: Lane,
|
||||
url: string,
|
||||
status: RunStatus,
|
||||
extra: Partial<ReportRequest> = {}
|
||||
) {
|
||||
try {
|
||||
await api('report', { source: SOURCE, lane, url, status, ...extra });
|
||||
} catch (error) {
|
||||
log('Could not report:', (error as Error).message);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lanes
|
||||
|
||||
async function fetchDetails(
|
||||
lane: Lane,
|
||||
wanted: { sourceId: string; url: string }[],
|
||||
referer: string
|
||||
) {
|
||||
const details: ListingDetail[] = [];
|
||||
for (const item of wanted) {
|
||||
const { html } = await getHtml(item.url, referer);
|
||||
const detail = parseDetailPage(html, item.url);
|
||||
if (detail) details.push(detail);
|
||||
else await report(lane, item.url, 'parse-error', { message: 'listing page did not parse' });
|
||||
}
|
||||
if (dryRun) {
|
||||
for (const detail of details) {
|
||||
log('detail', { ...detail, description: undefined, features: undefined });
|
||||
}
|
||||
}
|
||||
if (details.length) await api('details', { source: SOURCE, details });
|
||||
}
|
||||
|
||||
async function pollSearch(
|
||||
lane: Lane,
|
||||
url: string,
|
||||
completedSweep?: SyncRequest['completedSweep']
|
||||
): Promise<{ nextPageUrl: string | null; found: number }> {
|
||||
const startedAt = new Date().toISOString();
|
||||
const { html } = await getHtml(url);
|
||||
const page = parseSearchPage(html, url);
|
||||
if (!page.cards.length && page.nextPageUrl)
|
||||
await report(lane, url, 'parse-error', {
|
||||
message: 'no listing cards on a page with a next page'
|
||||
});
|
||||
if (dryRun) {
|
||||
log(`${lane} ${url}: ${page.cards.length} cards, next=${page.nextPageUrl}`);
|
||||
for (const card of page.cards.slice(0, 5)) log('card', card);
|
||||
}
|
||||
const sync = await api<SyncResponse>('sync', {
|
||||
source: SOURCE,
|
||||
lane,
|
||||
searchUrl: url,
|
||||
startedAt,
|
||||
cards: page.cards,
|
||||
completedSweep: page.nextPageUrl ? null : (completedSweep ?? null)
|
||||
} satisfies SyncRequest);
|
||||
if (sync) {
|
||||
if (sync.newIds.length || sync.alerted)
|
||||
log(`${lane} ${url}: ${sync.newIds.length} new, ${sync.alerted} alerted`);
|
||||
if (sync.needDetails.length) await fetchDetails(lane, sync.needDetails, url);
|
||||
} else if (dryRun && page.cards[0]) {
|
||||
await fetchDetails(lane, [page.cards[0]], url);
|
||||
}
|
||||
return { nextPageUrl: page.nextPageUrl, found: page.cards.length };
|
||||
}
|
||||
|
||||
async function fastTick() {
|
||||
for (const url of config.fastSearches) await pollSearch('fast', url);
|
||||
}
|
||||
|
||||
/** Only an unfiltered `/huurwoningen/<city>` sweep can tell us what disappeared. */
|
||||
function sweepScope(url: string) {
|
||||
const [kind, city, ...rest] = new URL(url).pathname.split('/').filter(Boolean);
|
||||
if (rest.length || !city) return null;
|
||||
if (kind === 'huurwoningen') return { offer: 'rent' as const, city };
|
||||
if (kind === 'koopwoningen') return { offer: 'buy' as const, city };
|
||||
return null;
|
||||
}
|
||||
|
||||
type Sweep = { index: number; page: number; startedAt: string };
|
||||
let sweep: Sweep | null = null;
|
||||
|
||||
/** One page of the slow lane, so fast polls can interleave. Returns false when idle. */
|
||||
async function slowStep(): Promise<boolean> {
|
||||
if (!sweep || !slowBudgetLeft()) return false;
|
||||
const base = config.slowSearches[sweep.index];
|
||||
if (!base) {
|
||||
log('Slow sweep finished');
|
||||
sweep = null;
|
||||
return false;
|
||||
}
|
||||
const url = searchPageUrl(base, sweep.page);
|
||||
const scope = sweepScope(base);
|
||||
const result = await pollSearch(
|
||||
'slow',
|
||||
url,
|
||||
scope ? { ...scope, sweepStartedAt: sweep.startedAt } : null
|
||||
);
|
||||
if (result.nextPageUrl && sweep.page < config.slowMaxPages) {
|
||||
sweep.page++;
|
||||
} else {
|
||||
sweep = { index: sweep.index + 1, page: 1, startedAt: new Date().toISOString() };
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Main loop
|
||||
|
||||
let stopping = false;
|
||||
process.on('SIGTERM', () => (stopping = true));
|
||||
process.on('SIGINT', () => (stopping = true));
|
||||
|
||||
async function guarded(
|
||||
lane: Lane,
|
||||
step: () => Promise<boolean | void>
|
||||
): Promise<'ok' | 'idle' | 'failed'> {
|
||||
const started = Date.now();
|
||||
try {
|
||||
return (await step()) === false ? 'idle' : 'ok';
|
||||
} catch (error) {
|
||||
if (error instanceof Challenged) {
|
||||
log(`CHALLENGED on ${error.url} (${error.status})`);
|
||||
await report(lane, error.url, 'challenged', {
|
||||
httpStatus: error.status,
|
||||
durationMs: Date.now() - started
|
||||
});
|
||||
} else {
|
||||
log(`${lane} failed:`, (error as Error).message);
|
||||
await report(lane, config.fastSearches[0], 'network-error', {
|
||||
message: (error as Error).message.slice(0, 500),
|
||||
durationMs: Date.now() - started
|
||||
});
|
||||
}
|
||||
return 'failed';
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
if (checkApi) {
|
||||
const result = await requestApi!<{ ok: boolean }>('health');
|
||||
if (result?.ok !== true) throw new Error('API health: unexpected response');
|
||||
log('API connection OK. Worker authentication passed; no Pararius requests were made.');
|
||||
return;
|
||||
}
|
||||
log(
|
||||
`house worker: ${config.fastSearches.length} fast, ${config.slowSearches.length} slow searches, mode=${config.fetchMode}${dryRun ? ', dry run' : ''}`
|
||||
);
|
||||
if (once) {
|
||||
const result = await guarded('fast', fastTick);
|
||||
process.exit(result === 'ok' ? 0 : 1);
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
let nextFast = Date.now();
|
||||
let nextSlow = Date.now() + 5 * 60_000; // let the fast lane go first
|
||||
while (!stopping) {
|
||||
const now = Date.now();
|
||||
let result: 'ok' | 'idle' | 'failed' = 'idle';
|
||||
if (now >= nextFast) {
|
||||
result = await guarded('fast', fastTick);
|
||||
nextFast = Date.now() + nextDelayMs(new Date());
|
||||
} else if (sweep || now >= nextSlow) {
|
||||
if (!sweep) {
|
||||
log('Slow sweep starting');
|
||||
sweep = { index: 0, page: 1, startedAt: new Date().toISOString() };
|
||||
nextSlow = now + config.slowEveryHours * 60 * 60_000;
|
||||
}
|
||||
result = await guarded('slow', slowStep);
|
||||
}
|
||||
if (result === 'failed') {
|
||||
failures++;
|
||||
const pause = backoffMs(failures);
|
||||
log(`Backing off ${Math.round(pause / 60_000)} min (failure ${failures})`);
|
||||
nextFast = Math.max(nextFast, Date.now() + pause);
|
||||
await sleep(pause);
|
||||
continue;
|
||||
}
|
||||
if (result === 'ok') failures = 0;
|
||||
else await sleep(Math.min(30_000, Math.max(5_000, nextFast - Date.now())));
|
||||
}
|
||||
log('Stopped');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await main();
|
||||
15
scripts/migrate-house-tracker.ts
Normal file
15
scripts/migrate-house-tracker.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import postgres from 'postgres';
|
||||
|
||||
if (!process.env.DB_URL) throw new Error('DB_URL is required');
|
||||
const client = postgres(process.env.DB_URL, { max: 1 });
|
||||
try {
|
||||
await client.begin(async (tx) => {
|
||||
await tx.unsafe(
|
||||
await readFile(new URL('./house-tracker-migration.sql', import.meta.url), 'utf8')
|
||||
);
|
||||
});
|
||||
console.log('House tracker migration applied');
|
||||
} finally {
|
||||
await client.end();
|
||||
}
|
||||
6
src/app.d.ts
vendored
6
src/app.d.ts
vendored
@@ -6,12 +6,12 @@ declare global {
|
||||
namespace App {
|
||||
// interface Error {}
|
||||
interface Locals {
|
||||
user: typeof import('$lib/server/auth').auth.$Infer.Session.user | null;
|
||||
session: typeof import('$lib/server/auth').auth.$Infer.Session.session | null;
|
||||
user: typeof import('#lib/server/auth.js').auth.$Infer.Session.user | null;
|
||||
session: typeof import('#lib/server/auth.js').auth.$Infer.Session.session | null;
|
||||
}
|
||||
// interface PageData {}
|
||||
interface PageState {
|
||||
animal?: import('$lib/animaldex/AnimalDetails.svelte').AnimalDetail;
|
||||
animal?: import('#lib/animaldex/AnimalDetails.svelte').AnimalDetail;
|
||||
}
|
||||
// interface Platform {}
|
||||
}
|
||||
|
||||
27
src/env.ts
Normal file
27
src/env.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { defineEnvVars } from '@sveltejs/kit/env';
|
||||
|
||||
// Every variable is optional and falls back to '' (falsy, like an unset $env/dynamic
|
||||
// value used to be); the code that uses each one checks whether it is configured.
|
||||
export const variables = defineEnvVars({
|
||||
PUBLIC_MAPTILER_API_KEY: { public: true, schema: (input) => input ?? '' },
|
||||
R2_PUBLIC_URL: { schema: (input) => input ?? '' },
|
||||
ORIGIN: { schema: (input) => input ?? '' },
|
||||
BETTER_AUTH_SECRET: { schema: (input) => input ?? '' },
|
||||
BETTER_AUTH_URL: { schema: (input) => input ?? '' },
|
||||
GOOGLE_CLIENT_ID: { schema: (input) => input ?? '' },
|
||||
GOOGLE_CLIENT_SECRET: { schema: (input) => input ?? '' },
|
||||
OSU_CLIENT_ID: { schema: (input) => input ?? '' },
|
||||
OSU_CLIENT_SECRET: { schema: (input) => input ?? '' },
|
||||
ANTHROPIC_API_KEY: { schema: (input) => input ?? '' },
|
||||
HOUSE_WORKER_TOKEN: { schema: (input) => input ?? '' },
|
||||
HOUSE_DISCORD_WEBHOOK_URL: { schema: (input) => input ?? '' },
|
||||
HOUSE_NTFY_URL: { schema: (input) => input ?? '' },
|
||||
HOUSE_NTFY_TOKEN: { schema: (input) => input ?? '' },
|
||||
R2_ACCESS_KEY_ID: { schema: (input) => input ?? '' },
|
||||
R2_SECRET_ACCESS_KEY: { schema: (input) => input ?? '' },
|
||||
R2_ACCOUNT_ID: { schema: (input) => input ?? '' },
|
||||
R2_BUCKET: { schema: (input) => input ?? '' },
|
||||
SPOTIFY_CLIENT_ID: { schema: (input) => input ?? '' },
|
||||
SPOTIFY_CLIENT_SECRET: { schema: (input) => input ?? '' },
|
||||
DB_URL: { schema: (input) => input ?? '' }
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { building } from '$app/environment';
|
||||
import { auth } from '$lib/server/auth';
|
||||
import type { Handle } from '@sveltejs/kit';
|
||||
import type { Handle } from '@sveltejs/kit/hooks';
|
||||
import { building } from '$app/env';
|
||||
import { auth } from '#lib/server/auth.js';
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
// Match the endpoint path here: the dev proxy may terminate HTTPS before Vite.
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
</dl>
|
||||
|
||||
<a
|
||||
href={resolve(`/projects/animaldex/register?animalId=${animal.id}`)}
|
||||
href={resolve(`projects/animaldex/register?animalId=${animal.id}`)}
|
||||
class="mt-6 inline-flex items-center gap-2 rounded bg-[var(--site-text)] px-4 py-2.5 text-base font-medium text-[var(--site-bg)]"
|
||||
>
|
||||
i saw this animal too <ArrowRight size={16} aria-hidden="true" />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { pokemonImage } from '$lib/game-tracker/game-art';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { NativeSelect } from '$lib/components/ui/native-select/index.js';
|
||||
import * as Table from '$lib/components/ui/table/index.js';
|
||||
import { pokemonImage } from '#lib/game-tracker/game-art.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { NativeSelect } from '#lib/components/ui/native-select/index.js';
|
||||
import * as Table from '#lib/components/ui/table/index.js';
|
||||
let moveSearch = $state('');
|
||||
let moveMethod = $state('all');
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
availabilityLabels,
|
||||
availabilityValues,
|
||||
obtainableAvailability
|
||||
} from '$lib/availability';
|
||||
import type { PokemonDetail } from '$lib/server/pokemon-detail';
|
||||
} from '#lib/availability.js';
|
||||
import type { PokemonDetail } from '#lib/server/pokemon-detail.js';
|
||||
|
||||
let {
|
||||
number,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { availabilityValues } from '$lib/availability';
|
||||
import { availabilityValues } from '#lib/availability.js';
|
||||
import { summarizeProgress } from './progress';
|
||||
|
||||
const catchable = availabilityValues.indexOf('catchable');
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { availabilityValues, obtainableAvailability } from '$lib/availability';
|
||||
import { availabilityValues, obtainableAvailability } from '#lib/availability.js';
|
||||
|
||||
type PokemonWithEntries = {
|
||||
id: number;
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
<script lang="ts">
|
||||
import { authClient } from '$lib/auth-client';
|
||||
import { authClient } from '#lib/auth-client.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { resolve } from '$app/paths';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import * as Card from '$lib/components/ui/card/index.js';
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { Label } from '$lib/components/ui/label/index.js';
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import * as Card from '#lib/components/ui/card/index.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { Label } from '#lib/components/ui/label/index.js';
|
||||
import { Separator } from '#lib/components/ui/separator/index.js';
|
||||
let {
|
||||
mode,
|
||||
providers,
|
||||
@@ -43,8 +43,10 @@
|
||||
return;
|
||||
}
|
||||
// Destination is a validated local path, prefixed by resolve('/') above.
|
||||
// eslint-disable-next-line svelte/no-navigation-without-resolve
|
||||
await goto(destination, { invalidateAll: true });
|
||||
// goto rejects paths that match no route, so fall back to a full page load.
|
||||
await goto(destination, { refreshAll: true }).catch(() => {
|
||||
window.location.href = destination;
|
||||
});
|
||||
} catch {
|
||||
message = 'Unable to connect. Please try again.';
|
||||
} finally {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import type { Component } from 'svelte';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
|
||||
let {
|
||||
icon: Icon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { Progress } from '$lib/components/ui/progress/index.js';
|
||||
import { Progress } from '#lib/components/ui/progress/index.js';
|
||||
|
||||
let {
|
||||
value,
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
import ChartNoAxesCombined from 'lucide-svelte/icons/chart-no-axes-combined';
|
||||
import FolderKanban from 'lucide-svelte/icons/folder-kanban';
|
||||
import Gamepad2 from 'lucide-svelte/icons/gamepad-2';
|
||||
import House from 'lucide-svelte/icons/house';
|
||||
import Globe2 from 'lucide-svelte/icons/globe-2';
|
||||
import Languages from 'lucide-svelte/icons/languages';
|
||||
import Music2 from 'lucide-svelte/icons/music-2';
|
||||
@@ -30,7 +31,8 @@
|
||||
'world opinions': Globe2,
|
||||
'language learner': Languages,
|
||||
'money tracker': Wallet,
|
||||
'cs prices': CandlestickChart
|
||||
'cs prices': CandlestickChart,
|
||||
'house tracker': House
|
||||
};
|
||||
|
||||
const Icon = $derived(icons[name as keyof typeof icons] ?? FolderKanban);
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
buttonVariants,
|
||||
type ButtonVariant,
|
||||
type ButtonSize
|
||||
} from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
} from '#lib/components/ui/button/index.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
buttonVariants,
|
||||
type ButtonVariant,
|
||||
type ButtonSize
|
||||
} from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
} from '#lib/components/ui/button/index.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChild, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import AlertDialogOverlay from './alert-dialog-overlay.svelte';
|
||||
import AlertDialogPortal from './alert-dialog-portal.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { AlertDialog as AlertDialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAnchorAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts" module>
|
||||
import { type VariantProps, tv } from 'tailwind-variants';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from 'svelte/elements';
|
||||
|
||||
export const buttonVariants = tv({
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { Checkbox as CheckboxPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import * as Dialog from '$lib/components/ui/dialog/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import * as Dialog from '#lib/components/ui/dialog/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import Command from './command.svelte';
|
||||
import type { Command as CommandPrimitive, Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive, useId } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import * as InputGroup from '$lib/components/ui/input-group/index.js';
|
||||
import * as InputGroup from '#lib/components/ui/input-group/index.js';
|
||||
import SearchIcon from '@lucide/svelte/icons/search';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Command as CommandPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
export type CommandRootApi = CommandPrimitive.Root;
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import XIcon from '@lucide/svelte/icons/x';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import * as Dialog from './index.js';
|
||||
import DialogPortal from './dialog-portal.svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Dialog as DialogPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import MinusIcon from '@lucide/svelte/icons/minus';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import DropdownMenuPortal from './dropdown-menu-portal.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { DropdownMenu as DropdownMenuPrimitive } from 'bits-ui';
|
||||
import ChevronRightIcon from '@lucide/svelte/icons/chevron-right';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -20,8 +20,8 @@
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { Button } from '$lib/components/ui/button/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Button } from '#lib/components/ui/button/index.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Input } from '$lib/components/ui/input/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Input } from '#lib/components/ui/input/index.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Textarea } from '$lib/components/ui/textarea/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Textarea } from '#lib/components/ui/textarea/index.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLInputAttributes, HTMLInputTypeAttribute } from 'svelte/elements';
|
||||
|
||||
type InputType = Exclude<HTMLInputTypeAttribute, 'file'>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Label as LabelPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { WithElementRef } from '$lib/utils.js';
|
||||
import type { WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLOptgroupAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLOptionAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLSelectAttributes } from 'svelte/elements';
|
||||
|
||||
type NativeSelectProps = Omit<WithElementRef<HTMLSelectAttributes>, 'size'> & {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import PopoverPortal from './popover-portal.svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Popover as PopoverPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Progress as ProgressPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import type { WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '#lib/utils.js';
|
||||
import type { WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
import SelectPortal from './select-portal.svelte';
|
||||
import SelectScrollDownButton from './select-scroll-down-button.svelte';
|
||||
import SelectScrollUpButton from './select-scroll-up-button.svelte';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import CheckIcon from '@lucide/svelte/icons/check';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { cn, type WithElementRef } from '$lib/utils.js';
|
||||
import { cn, type WithElementRef } from '#lib/utils.js';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronUpIcon from '@lucide/svelte/icons/chevron-up';
|
||||
import { cn, type WithoutChildrenOrChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChildrenOrChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { Separator } from '$lib/components/ui/separator/index.js';
|
||||
import { cn } from '$lib/utils.js';
|
||||
import { Separator } from '#lib/components/ui/separator/index.js';
|
||||
import { cn } from '#lib/utils.js';
|
||||
import type { Separator as SeparatorPrimitive } from 'bits-ui';
|
||||
|
||||
let {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Select as SelectPrimitive } from 'bits-ui';
|
||||
import ChevronDownIcon from '@lucide/svelte/icons/chevron-down';
|
||||
import { cn, type WithoutChild } from '$lib/utils.js';
|
||||
import { cn, type WithoutChild } from '#lib/utils.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user