diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..f6be8af
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,34 @@
+name: CI
+
+on:
+ push:
+ branches: [main, develop]
+ pull_request:
+ branches: [main]
+
+jobs:
+ lint-and-test:
+ name: Lint & Unit Tests
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+ cache: 'npm'
+
+ - name: Install dependencies
+ run: npm ci --ignore-scripts
+
+ - name: Format check (Prettier)
+ run: npm run format:check
+
+ - name: Lint (ESLint)
+ run: npm run lint
+
+ - name: Unit tests (Vitest)
+ run: npm test
diff --git a/.prettierrc.json b/.prettierrc.json
new file mode 100644
index 0000000..044758e
--- /dev/null
+++ b/.prettierrc.json
@@ -0,0 +1,7 @@
+{
+ "singleQuote": true,
+ "semi": true,
+ "trailingComma": "all",
+ "printWidth": 100,
+ "arrowParens": "always"
+}
diff --git a/NOTICE b/NOTICE
index 0dcce68..eeb64d2 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,4 +1,4 @@
-ImmoPilot
+Immo-Pilot
Copyright 2026 Christopher Henkel
This product is licensed under the Apache License, Version 2.0.
diff --git a/README.md b/README.md
index 8b184b0..ea58cd6 100644
--- a/README.md
+++ b/README.md
@@ -31,7 +31,7 @@ Currently supported: **ImmobilienScout24** & **Kleinanzeigen** β more provider
- π« **Blacklist** β per click or globally via keywords
- β€οΈ **Favorites** β persisted even if the agent is deleted
- π **Scraping** β manual, on startup, or via cron; with pagination and duplicate filtering
-- π§© **Provider system** β currently: Kleinanzeigen; more planned
+- π§© **Provider system** β currently: **ImmobilienScout24** & **Kleinanzeigen**; more planned
- ποΈ **Local** β SQLite
## Quickstart
diff --git a/eslint.config.js b/eslint.config.js
new file mode 100644
index 0000000..35bfa4f
--- /dev/null
+++ b/eslint.config.js
@@ -0,0 +1,24 @@
+import js from '@eslint/js';
+import globals from 'globals';
+import prettierConfig from 'eslint-config-prettier';
+
+export default [
+ js.configs.recommended,
+ prettierConfig,
+ {
+ languageOptions: {
+ globals: { ...globals.node },
+ ecmaVersion: 2022,
+ sourceType: 'module',
+ },
+ rules: {
+ 'no-unused-vars': ['warn', { argsIgnorePattern: '^_', caughtErrorsIgnorePattern: '^_' }],
+ 'no-console': 'off',
+ // Empty catch blocks are used intentionally throughout (fire-and-forget patterns)
+ 'no-empty': ['error', { allowEmptyCatch: true }],
+ },
+ },
+ {
+ ignores: ['client/', 'public/', 'node_modules/', 'data/', 'reports/'],
+ },
+];
diff --git a/package-lock.json b/package-lock.json
index 8c9414e..c6c111a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,10 +17,733 @@
"node-cron": "^3.0.3",
"playwright": "^1.42.1"
},
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "eslint": "^10.0.3",
+ "eslint-config-prettier": "^10.1.8",
+ "globals": "^17.4.0",
+ "prettier": "^3.8.1",
+ "vitest": "^4.1.0"
+ },
"engines": {
"node": ">=22.5.0"
}
},
+ "node_modules/@emnapi/core": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz",
+ "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz",
+ "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.23.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz",
+ "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^3.0.3",
+ "debug": "^4.3.1",
+ "minimatch": "^10.2.4"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/config-array/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.5.3",
+ "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz",
+ "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.1.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz",
+ "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "10.0.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
+ "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "eslint": "^10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz",
+ "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz",
+ "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^1.1.1",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/runtime": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz",
+ "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.115.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz",
+ "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz",
+ "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz",
+ "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz",
+ "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz",
+ "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz",
+ "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz",
+ "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz",
+ "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/esrecurse": {
+ "version": "4.3.1",
+ "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
+ "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz",
+ "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.0",
+ "@vitest/utils": "4.1.0",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz",
+ "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.0",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz",
+ "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz",
+ "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.0",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz",
+ "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.0",
+ "@vitest/utils": "4.1.0",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz",
+ "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz",
+ "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.0",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
"node_modules/accepts": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
@@ -43,12 +766,73 @@
"node": ">= 0.6"
}
},
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "fast-json-stable-stringify": "^2.0.0",
+ "json-schema-traverse": "^0.4.1",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
"node_modules/array-flatten": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
"license": "MIT"
},
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
@@ -73,6 +857,19 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/brace-expansion": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
+ "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@@ -111,6 +908,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/compressible": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz",
@@ -162,6 +969,13 @@
"node": ">= 0.6"
}
},
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/cookie": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
@@ -194,6 +1008,21 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
"node_modules/debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
@@ -203,6 +1032,13 @@
"ms": "2.0.0"
}
},
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -222,6 +1058,16 @@
"npm": "1.2.8000 || >= 1.4.16"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dotenv": {
"version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -269,35 +1115,259 @@
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
- "node": ">= 0.4"
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "10.0.3",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz",
+ "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.8.0",
+ "@eslint-community/regexpp": "^4.12.2",
+ "@eslint/config-array": "^0.23.3",
+ "@eslint/config-helpers": "^0.5.2",
+ "@eslint/core": "^1.1.1",
+ "@eslint/plugin-kit": "^0.6.1",
+ "@humanfs/node": "^0.16.6",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@humanwhocodes/retry": "^0.4.2",
+ "@types/estree": "^1.0.6",
+ "ajv": "^6.14.0",
+ "cross-spawn": "^7.0.6",
+ "debug": "^4.3.2",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^9.1.2",
+ "eslint-visitor-keys": "^5.0.1",
+ "espree": "^11.1.1",
+ "esquery": "^1.7.0",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^8.0.0",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "minimatch": "^10.2.4",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-config-prettier": {
+ "version": "10.1.8",
+ "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz",
+ "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "eslint-config-prettier": "bin/cli.js"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint-config-prettier"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "9.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
+ "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@types/esrecurse": "^4.3.1",
+ "@types/estree": "^1.0.8",
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint/node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint/node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/espree": {
+ "version": "11.2.0",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
+ "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.16.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^5.0.1"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
}
},
- "node_modules/es-errors": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
- "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
- "license": "MIT",
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
"engines": {
- "node": ">= 0.4"
+ "node": ">=4.0"
}
},
- "node_modules/es-object-atoms": {
- "version": "1.1.1",
- "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
- "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
"license": "MIT",
"dependencies": {
- "es-errors": "^1.3.0"
- },
- "engines": {
- "node": ">= 0.4"
+ "@types/estree": "^1.0.0"
}
},
- "node_modules/escape-html": {
- "version": "1.0.3",
- "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
- "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
- "license": "MIT"
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
},
"node_modules/etag": {
"version": "1.8.1",
@@ -308,6 +1378,16 @@
"node": ">= 0.6"
}
},
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
@@ -354,6 +1434,58 @@
"url": "https://opencollective.com/express"
}
},
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
"node_modules/finalhandler": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
@@ -372,6 +1504,44 @@
"node": ">= 0.8"
}
},
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz",
+ "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/forwarded": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -450,6 +1620,32 @@
"node": ">= 0.4"
}
},
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz",
+ "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -518,6 +1714,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
@@ -533,6 +1749,368 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.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"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -611,12 +2189,54 @@
"node": ">= 0.6"
}
},
+ "node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
"node_modules/ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"license": "MIT"
},
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/negotiator": {
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz",
@@ -659,6 +2279,17 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
"node_modules/on-finished": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
@@ -680,6 +2311,56 @@
"node": ">= 0.8"
}
},
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0",
+ "word-wrap": "^1.2.5"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -689,12 +2370,60 @@
"node": ">= 0.8"
}
},
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/path-to-regexp": {
"version": "0.1.12",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
"license": "MIT"
},
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
"node_modules/playwright": {
"version": "1.58.2",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
@@ -725,6 +2454,61 @@
"node": ">=18"
}
},
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/prettier": {
+ "version": "3.8.1",
+ "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
+ "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "prettier": "bin/prettier.cjs"
+ },
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/prettier/prettier?sponsor=1"
+ }
+ },
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -738,6 +2522,16 @@
"node": ">= 0.10"
}
},
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
@@ -777,6 +2571,40 @@
"node": ">= 0.8"
}
},
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.9",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz",
+ "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.115.0",
+ "@rolldown/pluginutils": "1.0.0-rc.9"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.9",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9"
+ }
+ },
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -854,6 +2682,29 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/side-channel": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
@@ -926,6 +2777,30 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
@@ -935,6 +2810,57 @@
"node": ">= 0.8"
}
},
+ "node_modules/std-env": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+ "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
+ "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
"node_modules/toidentifier": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
@@ -944,6 +2870,27 @@
"node": ">=0.6"
}
},
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
"node_modules/type-is": {
"version": "1.6.18",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
@@ -966,6 +2913,16 @@
"node": ">= 0.8"
}
},
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
"node_modules/utils-merge": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
@@ -992,6 +2949,239 @@
"engines": {
"node": ">= 0.8"
}
+ },
+ "node_modules/vite": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz",
+ "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@oxc-project/runtime": "0.115.0",
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.3",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.9",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.0.0-alpha.31",
+ "esbuild": "^0.27.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz",
+ "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.0",
+ "@vitest/mocker": "4.1.0",
+ "@vitest/pretty-format": "4.1.0",
+ "@vitest/runner": "4.1.0",
+ "@vitest/snapshot": "4.1.0",
+ "@vitest/spy": "4.1.0",
+ "@vitest/utils": "4.1.0",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.0.3",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.0",
+ "@vitest/browser-preview": "4.1.0",
+ "@vitest/browser-webdriverio": "4.1.0",
+ "@vitest/ui": "4.1.0",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/package.json b/package.json
index 0655dfd..985d7da 100644
--- a/package.json
+++ b/package.json
@@ -1,16 +1,21 @@
{
"name": "immo-pilot",
"version": "1.0.0",
- "description": "ImmoPilot - die Wohnungssuche-App",
+ "description": "Immo-Pilot - die Wohnungssuche-App",
"type": "module",
"main": "src/server.js",
"scripts": {
- "start": "node src/server.js",
- "dev": "node --watch src/server.js",
- "dev:client": "cd client && npm run dev",
- "build:client": "cd client && npm run build",
- "db": "node scripts/query-db.js",
- "postinstall": "playwright install chromium"
+ "start": "node src/server.js",
+ "dev": "node --watch src/server.js",
+ "dev:client": "cd client && npm run dev",
+ "build:client": "cd client && npm run build",
+ "db": "node scripts/query-db.js",
+ "test": "vitest run",
+ "test:watch": "vitest",
+ "lint": "eslint src/ tests/",
+ "format": "prettier --write src/ tests/",
+ "format:check": "prettier --check src/ tests/",
+ "postinstall": "playwright install chromium"
},
"dependencies": {
"compression": "^1.7.4",
@@ -23,5 +28,13 @@
"license": "Apache-2.0",
"engines": {
"node": ">=22.5.0"
+ },
+ "devDependencies": {
+ "@eslint/js": "^10.0.1",
+ "eslint": "^10.0.3",
+ "eslint-config-prettier": "^10.1.8",
+ "globals": "^17.4.0",
+ "prettier": "^3.8.1",
+ "vitest": "^4.1.0"
}
}
diff --git a/src/db/database.js b/src/db/database.js
index 495dc42..1a80a58 100644
--- a/src/db/database.js
+++ b/src/db/database.js
@@ -9,8 +9,8 @@ import fs from 'fs';
import { normalizeAvailableFrom } from '../utils.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
-const DB_DIR = path.join(__dirname, '..', '..', 'data');
-const DB_PATH = path.join(DB_DIR, 'listings.db');
+const DB_DIR = path.join(__dirname, '..', '..', 'data');
+const DB_PATH = path.join(DB_DIR, 'listings.db');
if (!fs.existsSync(DB_DIR)) fs.mkdirSync(DB_DIR, { recursive: true });
@@ -72,15 +72,19 @@ db.exec(`
// ββ Migrations for existing DBs βββββββββββββββββββββββββββββββββββββββββββ
-const safeAlter = (sql) => { try { db.exec(sql); } catch (_) {} };
+const safeAlter = (sql) => {
+ try {
+ db.exec(sql);
+ } catch (_) {}
+};
// listings table additions
safeAlter('ALTER TABLE listings ADD COLUMN rooms TEXT');
safeAlter('ALTER TABLE listings ADD COLUMN publisher TEXT');
safeAlter('ALTER TABLE listings ADD COLUMN listed_at TEXT');
safeAlter('ALTER TABLE listings ADD COLUMN images TEXT');
-safeAlter('ALTER TABLE listings ADD COLUMN provider TEXT DEFAULT \'kleinanzeigen\'');
-safeAlter('ALTER TABLE listings ADD COLUMN listing_type TEXT DEFAULT \'miete\'');
+safeAlter("ALTER TABLE listings ADD COLUMN provider TEXT DEFAULT 'kleinanzeigen'");
+safeAlter("ALTER TABLE listings ADD COLUMN listing_type TEXT DEFAULT 'miete'");
safeAlter('ALTER TABLE listings ADD COLUMN search_config_id INTEGER');
safeAlter('ALTER TABLE listings ADD COLUMN is_blacklisted INTEGER DEFAULT 0');
@@ -90,7 +94,7 @@ safeAlter('ALTER TABLE scrape_runs ADD COLUMN listing_type TEXT');
safeAlter('ALTER TABLE scrape_runs ADD COLUMN search_config_id INTEGER');
// search_configs table additions
-safeAlter('ALTER TABLE search_configs ADD COLUMN name TEXT DEFAULT \'\'');
+safeAlter("ALTER TABLE search_configs ADD COLUMN name TEXT DEFAULT ''");
safeAlter('ALTER TABLE search_configs DROP COLUMN city');
safeAlter('ALTER TABLE search_configs DROP COLUMN radius');
@@ -104,28 +108,36 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_listings_link ON listings(link)');
// Migration: set search_config_id = NULL for orphaned listings (deleted agents)
{
- const result = db.prepare('UPDATE listings SET search_config_id = NULL WHERE search_config_id IS NOT NULL AND search_config_id NOT IN (SELECT id FROM search_configs)').run();
- if (result.changes > 0) console.log(`[db] ${result.changes} orphaned listing(s) cleaned up (deleted agents).`);
+ const result = db
+ .prepare(
+ 'UPDATE listings SET search_config_id = NULL WHERE search_config_id IS NOT NULL AND search_config_id NOT IN (SELECT id FROM search_configs)',
+ )
+ .run();
+ if (result.changes > 0)
+ console.log(`[db] ${result.changes} orphaned listing(s) cleaned up (deleted agents).`);
}
// Upgrade image URLs of all existing listings to full resolution (one-time migration)
{
const rows = db.prepare('SELECT id, image FROM listings WHERE image IS NOT NULL').all();
- const upgrade = (u) => u
- .replace('/thumbs/images/', '/images/')
- .replace(/s-l\d+\./, 's-l1600.');
+ const upgrade = (u) => u.replace('/thumbs/images/', '/images/').replace(/s-l\d+\./, 's-l1600.');
const stmt = db.prepare('UPDATE listings SET image = ? WHERE id = ?');
let count = 0;
for (const row of rows) {
const upgraded = upgrade(row.image);
- if (upgraded !== row.image) { stmt.run(upgraded, row.id); count++; }
+ if (upgraded !== row.image) {
+ stmt.run(upgraded, row.id);
+ count++;
+ }
}
if (count > 0) console.log(`[db] ${count} image URL(s) upgraded to s-l1600.`);
}
// Normalize available_from values to ISO where possible
{
- const rows = db.prepare('SELECT id, available_from FROM listings WHERE available_from IS NOT NULL').all();
+ const rows = db
+ .prepare('SELECT id, available_from FROM listings WHERE available_from IS NOT NULL')
+ .all();
const stmt = db.prepare('UPDATE listings SET available_from = ? WHERE id = ?');
let count = 0;
for (const row of rows) {
@@ -140,11 +152,21 @@ db.exec('CREATE INDEX IF NOT EXISTS idx_listings_link ON listings(link)');
// ββ Search Configs ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-export function createSearchConfig({ provider = 'kleinanzeigen', listingType = 'miete', maxPages = 10, extraParams = {}, name = '' }) {
- const res = db.prepare(`
+export function createSearchConfig({
+ provider = 'kleinanzeigen',
+ listingType = 'miete',
+ maxPages = 10,
+ extraParams = {},
+ name = '',
+}) {
+ const res = db
+ .prepare(
+ `
INSERT INTO search_configs (provider, listing_type, max_pages, extra_params, name)
VALUES (?, ?, ?, ?, ?)
- `).run(provider, listingType, maxPages, JSON.stringify(extraParams), name);
+ `,
+ )
+ .run(provider, listingType, maxPages, JSON.stringify(extraParams), name);
return getSearchConfigById(Number(res.lastInsertRowid));
}
@@ -166,8 +188,17 @@ export function updateSearchConfig(id, data) {
const fields = [];
const params = [];
for (const [k, v] of Object.entries(data)) {
- const col = k === 'listingType' ? 'listing_type' : k === 'maxPages' ? 'max_pages' : k === 'extraParams' ? 'extra_params' : k;
- if (['provider', 'listing_type', 'max_pages', 'extra_params', 'enabled', 'name'].includes(col)) {
+ const col =
+ k === 'listingType'
+ ? 'listing_type'
+ : k === 'maxPages'
+ ? 'max_pages'
+ : k === 'extraParams'
+ ? 'extra_params'
+ : k;
+ if (
+ ['provider', 'listing_type', 'max_pages', 'extra_params', 'enabled', 'name'].includes(col)
+ ) {
fields.push(`${col} = ?`);
params.push(col === 'extra_params' ? JSON.stringify(v) : v);
}
@@ -180,7 +211,9 @@ export function updateSearchConfig(id, data) {
export function deleteSearchConfig(id) {
// Detach favorited & blacklisted listings β they survive agent deletion
- db.prepare('UPDATE listings SET search_config_id = NULL WHERE search_config_id = ? AND (is_favorite = 1 OR is_blacklisted = 1)').run(id);
+ db.prepare(
+ 'UPDATE listings SET search_config_id = NULL WHERE search_config_id = ? AND (is_favorite = 1 OR is_blacklisted = 1)',
+ ).run(id);
// Delete remaining listings for this agent
db.prepare('DELETE FROM listings WHERE search_config_id = ?').run(id);
// Delete scrape run history for this agent
@@ -194,8 +227,13 @@ export function deleteSearchConfig(id) {
export function blacklistListing(listingId) {
const listing = getListingById(listingId);
if (!listing) throw new Error('Listing nicht gefunden');
- db.prepare('INSERT OR IGNORE INTO blacklist (listing_id, url) VALUES (?, ?)').run(listingId, listing.link);
- db.prepare("UPDATE listings SET is_blacklisted = 1, is_favorite = 0, favorited_at = NULL, blacklisted_at = datetime('now') WHERE id = ?").run(listingId);
+ db.prepare('INSERT OR IGNORE INTO blacklist (listing_id, url) VALUES (?, ?)').run(
+ listingId,
+ listing.link,
+ );
+ db.prepare(
+ "UPDATE listings SET is_blacklisted = 1, is_favorite = 0, favorited_at = NULL, blacklisted_at = datetime('now') WHERE id = ?",
+ ).run(listingId);
return { ok: true, wasFavorite: listing.is_favorite === 1 };
}
@@ -216,11 +254,15 @@ export function getBlacklistCount() {
}
export function clearAllFavorites() {
- db.prepare('UPDATE listings SET is_favorite = 0, favorited_at = NULL WHERE is_favorite = 1').run();
+ db.prepare(
+ 'UPDATE listings SET is_favorite = 0, favorited_at = NULL WHERE is_favorite = 1',
+ ).run();
}
export function clearFavoritesByConfig(searchConfigId) {
- db.prepare('UPDATE listings SET is_favorite = 0, favorited_at = NULL WHERE search_config_id = ? AND is_favorite = 1').run(searchConfigId);
+ db.prepare(
+ 'UPDATE listings SET is_favorite = 0, favorited_at = NULL WHERE search_config_id = ? AND is_favorite = 1',
+ ).run(searchConfigId);
}
export function clearAllBlacklist() {
@@ -229,21 +271,33 @@ export function clearAllBlacklist() {
}
export function clearBlacklistByConfig(searchConfigId) {
- db.prepare(`
+ db.prepare(
+ `
DELETE FROM blacklist WHERE listing_id IN (
SELECT id FROM listings WHERE search_config_id = ?
)
- `).run(searchConfigId);
- db.prepare('UPDATE listings SET is_blacklisted = 0 WHERE search_config_id = ? AND is_blacklisted = 1').run(searchConfigId);
+ `,
+ ).run(searchConfigId);
+ db.prepare(
+ 'UPDATE listings SET is_blacklisted = 0 WHERE search_config_id = ? AND is_blacklisted = 1',
+ ).run(searchConfigId);
}
// ββ Stats ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function getStats() {
- const total = db.prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_blacklisted = 0').get()?.cnt ?? 0;
- const unseen = db.prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_blacklisted = 0 AND is_seen = 0').get()?.cnt ?? 0;
- const favorites = db.prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_favorite = 1 AND is_blacklisted = 0').get()?.cnt ?? 0;
- const blacklisted = db.prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_blacklisted = 1').get()?.cnt ?? 0;
+ const total =
+ db.prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_blacklisted = 0').get()?.cnt ?? 0;
+ const unseen =
+ db
+ .prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_blacklisted = 0 AND is_seen = 0')
+ .get()?.cnt ?? 0;
+ const favorites =
+ db
+ .prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_favorite = 1 AND is_blacklisted = 0')
+ .get()?.cnt ?? 0;
+ const blacklisted =
+ db.prepare('SELECT COUNT(*) as cnt FROM listings WHERE is_blacklisted = 1').get()?.cnt ?? 0;
return { total, unseen, favorites, blacklisted };
}
@@ -261,28 +315,41 @@ export function upsertListing(l) {
// or is an unpinned orphan (no favorite, no blacklist entry).
// Listings from other agents (favorited/blacklisted by another agent) remain
// untouched β no cross-agent carry-over.
- let carrySeen = 0, carryFav = 0, carryFirstSeen = null;
+ let carrySeen = 0,
+ carryFav = 0,
+ carryFirstSeen = null;
if (l.link) {
const existingByLink = db.prepare('SELECT * FROM listings WHERE link = ? LIMIT 1').get(l.link);
if (existingByLink && existingByLink.id !== l.id) {
- const isSameAgent = existingByLink.search_config_id === l.search_config_id;
- const isSafeOrphan = existingByLink.search_config_id == null
- && !existingByLink.is_favorite
- && !existingByLink.is_blacklisted;
+ const isSameAgent = existingByLink.search_config_id === l.search_config_id;
+ const isSafeOrphan =
+ existingByLink.search_config_id == null &&
+ !existingByLink.is_favorite &&
+ !existingByLink.is_blacklisted;
if (isSameAgent || isSafeOrphan) {
// Same agent or unpinned orphan β carry over flags, replace old entry
carrySeen = existingByLink.is_seen ?? 0;
- carryFav = existingByLink.is_favorite ?? 0;
+ carryFav = existingByLink.is_favorite ?? 0;
carryFirstSeen = existingByLink.first_seen ?? null;
- try { db.prepare('UPDATE blacklist SET listing_id = ? WHERE listing_id = ?').run(l.id, existingByLink.id); } catch {}
- try { db.prepare('UPDATE blacklist SET listing_id = ? WHERE url = ?').run(l.id, l.link); } catch {}
- try { db.prepare('DELETE FROM listings WHERE id = ?').run(existingByLink.id); } catch {}
+ try {
+ db.prepare('UPDATE blacklist SET listing_id = ? WHERE listing_id = ?').run(
+ l.id,
+ existingByLink.id,
+ );
+ } catch {}
+ try {
+ db.prepare('UPDATE blacklist SET listing_id = ? WHERE url = ?').run(l.id, l.link);
+ } catch {}
+ try {
+ db.prepare('DELETE FROM listings WHERE id = ?').run(existingByLink.id);
+ } catch {}
}
// Otherwise (different agent with a pinned entry): leave the old entry untouched,
// the new listing will be inserted normally for the current agent.
}
}
- db.prepare(`
+ db.prepare(
+ `
INSERT INTO listings (id, source, provider, listing_type, search_config_id, title, price, size, rooms, address, description, publisher, link, image, is_blacklisted, listed_at, available_from, first_seen, last_seen)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
@@ -297,17 +364,35 @@ export function upsertListing(l) {
available_from = COALESCE(excluded.available_from, available_from),
last_seen = excluded.last_seen,
search_config_id = COALESCE(search_config_id, excluded.search_config_id)
- `).run(
- l.id, l.source ?? l.provider ?? 'kleinanzeigen', l.provider ?? 'kleinanzeigen', l.listing_type ?? 'miete',
+ `,
+ ).run(
+ l.id,
+ l.source ?? l.provider ?? 'kleinanzeigen',
+ l.provider ?? 'kleinanzeigen',
+ l.listing_type ?? 'miete',
l.search_config_id ?? null,
- l.title, l.price ?? null, l.size ?? null, l.rooms ?? null,
- l.address ?? null, l.description ?? null, l.publisher ?? null, l.link, l.image ?? null,
- l.is_blacklisted ?? 0, l.listed_at ?? null, l.available_from ?? null, (carryFirstSeen && (!l.first_seen || carryFirstSeen < l.first_seen)) ? carryFirstSeen : l.first_seen, l.last_seen
+ l.title,
+ l.price ?? null,
+ l.size ?? null,
+ l.rooms ?? null,
+ l.address ?? null,
+ l.description ?? null,
+ l.publisher ?? null,
+ l.link,
+ l.image ?? null,
+ l.is_blacklisted ?? 0,
+ l.listed_at ?? null,
+ l.available_from ?? null,
+ carryFirstSeen && (!l.first_seen || carryFirstSeen < l.first_seen)
+ ? carryFirstSeen
+ : l.first_seen,
+ l.last_seen,
);
if (carrySeen || carryFav) {
- db.prepare('UPDATE listings SET is_seen = COALESCE(?, is_seen), is_favorite = COALESCE(?, is_favorite) WHERE id = ?')
- .run(carrySeen ? 1 : null, carryFav ? 1 : null, l.id);
+ db.prepare(
+ 'UPDATE listings SET is_seen = COALESCE(?, is_seen), is_favorite = COALESCE(?, is_favorite) WHERE id = ?',
+ ).run(carrySeen ? 1 : null, carryFav ? 1 : null, l.id);
}
}
@@ -332,39 +417,54 @@ export function toggleFavorite(id) {
if (!listing) return null;
if (listing.is_blacklisted) {
db.prepare('DELETE FROM blacklist WHERE listing_id = ?').run(id);
- db.prepare("UPDATE listings SET is_favorite = 1, is_blacklisted = 0, favorited_at = datetime('now') WHERE id = ?").run(id);
+ db.prepare(
+ "UPDATE listings SET is_favorite = 1, is_blacklisted = 0, favorited_at = datetime('now') WHERE id = ?",
+ ).run(id);
} else if (listing.is_favorite) {
db.prepare('UPDATE listings SET is_favorite = 0, favorited_at = NULL WHERE id = ?').run(id);
} else {
- db.prepare("UPDATE listings SET is_favorite = 1, favorited_at = datetime('now') WHERE id = ?").run(id);
+ db.prepare(
+ "UPDATE listings SET is_favorite = 1, favorited_at = datetime('now') WHERE id = ?",
+ ).run(id);
}
return getListingById(id);
}
export function getListings({
- onlyUnseen = false,
- onlyFavorites = false,
- listingType = null,
- provider = null,
- searchConfigId = null,
- hideBlacklisted = true,
- showBlacklisted = false,
- includeBlacklisted = false,
- blacklistKeywords = [],
+ onlyUnseen = false,
+ onlyFavorites = false,
+ listingType = null,
+ provider = null,
+ searchConfigId = null,
+ hideBlacklisted = true,
+ showBlacklisted = false,
+ includeBlacklisted = false,
+ blacklistKeywords = [],
} = {}) {
const conditions = [];
- const params = [];
+ const params = [];
- if (onlyUnseen) conditions.push('is_seen = 0');
- if (onlyFavorites) conditions.push('is_favorite = 1');
- if (showBlacklisted) conditions.push('is_blacklisted = 1');
+ if (onlyUnseen) conditions.push('is_seen = 0');
+ if (onlyFavorites) conditions.push('is_favorite = 1');
+ if (showBlacklisted) conditions.push('is_blacklisted = 1');
else if (!includeBlacklisted && hideBlacklisted) conditions.push('is_blacklisted = 0');
- if (listingType) { conditions.push('listing_type = ?'); params.push(listingType); }
- if (provider) { conditions.push('provider = ?'); params.push(provider); }
- if (searchConfigId) { conditions.push('search_config_id = ?'); params.push(searchConfigId); }
+ if (listingType) {
+ conditions.push('listing_type = ?');
+ params.push(listingType);
+ }
+ if (provider) {
+ conditions.push('provider = ?');
+ params.push(provider);
+ }
+ if (searchConfigId) {
+ conditions.push('search_config_id = ?');
+ params.push(searchConfigId);
+ }
for (const term of blacklistKeywords) {
- conditions.push("(LOWER(COALESCE(title,'')) NOT LIKE LOWER(?) AND LOWER(COALESCE(description,'')) NOT LIKE LOWER(?) AND LOWER(COALESCE(publisher,'')) NOT LIKE LOWER(?))");
+ conditions.push(
+ "(LOWER(COALESCE(title,'')) NOT LIKE LOWER(?) AND LOWER(COALESCE(description,'')) NOT LIKE LOWER(?) AND LOWER(COALESCE(publisher,'')) NOT LIKE LOWER(?))",
+ );
params.push(`%${term}%`, `%${term}%`, `%${term}%`);
}
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
@@ -387,22 +487,38 @@ export function purgeListingsKeepPinned() {
db.exec('VACUUM');
}
-export function purgeListingsByConfig(searchConfigId) {
- db.prepare('DELETE FROM listings WHERE search_config_id = ? AND is_favorite = 0 AND is_blacklisted = 0').run(searchConfigId);
+export function purgeListingsByConfig(searchConfigId) {
+ db.prepare(
+ 'DELETE FROM listings WHERE search_config_id = ? AND is_favorite = 0 AND is_blacklisted = 0',
+ ).run(searchConfigId);
}
export function getExistingIds(provider, listingType, searchConfigId = null) {
const conditions = [];
const params = [];
- if (provider) { conditions.push('provider = ?'); params.push(provider); }
- if (listingType) { conditions.push('listing_type = ?'); params.push(listingType); }
- if (searchConfigId) { conditions.push('search_config_id = ?'); params.push(searchConfigId); }
+ if (provider) {
+ conditions.push('provider = ?');
+ params.push(provider);
+ }
+ if (listingType) {
+ conditions.push('listing_type = ?');
+ params.push(listingType);
+ }
+ if (searchConfigId) {
+ conditions.push('search_config_id = ?');
+ params.push(searchConfigId);
+ }
const where = conditions.length ? `WHERE ${conditions.join(' AND ')}` : '';
- return db.prepare(`SELECT id FROM listings ${where}`).all(...params).map(r => r.id);
+ return db
+ .prepare(`SELECT id FROM listings ${where}`)
+ .all(...params)
+ .map((r) => r.id);
}
export function getStatsPerConfig() {
- return db.prepare(`
+ return db
+ .prepare(
+ `
SELECT
search_config_id,
SUM(CASE WHEN is_blacklisted = 0 THEN 1 ELSE 0 END) as total,
@@ -411,37 +527,49 @@ export function getStatsPerConfig() {
SUM(CASE WHEN is_blacklisted = 1 THEN 1 ELSE 0 END) as blacklisted
FROM listings
GROUP BY search_config_id
- `).all();
+ `,
+ )
+ .all();
}
// Stats for detached listings (search_config_id IS NULL) β favorites of deleted agents
export function getOrphanStats() {
- const row = db.prepare(`
+ const row = db
+ .prepare(
+ `
SELECT
COUNT(*) as total,
SUM(CASE WHEN is_seen = 0 THEN 1 ELSE 0 END) as unseen,
SUM(CASE WHEN is_favorite = 1 THEN 1 ELSE 0 END) as favorites
FROM listings
WHERE search_config_id IS NULL AND is_blacklisted = 0
- `).get();
+ `,
+ )
+ .get();
return { total: row?.total ?? 0, unseen: row?.unseen ?? 0, favorites: row?.favorites ?? 0 };
}
// ββ Scrape-Runs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
export function startScrapeRun(source, startedAt, { provider, listingType, searchConfigId } = {}) {
- const res = db.prepare(`
+ const res = db
+ .prepare(
+ `
INSERT INTO scrape_runs (source, started_at, status, provider, listing_type, search_config_id)
VALUES (?, ?, 'running', ?, ?, ?)
- `).run(source, startedAt, provider ?? source, listingType ?? null, searchConfigId ?? null);
+ `,
+ )
+ .run(source, startedAt, provider ?? source, listingType ?? null, searchConfigId ?? null);
return Number(res.lastInsertRowid);
}
export function finishScrapeRun(runId, { endedAt, status, newCount, totalCount, error }) {
- db.prepare(`
+ db.prepare(
+ `
UPDATE scrape_runs SET ended_at = ?, status = ?, new_count = ?, total_count = ?, error = ?
WHERE id = ?
- `).run(endedAt, status, newCount, totalCount, error ?? null, runId);
+ `,
+ ).run(endedAt, status, newCount, totalCount, error ?? null, runId);
}
export function getRecentRuns(limit = 20) {
diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js
index f81f695..a0de89a 100644
--- a/src/middleware/errorHandler.js
+++ b/src/middleware/errorHandler.js
@@ -19,7 +19,7 @@ export function notFoundHandler(req, res, next) {
/**
* Global error handler.
*/
-export function errorHandler(err, req, res, next) {
+export function errorHandler(err, req, res, _next) {
const status = err.status || err.statusCode || 500;
const message = err.message || 'Internal Server Error';
@@ -51,7 +51,7 @@ export function errorHandler(err, req, res, next) {
*/
export function requestLogger(req, res, next) {
const start = Date.now();
-
+
res.on('finish', () => {
const duration = Date.now() - start;
const logLevel = res.statusCode >= 400 ? 'ERROR' : 'INFO';
diff --git a/src/providers/immoscout24/index.js b/src/providers/immoscout24/index.js
index 04ebdd2..2d2ad7b 100644
--- a/src/providers/immoscout24/index.js
+++ b/src/providers/immoscout24/index.js
@@ -12,10 +12,17 @@
* - shorttermaccommodation β Wohnen auf Zeit
*/
-import { buildHash, LISTING_PATTERNS, parsePublishedDate, pickByPattern, sleep } from '../../utils.js';
+import {
+ buildHash,
+ LISTING_PATTERNS,
+ parsePublishedDate,
+ pickByPattern,
+ sleep,
+} from '../../utils.js';
// Set LOG_RAW_VS_PARSED=1 to enable per-listing debug output.
-const LOG_RAW_VS_PARSED = process.env.LOG_RAW_VS_PARSED === '1' || process.env.LOG_RAW_VS_PARSED === 'true';
+const LOG_RAW_VS_PARSED =
+ process.env.LOG_RAW_VS_PARSED === '1' || process.env.LOG_RAW_VS_PARSED === 'true';
// ββ Error Class βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -30,10 +37,10 @@ class Is24ProviderError extends Error {
// ββ Property Types ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const PROPERTY_KINDS = [
- { key: 'apartmentrent', label: 'Wohnung mieten' },
- { key: 'apartmentbuy', label: 'Wohnung kaufen' },
- { key: 'houserent', label: 'Haus mieten' },
- { key: 'housebuy', label: 'Haus kaufen' },
+ { key: 'apartmentrent', label: 'Wohnung mieten' },
+ { key: 'apartmentbuy', label: 'Wohnung kaufen' },
+ { key: 'houserent', label: 'Haus mieten' },
+ { key: 'housebuy', label: 'Haus kaufen' },
{ key: 'shorttermaccommodation', label: 'Wohnen auf Zeit' },
];
@@ -42,10 +49,10 @@ const KNOWN_PROPERTY_KEYS = new Set(PROPERTY_KINDS.map((k) => k.key));
// ββ Path Routing ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const PATH_ROUTES = [
- { suffix: 'wohnung-mieten', kind: 'apartmentrent' },
- { suffix: 'wohnung-kaufen', kind: 'apartmentbuy' },
- { suffix: 'haus-mieten', kind: 'houserent' },
- { suffix: 'haus-kaufen', kind: 'housebuy' },
+ { suffix: 'wohnung-mieten', kind: 'apartmentrent' },
+ { suffix: 'wohnung-kaufen', kind: 'apartmentbuy' },
+ { suffix: 'haus-mieten', kind: 'houserent' },
+ { suffix: 'haus-kaufen', kind: 'housebuy' },
{ suffix: 'wohnen-auf-zeit', kind: 'shorttermaccommodation' },
];
@@ -56,15 +63,29 @@ function findRoute(suffix) {
// ββ Parameter Processing ββββββββββββββββββββββββββββββββββββββββββββββββββββ
const PASSTHROUGH_KEYS = new Set([
- 'price', 'pricetype', 'numberofrooms', 'livingspace',
- 'geocoordinates', 'geocodes', 'sorting', 'fulltext',
- 'apartmenttypes', 'floor', 'newbuilding', 'equipment',
- 'petsallowedtypes', 'constructionyear', 'energyefficiencyclasses',
- 'exclusioncriteria', 'heatingtypes', 'haspromotion', 'startrentaldate',
+ 'price',
+ 'pricetype',
+ 'numberofrooms',
+ 'livingspace',
+ 'geocoordinates',
+ 'geocodes',
+ 'sorting',
+ 'fulltext',
+ 'apartmenttypes',
+ 'floor',
+ 'newbuilding',
+ 'equipment',
+ 'petsallowedtypes',
+ 'constructionyear',
+ 'energyefficiencyclasses',
+ 'exclusioncriteria',
+ 'heatingtypes',
+ 'haspromotion',
+ 'startrentaldate',
]);
// Sort codes: Web UI uses numeric IDs, the API uses named identifiers
-const SORT_LABELS = { '1': 'standard', '2': '-firstactivation' };
+const SORT_LABELS = { 1: 'standard', 2: '-firstactivation' };
function resolveSortCode(code) {
return SORT_LABELS[code] ?? code;
@@ -74,15 +95,15 @@ function resolveSortCode(code) {
/**
* Extracts geo information from the path segments of an IS24 URL.
- * @param {string[]} segments
+ * @param {string[]} segments
* @returns {{ isRadius: boolean, path: string, segments: string[] }}
*/
function extractGeoInfo(segments) {
- const cleaned = segments.filter((s) => s.toLowerCase() !== 'suche');
+ const cleaned = segments.filter((s) => s.toLowerCase() !== 'suche');
const isRadius = cleaned.includes('radius');
return {
isRadius,
- path: '/' + cleaned.join('/'),
+ path: '/' + cleaned.join('/'),
segments: cleaned,
};
}
@@ -122,13 +143,13 @@ function parseSearchFromUrl(webUrl) {
if (!route) {
throw new Is24ProviderError(
'Spezielle Filter werden nicht unterstΓΌtzt. ' +
- 'Bitte verwende eine Standard-Suche (Wohnung/Haus mieten oder kaufen, Wohnen auf Zeit).',
+ 'Bitte verwende eine Standard-Suche (Wohnung/Haus mieten oder kaufen, Wohnen auf Zeit).',
);
}
return {
- type: route.kind,
- geo: extractGeoInfo(parts.slice(0, -1)),
+ type: route.kind,
+ geo: extractGeoInfo(parts.slice(0, -1)),
params: extractParams(parsed.searchParams),
};
}
@@ -141,7 +162,7 @@ function parseSearchFromUrl(webUrl) {
*/
function buildApiEndpoint(search) {
const fields = {
- searchType: search.geo.isRadius ? 'radius' : 'region',
+ searchType: search.geo.isRadius ? 'radius' : 'region',
realestatetype: search.type,
};
@@ -188,7 +209,7 @@ export function validateUrl(url) {
const REQUEST_HEADERS = {
'User-Agent': 'ImmoScout_28.3_34.0_._',
- 'Accept': 'application/json',
+ Accept: 'application/json',
};
async function requestPage(baseEndpoint, pageIdx, log = console.log) {
@@ -196,48 +217,49 @@ async function requestPage(baseEndpoint, pageIdx, log = console.log) {
log(`[immoscout24] Seite ${pageIdx}: ${endpoint}`);
const response = await fetch(endpoint, {
- method: 'POST',
+ method: 'POST',
headers: REQUEST_HEADERS,
});
if (!response.ok) {
- const userMessage = response.status >= 400 && response.status < 500
- ? `Diese Such-URL wird nicht unterstΓΌtzt. Bitte verwende eine Standard-Suche ohne Kartenausschnitt oder spezielle Filter.`
- : `API nicht erreichbar (HTTP ${response.status}). Bitte spΓ€ter erneut versuchen.`;
+ const userMessage =
+ response.status >= 400 && response.status < 500
+ ? `Diese Such-URL wird nicht unterstΓΌtzt. Bitte verwende eine Standard-Suche ohne Kartenausschnitt oder spezielle Filter.`
+ : `API nicht erreichbar (HTTP ${response.status}). Bitte spΓ€ter erneut versuchen.`;
throw new Is24ProviderError(userMessage, { page: pageIdx, status: response.status });
}
return response.json();
}
-function transformResultItem(raw) {
+export function transformResultItem(raw) {
if (!raw?.id) return null;
- const attrs = raw.attributes ?? [];
+ const attrs = raw.attributes ?? [];
const attrValues = attrs.map((a) => String(a.value ?? ''));
- const link = `https://www.immobilienscout24.de/expose/${raw.id}`;
+ const link = `https://www.immobilienscout24.de/expose/${raw.id}`;
return {
- id: buildHash('immoscout24', String(raw.id)),
- title: raw.title ?? '',
- price: pickByPattern(attrValues, 'price'),
- size: pickByPattern(attrValues, 'size'),
- rooms: pickByPattern(attrValues, 'rooms'),
+ id: buildHash('immoscout24', String(raw.id)),
+ title: raw.title ?? '',
+ price: pickByPattern(attrValues, 'price'),
+ size: pickByPattern(attrValues, 'size'),
+ rooms: pickByPattern(attrValues, 'rooms'),
availableFrom: pickByPattern(attrValues, 'date'),
- address: raw.address?.line ?? null,
+ address: raw.address?.line ?? null,
description: null,
- publisher: raw.isPrivate ? 'Privat' : 'Makler',
+ publisher: raw.isPrivate ? 'Privat' : 'Makler',
link,
- image: raw.titlePicture?.full ?? raw.titlePicture?.preview ?? null,
- listedAt: parsePublishedDate(raw.published),
- lat: raw.address?.lat ?? null,
- lon: raw.address?.lon ?? null,
+ image: raw.titlePicture?.full ?? raw.titlePicture?.preview ?? null,
+ listedAt: parsePublishedDate(raw.published),
+ lat: raw.address?.lat ?? null,
+ lon: raw.address?.lon ?? null,
};
}
export function logRawVsParsed(raw, parsed, log = console.log) {
- const attrs = raw.attributes ?? [];
- const sep = 'β'.repeat(60);
+ const attrs = raw.attributes ?? [];
+ const sep = 'β'.repeat(60);
const hasWarn = attrs.some((a) => {
const v = String(a.value ?? '');
return !Object.values(LISTING_PATTERNS).some((re) => re.test(v));
@@ -245,13 +267,15 @@ export function logRawVsParsed(raw, parsed, log = console.log) {
log(`[immoscout24] β ${sep}`);
log(`[immoscout24] β ${raw.title || '(kein Titel)'}`);
- log(`[immoscout24] β Expose-ID : ${raw.id} β https://www.immobilienscout24.de/expose/${raw.id}`);
+ log(
+ `[immoscout24] β Expose-ID : ${raw.id} β https://www.immobilienscout24.de/expose/${raw.id}`,
+ );
log(`[immoscout24] β Anbieter : ${raw.isPrivate ? 'Privat' : 'Makler'}`);
log(`[immoscout24] β Adresse : ${raw.address?.line ?? 'β'}`);
log(`[immoscout24] β VerΓΆff. : ${raw.published ?? 'β'}`);
log(`[immoscout24] β ββ Rohe Attribute β Erkennung ββββββββββββββββββββββββββ`);
for (let i = 0; i < attrs.length; i++) {
- const val = String(attrs[i]?.value ?? '');
+ const val = String(attrs[i]?.value ?? '');
const matched = Object.entries(LISTING_PATTERNS)
.filter(([, re]) => re.test(val))
.map(([k]) => k);
@@ -260,7 +284,7 @@ export function logRawVsParsed(raw, parsed, log = console.log) {
}
log(`[immoscout24] β ββ Parsed βββββββββββββββββββββββββββββββββββββββββββββββ`);
log(`[immoscout24] β Preis : ${parsed.price ?? 'β'}`);
- log(`[immoscout24] β GrΓΆΓe : ${parsed.size ?? 'β'}`);
+ log(`[immoscout24] β GrΓΆΓe : ${parsed.size ?? 'β'}`);
log(`[immoscout24] β Zimmer : ${parsed.rooms ?? (hasWarn ? 'β (β fehlend)' : 'β')}`);
log(`[immoscout24] β Einzug : ${parsed.availableFrom ?? 'β'}`);
log(`[immoscout24] β ${sep}`);
@@ -272,11 +296,13 @@ function collectListingsFromResponse(payload) {
.filter((entry) => entry.type === 'EXPOSE_RESULT')
.map((entry) => entry.item)
.filter(Boolean);
- const listings = rawItems.map((raw) => {
- const parsed = transformResultItem(raw);
- if (parsed && LOG_RAW_VS_PARSED) logRawVsParsed(raw, parsed);
- return parsed;
- }).filter(Boolean);
+ const listings = rawItems
+ .map((raw) => {
+ const parsed = transformResultItem(raw);
+ if (parsed && LOG_RAW_VS_PARSED) logRawVsParsed(raw, parsed);
+ return parsed;
+ })
+ .filter(Boolean);
return { listings, rawItems };
}
@@ -305,11 +331,7 @@ export async function scrape(inputUrl, maxPages = 10, opts = {}) {
* @returns {Promise<{ mobileUrl: string, hitCount: number|string, pageCount: number, targetPages: number, pages: Array<{ pageNum: number, listings: object[] }> }>}
*/
export async function scrapePages(inputUrl, maxPages = 10, opts = {}) {
- const {
- signal,
- onProgress,
- log = console.log,
- } = opts;
+ const { signal, onProgress, log = console.log } = opts;
// Detect if already an API URL
const isApiEndpoint = inputUrl.includes('api.mobile.immobilienscout24.de');
@@ -330,9 +352,9 @@ export async function scrapePages(inputUrl, maxPages = 10, opts = {}) {
}
// Fetch first page (contains paging metadata)
- const firstPage = await requestPage(apiBase, 1, log);
- const pageCount = firstPage.numberOfPages ?? 1;
- const hitCount = firstPage.totalResults ?? '?';
+ const firstPage = await requestPage(apiBase, 1, log);
+ const pageCount = firstPage.numberOfPages ?? 1;
+ const hitCount = firstPage.totalResults ?? '?';
const targetPages = Math.min(maxPages, pageCount);
const pages = [];
@@ -367,7 +389,7 @@ export async function scrapePages(inputUrl, maxPages = 10, opts = {}) {
// ββ Provider Exports ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
-export const id = 'immoscout24';
+export const id = 'immoscout24';
export const name = 'ImmobilienScout24';
export const listingTypes = PROPERTY_KINDS.map(({ key, label }) => ({ id: key, label }));
@@ -379,19 +401,23 @@ export function inferListingTypeFromUrl(url) {
// Web URL: resolve suffix via routing table
try {
- const parsed = new URL(url);
- const parts = parsed.pathname.split('/').filter(Boolean);
- const tail = parts[parts.length - 1];
- const route = findRoute(tail);
+ const parsed = new URL(url);
+ const parts = parsed.pathname.split('/').filter(Boolean);
+ const tail = parts[parts.length - 1];
+ const route = findRoute(tail);
if (route) return route.kind;
- } catch { /* ignore */ }
+ } catch {
+ /* ignore */
+ }
// Mobile API URL: read realestatetype parameter
try {
const parsed = new URL(url);
const rt = parsed.searchParams.get('realestatetype');
if (rt && KNOWN_PROPERTY_KEYS.has(rt)) return rt;
- } catch { /* ignore */ }
+ } catch {
+ /* ignore */
+ }
return 'apartmentrent';
}
diff --git a/src/providers/kleinanzeigen/index.js b/src/providers/kleinanzeigen/index.js
index 5865f25..2da903a 100644
--- a/src/providers/kleinanzeigen/index.js
+++ b/src/providers/kleinanzeigen/index.js
@@ -43,21 +43,19 @@ function buildPageUrl(baseUrl, pageNum) {
/** Replaces thumbnail path and low resolution with a larger image */
function upscaleImageUrl(url) {
- return (url || '')
- .replace('/thumbs/images/', '/images/')
- .replace(/s-l\d+\./, 's-l640.');
+ return (url || '').replace('/thumbs/images/', '/images/').replace(/s-l\d+\./, 's-l640.');
}
function parseListing(raw) {
- const link = `https://www.kleinanzeigen.de${raw.link}`;
+ const link = `https://www.kleinanzeigen.de${raw.link}`;
const tagParts = (raw.tags ?? '').split('Β·').map((s) => s.trim());
return {
...raw,
- id: buildHash(raw.id, link),
+ id: buildHash(raw.id, link),
link,
- size: pickByPattern(tagParts, 'size'),
- rooms: pickByPattern(tagParts, 'rooms'),
- image: upscaleImageUrl(raw.image),
+ size: pickByPattern(tagParts, 'size'),
+ rooms: pickByPattern(tagParts, 'rooms'),
+ image: upscaleImageUrl(raw.image),
listedAt: parsePublishedDate(raw.listedAt),
};
}
@@ -65,16 +63,16 @@ function parseListing(raw) {
// ββ Field Extraction βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const FIELD_EXTRACTORS = {
- id: { attr: 'data-adid', scope: '.aditem' },
- price: { text: '.aditem-main--middle--price-shipping--price' },
- tags: { text: '.aditem-main--middle--tags' },
- title: { text: '.aditem-main .text-module-begin a' },
- link: { attr: 'href', scope: '.aditem-main .text-module-begin a' },
+ id: { attr: 'data-adid', scope: '.aditem' },
+ price: { text: '.aditem-main--middle--price-shipping--price' },
+ tags: { text: '.aditem-main--middle--tags' },
+ title: { text: '.aditem-main .text-module-begin a' },
+ link: { attr: 'href', scope: '.aditem-main .text-module-begin a' },
description: { text: '.aditem-main .aditem-main--middle--description' },
- address: { text: '.aditem-main--top--left' },
- listedAt: { text: '.aditem-main--top--right' },
- image: { attr: 'src', scope: 'img' },
- publisher: { text: '.aditem-main--bottom' },
+ address: { text: '.aditem-main--top--left' },
+ listedAt: { text: '.aditem-main--top--right' },
+ image: { attr: 'src', scope: 'img' },
+ publisher: { text: '.aditem-main--bottom' },
};
// ββ Crawl Config Builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
diff --git a/src/providers/registry.js b/src/providers/registry.js
index e2eb106..7d57e4a 100644
--- a/src/providers/registry.js
+++ b/src/providers/registry.js
@@ -22,7 +22,6 @@ function register(provider) {
register(kleinanzeigen);
register(immoscout24);
-
/**
* Returns a provider by its ID.
*/
diff --git a/src/routes/configs.js b/src/routes/configs.js
index ac2ea39..cc26a34 100644
--- a/src/routes/configs.js
+++ b/src/routes/configs.js
@@ -18,100 +18,66 @@ const router = Router();
// ββ Providers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// GET /api/providers β list all available providers with listing types
-router.get('/providers', asyncHandler(async (_req, res) => {
- res.json(getAllProviders());
-}));
+router.get(
+ '/providers',
+ asyncHandler(async (_req, res) => {
+ res.json(getAllProviders());
+ }),
+);
// POST /api/configs/infer-url β infers provider and listing type from a URL
-router.post('/infer-url', asyncHandler(async (req, res) => {
- const { url } = req.body;
+router.post(
+ '/infer-url',
+ asyncHandler(async (req, res) => {
+ const { url } = req.body;
if (!url) return res.status(400).json({ error: 'URL fehlt' });
- const result = inferFromUrl(url);
- if (!result) return res.json({ detected: false });
- const provider = getProvider(result.providerId);
- const typeInfo = provider?.listingTypes?.find(t => t.id === result.listingTypeId);
- return res.json({
- detected: true,
- providerId: result.providerId,
- listingTypeId: result.listingTypeId,
- listingTypeLabel: typeInfo?.label || result.listingTypeId,
- });
-}));
+ const result = inferFromUrl(url);
+ if (!result) return res.json({ detected: false });
+ const provider = getProvider(result.providerId);
+ const typeInfo = provider?.listingTypes?.find((t) => t.id === result.listingTypeId);
+ return res.json({
+ detected: true,
+ providerId: result.providerId,
+ listingTypeId: result.listingTypeId,
+ listingTypeLabel: typeInfo?.label || result.listingTypeId,
+ });
+ }),
+);
// ββ Search Configs βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// GET /api/configs β all search configurations
-router.get('/', asyncHandler(async (_req, res) => {
- const configs = getAllSearchConfigs();
+router.get(
+ '/',
+ asyncHandler(async (_req, res) => {
+ const configs = getAllSearchConfigs();
// Compute the current scrape URL for each config
- const enriched = configs.map(cfg => {
- let scrapeUrl = '';
- try {
- const extraParams = JSON.parse(cfg.extra_params || '{}');
- scrapeUrl = extraParams.directUrl || '';
- } catch {}
- return { ...cfg, scrape_url: scrapeUrl };
- });
- res.json(enriched);
-}));
+ const enriched = configs.map((cfg) => {
+ let scrapeUrl = '';
+ try {
+ const extraParams = JSON.parse(cfg.extra_params || '{}');
+ scrapeUrl = extraParams.directUrl || '';
+ } catch {}
+ return { ...cfg, scrape_url: scrapeUrl };
+ });
+ res.json(enriched);
+ }),
+);
// POST /api/configs β create a new search configuration
-router.post('/', asyncHandler(async (req, res) => {
- const directUrl = req.body.directUrl || '';
- const name = req.body.name?.trim() || '';
-
- if (!name) {
- return res.status(400).json({ error: 'Name ist erforderlich.' });
- }
- if (!directUrl) {
- return res.status(400).json({ error: 'Scrape-URL ist erforderlich.' });
- }
-
- // Validate IS24 URL against supported search categories
- if (directUrl.includes('immobilienscout24.de')) {
- try {
- validateIs24Url(directUrl);
- } catch (e) {
- return res.status(400).json({ error: e.message });
+router.post(
+ '/',
+ asyncHandler(async (req, res) => {
+ const directUrl = req.body.directUrl || '';
+ const name = req.body.name?.trim() || '';
+
+ if (!name) {
+ return res.status(400).json({ error: 'Name ist erforderlich.' });
}
- }
-
- const extraParams = { directUrl };
- console.log(`[config] New config: ${name}`);
-
- // Auto-detect listing type & provider from URL
- let providerId = req.body.provider || 'kleinanzeigen';
- let listingType = req.body.listingType ?? null;
- if (!listingType) {
- const inferred = inferFromUrl(directUrl);
- if (inferred) {
- listingType = inferred.listingTypeId;
- providerId = inferred.providerId;
- console.log(`[config] Inferred from URL: provider=${providerId}, type=${listingType}`);
- } else {
- listingType = 'miete'; // safe fallback
+ if (!directUrl) {
+ return res.status(400).json({ error: 'Scrape-URL ist erforderlich.' });
}
- }
- const config = createSearchConfig({
- provider: providerId,
- listingType,
- maxPages: Number(req.body.maxPages) || 10,
- extraParams,
- name,
- });
-
- res.status(201).json({ ...config, scrape_url: directUrl });
-}));
-
-// PATCH /api/configs/:id β update a search configuration
-router.patch('/:id', asyncHandler(async (req, res) => {
- const id = Number(req.params.id);
- const data = {};
-
- if (req.body.directUrl !== undefined) {
- const directUrl = req.body.directUrl;
- data.extraParams = { directUrl };
// Validate IS24 URL against supported search categories
if (directUrl.includes('immobilienscout24.de')) {
try {
@@ -121,36 +87,88 @@ router.patch('/:id', asyncHandler(async (req, res) => {
}
}
- // Auto-infer listing type from new URL if not explicitly supplied
- if (req.body.listing_type === undefined && req.body.listingType === undefined) {
+ const extraParams = { directUrl };
+ console.log(`[config] New config: ${name}`);
+
+ // Auto-detect listing type & provider from URL
+ let providerId = req.body.provider || 'kleinanzeigen';
+ let listingType = req.body.listingType ?? null;
+ if (!listingType) {
const inferred = inferFromUrl(directUrl);
if (inferred) {
- data.listingType = inferred.listingTypeId;
- data.provider = inferred.providerId;
+ listingType = inferred.listingTypeId;
+ providerId = inferred.providerId;
+ console.log(`[config] Inferred from URL: provider=${providerId}, type=${listingType}`);
+ } else {
+ listingType = 'miete'; // safe fallback
}
}
- }
- if (req.body.maxPages !== undefined) data.maxPages = Number(req.body.maxPages);
- if (req.body.enabled !== undefined) data.enabled = req.body.enabled ? 1 : 0;
- if (req.body.name !== undefined) data.name = req.body.name;
- if (req.body.listingType !== undefined) data.listingType = req.body.listingType;
- if (req.body.provider !== undefined) data.provider = req.body.provider;
- const updated = updateSearchConfig(id, data);
+ const config = createSearchConfig({
+ provider: providerId,
+ listingType,
+ maxPages: Number(req.body.maxPages) || 10,
+ extraParams,
+ name,
+ });
+
+ res.status(201).json({ ...config, scrape_url: directUrl });
+ }),
+);
- let scrapeUrl = '';
- try {
- const extraParams = JSON.parse(updated.extra_params || '{}');
- scrapeUrl = extraParams.directUrl || '';
- } catch {}
+// PATCH /api/configs/:id β update a search configuration
+router.patch(
+ '/:id',
+ asyncHandler(async (req, res) => {
+ const id = Number(req.params.id);
+ const data = {};
+
+ if (req.body.directUrl !== undefined) {
+ const directUrl = req.body.directUrl;
+ data.extraParams = { directUrl };
+ // Validate IS24 URL against supported search categories
+ if (directUrl.includes('immobilienscout24.de')) {
+ try {
+ validateIs24Url(directUrl);
+ } catch (e) {
+ return res.status(400).json({ error: e.message });
+ }
+ }
+
+ // Auto-infer listing type from new URL if not explicitly supplied
+ if (req.body.listing_type === undefined && req.body.listingType === undefined) {
+ const inferred = inferFromUrl(directUrl);
+ if (inferred) {
+ data.listingType = inferred.listingTypeId;
+ data.provider = inferred.providerId;
+ }
+ }
+ }
+ if (req.body.maxPages !== undefined) data.maxPages = Number(req.body.maxPages);
+ if (req.body.enabled !== undefined) data.enabled = req.body.enabled ? 1 : 0;
+ if (req.body.name !== undefined) data.name = req.body.name;
+ if (req.body.listingType !== undefined) data.listingType = req.body.listingType;
+ if (req.body.provider !== undefined) data.provider = req.body.provider;
+
+ const updated = updateSearchConfig(id, data);
+
+ let scrapeUrl = '';
+ try {
+ const extraParams = JSON.parse(updated.extra_params || '{}');
+ scrapeUrl = extraParams.directUrl || '';
+ } catch {}
- res.json({ ...updated, scrape_url: scrapeUrl });
-}));
+ res.json({ ...updated, scrape_url: scrapeUrl });
+ }),
+);
// DELETE /api/configs/:id β delete a search configuration
-router.delete('/:id', asyncHandler(async (req, res) => {
- deleteSearchConfig(Number(req.params.id));
- res.json({ ok: true });
-}));
+router.delete(
+ '/:id',
+ asyncHandler(async (req, res) => {
+ deleteSearchConfig(Number(req.params.id));
+ res.json({ ok: true });
+ }),
+);
export default router;
diff --git a/src/routes/listings.js b/src/routes/listings.js
index 51ef0c1..2d7d101 100644
--- a/src/routes/listings.js
+++ b/src/routes/listings.js
@@ -32,8 +32,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_PATH = path.join(__dirname, '..', '..', 'config', 'default.json');
function readConfig() {
- try { return JSON.parse(readFileSync(CONFIG_PATH, 'utf8')); }
- catch { return { blacklistKeywords: [] }; }
+ try {
+ return JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
+ } catch {
+ return { blacklistKeywords: [] };
+ }
}
function countStats(rows) {
@@ -47,135 +50,176 @@ function countStats(rows) {
const router = Router();
// DELETE /api/listings/clear-favorites β resets all favorites
-router.delete('/clear-favorites', asyncHandler(async (_req, res) => {
- clearAllFavorites();
- res.json({ ok: true });
-}));
+router.delete(
+ '/clear-favorites',
+ asyncHandler(async (_req, res) => {
+ clearAllFavorites();
+ res.json({ ok: true });
+ }),
+);
// DELETE /api/listings/clear-favorites/:configId β resets favorites for a single agent
-router.delete('/clear-favorites/:configId', asyncHandler(async (req, res) => {
- clearFavoritesByConfig(Number(req.params.configId));
- res.json({ ok: true });
-}));
+router.delete(
+ '/clear-favorites/:configId',
+ asyncHandler(async (req, res) => {
+ clearFavoritesByConfig(Number(req.params.configId));
+ res.json({ ok: true });
+ }),
+);
// DELETE /api/listings/clear-blacklist β clears the entire blacklist
-router.delete('/clear-blacklist', asyncHandler(async (_req, res) => {
- clearAllBlacklist();
- res.json({ ok: true });
-}));
+router.delete(
+ '/clear-blacklist',
+ asyncHandler(async (_req, res) => {
+ clearAllBlacklist();
+ res.json({ ok: true });
+ }),
+);
// DELETE /api/listings/clear-blacklist/:configId β clears the blacklist for a single agent
-router.delete('/clear-blacklist/:configId', asyncHandler(async (req, res) => {
- clearBlacklistByConfig(Number(req.params.configId));
- res.json({ ok: true });
-}));
+router.delete(
+ '/clear-blacklist/:configId',
+ asyncHandler(async (req, res) => {
+ clearBlacklistByConfig(Number(req.params.configId));
+ res.json({ ok: true });
+ }),
+);
// DELETE /api/listings/reset β deletes all listings (favorites & blacklist are retained)
-router.delete('/reset', asyncHandler(async (_req, res) => {
- purgeListingsKeepPinned();
- res.json({ ok: true });
-}));
+router.delete(
+ '/reset',
+ asyncHandler(async (_req, res) => {
+ purgeListingsKeepPinned();
+ res.json({ ok: true });
+ }),
+);
// DELETE /api/listings/reset/:configId β deletes listings for a single search agent
-router.delete('/reset/:configId', asyncHandler(async (req, res) => {
- purgeListingsByConfig(Number(req.params.configId));
- res.json({ ok: true });
-}));
+router.delete(
+ '/reset/:configId',
+ asyncHandler(async (req, res) => {
+ purgeListingsByConfig(Number(req.params.configId));
+ res.json({ ok: true });
+ }),
+);
// GET /api/listings/runs
-router.get('/runs', asyncHandler(async (_req, res) => {
- res.json(getRecentRuns(50));
-}));
+router.get(
+ '/runs',
+ asyncHandler(async (_req, res) => {
+ res.json(getRecentRuns(50));
+ }),
+);
// PATCH /api/listings/seen-all
-router.patch('/seen-all', asyncHandler(async (_req, res) => {
- markAllSeen();
- res.json({ ok: true });
-}));
+router.patch(
+ '/seen-all',
+ asyncHandler(async (_req, res) => {
+ markAllSeen();
+ res.json({ ok: true });
+ }),
+);
// PATCH /api/listings/:id/unseen
-router.patch('/:id/unseen', asyncHandler(async (req, res) => {
- markUnseen(req.params.id);
- res.json({ ok: true });
-}));
+router.patch(
+ '/:id/unseen',
+ asyncHandler(async (req, res) => {
+ markUnseen(req.params.id);
+ res.json({ ok: true });
+ }),
+);
// GET /api/listings/stats
-router.get('/stats', asyncHandler(async (_req, res) => {
- const cfg = readConfig();
- const blacklistKeywords = cfg.blacklistKeywords ?? [];
-
- const visibleRows = getListings({
- hideBlacklisted: true,
- showBlacklisted: false,
- blacklistKeywords,
- });
- const blacklistedRows = getListings({
- hideBlacklisted: false,
- showBlacklisted: true,
- blacklistKeywords,
- });
-
- res.json({
- ...countStats(visibleRows),
- blacklisted: blacklistedRows.length,
- });
-}));
-
-// GET /api/listings/stats/per-config
-router.get('/stats/per-config', asyncHandler(async (_req, res) => {
- const cfg = readConfig();
- const blacklistKeywords = cfg.blacklistKeywords ?? [];
+router.get(
+ '/stats',
+ asyncHandler(async (_req, res) => {
+ const cfg = readConfig();
+ const blacklistKeywords = cfg.blacklistKeywords ?? [];
- const perConfig = getAllSearchConfigs().map((searchConfig) => {
const visibleRows = getListings({
- searchConfigId: searchConfig.id,
hideBlacklisted: true,
showBlacklisted: false,
blacklistKeywords,
});
const blacklistedRows = getListings({
- searchConfigId: searchConfig.id,
hideBlacklisted: false,
showBlacklisted: true,
blacklistKeywords,
});
- const visibleStats = countStats(visibleRows);
- return {
- search_config_id: searchConfig.id,
- total: visibleStats.total,
- unseen: visibleStats.unseen,
- favorites: visibleStats.favorites,
+ res.json({
+ ...countStats(visibleRows),
blacklisted: blacklistedRows.length,
- };
- });
- const orphans = getOrphanStats();
- res.json({ perConfig, orphans });
-}));
+ });
+ }),
+);
+
+// GET /api/listings/stats/per-config
+router.get(
+ '/stats/per-config',
+ asyncHandler(async (_req, res) => {
+ const cfg = readConfig();
+ const blacklistKeywords = cfg.blacklistKeywords ?? [];
+
+ const perConfig = getAllSearchConfigs().map((searchConfig) => {
+ const visibleRows = getListings({
+ searchConfigId: searchConfig.id,
+ hideBlacklisted: true,
+ showBlacklisted: false,
+ blacklistKeywords,
+ });
+ const blacklistedRows = getListings({
+ searchConfigId: searchConfig.id,
+ hideBlacklisted: false,
+ showBlacklisted: true,
+ blacklistKeywords,
+ });
+ const visibleStats = countStats(visibleRows);
+
+ return {
+ search_config_id: searchConfig.id,
+ total: visibleStats.total,
+ unseen: visibleStats.unseen,
+ favorites: visibleStats.favorites,
+ blacklisted: blacklistedRows.length,
+ };
+ });
+ const orphans = getOrphanStats();
+ res.json({ perConfig, orphans });
+ }),
+);
// GET /api/listings
-router.get('/', asyncHandler(async (req, res) => {
- const onlyUnseen = req.query.unseen === 'true';
- const onlyFavorites = req.query.favorites === 'true';
- const listingType = req.query.type || null;
- const provider = req.query.provider || null;
- const searchConfigId = req.query.search_config_id ? Number(req.query.search_config_id) : null;
- const showBlacklisted = req.query.blacklisted === 'true';
- const includeBlacklisted = req.query.include_blacklisted === 'true';
- const hideBlacklisted = !showBlacklisted;
-
- const cfg = readConfig();
- const blacklistKeywords = cfg.blacklistKeywords ?? [];
-
- const listings = getListings({
- onlyUnseen, onlyFavorites,
- listingType, provider, searchConfigId,
- hideBlacklisted, showBlacklisted, includeBlacklisted,
- blacklistKeywords,
- });
-
- res.json(listings);
-}));
+router.get(
+ '/',
+ asyncHandler(async (req, res) => {
+ const onlyUnseen = req.query.unseen === 'true';
+ const onlyFavorites = req.query.favorites === 'true';
+ const listingType = req.query.type || null;
+ const provider = req.query.provider || null;
+ const searchConfigId = req.query.search_config_id ? Number(req.query.search_config_id) : null;
+ const showBlacklisted = req.query.blacklisted === 'true';
+ const includeBlacklisted = req.query.include_blacklisted === 'true';
+ const hideBlacklisted = !showBlacklisted;
+
+ const cfg = readConfig();
+ const blacklistKeywords = cfg.blacklistKeywords ?? [];
+
+ const listings = getListings({
+ onlyUnseen,
+ onlyFavorites,
+ listingType,
+ provider,
+ searchConfigId,
+ hideBlacklisted,
+ showBlacklisted,
+ includeBlacklisted,
+ blacklistKeywords,
+ });
+
+ res.json(listings);
+ }),
+);
// ββ Image Fetching ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@@ -190,9 +234,10 @@ async function fetchImagesForListing(listing) {
try {
const response = await fetch(listing.link, {
headers: {
- 'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
+ 'User-Agent':
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36',
'Accept-Language': 'de-DE,de;q=0.9',
- 'Accept': 'text/html',
+ Accept: 'text/html',
},
signal: AbortSignal.timeout(8000),
});
@@ -203,25 +248,31 @@ async function fetchImagesForListing(listing) {
const dataImgSrc = [...html.matchAll(/data-imgsrc="([^"]+\/images\/.+?)"/g)];
for (const m of dataImgSrc) urls.add(m[1]);
- const galleryImgs = [...html.matchAll(/class="galleryimage-element[^"]*"[^>]*>[\s\S]*?
]+src="([^"]+)"/g)];
+ const galleryImgs = [
+ ...html.matchAll(/class="galleryimage-element[^"]*"[^>]*>[\s\S]*?
]+src="([^"]+)"/g),
+ ];
for (const m of galleryImgs) urls.add(m[1]);
- const jsonLdMatch = html.match(/