From 23583f54c6bf1c079a87e42328b0d66a423fa878 Mon Sep 17 00:00:00 2001 From: Giancarmine Salucci Date: Sun, 30 Nov 2025 09:06:44 +0100 Subject: [PATCH] full tour --- Dockerfile | 17 +- docker-compose.yml | 26 +- docker-compose_ori.yml | 34 + package-lock.json | 902 ++++++++++++++++++++++- package.json | 13 +- playwright-service/Dockerfile | 6 - playwright-service/server.js | 10 - scripts/gen-auth.js | 2 +- secrets/auth.json | 93 +++ src/app.server.ts | 27 + src/lib/server/browser.ts | 52 ++ src/lib/server/tandoor-config.ts | 12 + src/lib/server/tandoor.ts | 330 +++++++++ src/routes/api/extract/+server.ts | 125 +++- src/routes/api/tandoor-config/+server.ts | 5 + src/routes/api/tandoor/+server.ts | 32 + src/routes/share/+page.svelte | 77 ++ vite.config.ts | 5 + 18 files changed, 1679 insertions(+), 89 deletions(-) create mode 100644 docker-compose_ori.yml delete mode 100644 playwright-service/Dockerfile delete mode 100644 playwright-service/server.js create mode 100644 secrets/auth.json create mode 100644 src/app.server.ts create mode 100644 src/lib/server/browser.ts create mode 100644 src/lib/server/tandoor-config.ts create mode 100644 src/lib/server/tandoor.ts create mode 100644 src/routes/api/tandoor-config/+server.ts create mode 100644 src/routes/api/tandoor/+server.ts diff --git a/Dockerfile b/Dockerfile index d5e76c5..b9effe0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,20 @@ FROM node:22-alpine WORKDIR /app + +# Install Playwright system dependencies +RUN apk add --no-cache \ + chromium \ + font-liberation \ + liberation-fonts \ + noto \ + noto-cjk + COPY package*.json ./ RUN npm ci + COPY . . -EXPOSE 5173 -CMD ["npm", "run", "dev", "--", "--host"] \ No newline at end of file +RUN npm run build + +EXPOSE 3000 +ENV NODE_ENV=production +CMD ["node", "-e", "import('./build/index.js')"] \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 466de75..0bbe3d4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,34 +1,10 @@ services: app: build: . - ports: - - "5173:5173" - environment: - - PLAYWRIGHT_WS_ENDPOINT=ws://playwright-service:3000 - - OPENAI_BASE_URL=http://ollama:11434/v1 - - OPENAI_API_KEY=ollama - - LLM_MODEL=llama3.2 - volumes: - - ./src:/app/src - - ./secrets:/app/secrets:ro - depends_on: - - playwright-service - - ollama - - playwright-service: - build: ./playwright-service - ipc: host ports: ["3000:3000"] environment: - DISPLAY=:99 security_opt: - seccomp=unconfined - - ollama: - image: ollama/ollama:latest - ports: ["11434:11434"] volumes: - - ollama_data:/root/.ollama - -volumes: - ollama_data: \ No newline at end of file + - ./secrets:/app/secrets \ No newline at end of file diff --git a/docker-compose_ori.yml b/docker-compose_ori.yml new file mode 100644 index 0000000..466de75 --- /dev/null +++ b/docker-compose_ori.yml @@ -0,0 +1,34 @@ +services: + app: + build: . + ports: + - "5173:5173" + environment: + - PLAYWRIGHT_WS_ENDPOINT=ws://playwright-service:3000 + - OPENAI_BASE_URL=http://ollama:11434/v1 + - OPENAI_API_KEY=ollama + - LLM_MODEL=llama3.2 + volumes: + - ./src:/app/src + - ./secrets:/app/secrets:ro + depends_on: + - playwright-service + - ollama + + playwright-service: + build: ./playwright-service + ipc: host + ports: ["3000:3000"] + environment: + - DISPLAY=:99 + security_opt: + - seccomp=unconfined + + ollama: + image: ollama/ollama:latest + ports: ["11434:11434"] + volumes: + - ollama_data:/root/.ollama + +volumes: + ollama_data: \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7598206..042e9bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "openai": "^4.20.0", + "playwright": "^1.56.1", "zod": "^3.23.0" }, "devDependencies": { @@ -24,8 +25,8 @@ "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.13.0", + "fast-glob": "^3.3.3", "globals": "^16.5.0", - "playwright": "^1.56.1", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "prettier-plugin-tailwindcss": "^0.7.1", @@ -39,6 +40,80 @@ "vitest-browser-svelte": "^2.0.1" } }, + "node_modules/@acemir/cssom": { + "version": "0.9.24", + "resolved": "https://registry.npmjs.org/@acemir/cssom/-/cssom-0.9.24.tgz", + "integrity": "sha512-5YjgMmAiT2rjJZU7XK1SNI7iqTy92DpaYVgG6x63FxkJ11UpYfLndHJATtinWJClAXiOlW9XWaUyAQf8pMrQPg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/@asamuzakjp/css-color": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.1.0.tgz", + "integrity": "sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.4", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.4.tgz", + "integrity": "sha512-buQDjkm+wDPXd6c13534URWZqbz0RP5PAhXZ+LIoa5LgwInT9HVJvGIJivg75vi8I13CxDGdTnz+aY5YUJlIAA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1708,6 +1783,153 @@ "node": ">=6.9.0" } }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.20", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.20.tgz", + "integrity": "sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.12", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", @@ -2486,6 +2708,44 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.29", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", @@ -3882,6 +4142,18 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14" + } + }, "node_modules/agentkeepalive": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", @@ -4146,6 +4418,18 @@ "baseline-browser-mapping": "dist/cli.js" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -4157,6 +4441,19 @@ "concat-map": "0.0.1" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/browserslist": { "version": "4.28.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.0.tgz", @@ -4460,6 +4757,22 @@ "node": ">=8" } }, + "node_modules/css-tree": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mdn-data": "2.12.2", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4473,6 +4786,82 @@ "node": ">=4" } }, + "node_modules/cssstyle": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.3.tgz", + "integrity": "sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/data-view-buffer": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", @@ -4548,6 +4937,15 @@ } } }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4698,6 +5096,21 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/es-abstract": { "version": "1.24.0", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", @@ -5159,6 +5572,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "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", @@ -5191,6 +5634,16 @@ "license": "BSD-3-Clause", "peer": true }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -5258,6 +5711,19 @@ "node": ">=10" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -5387,7 +5853,6 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5732,6 +6197,53 @@ "node": ">= 0.4" } }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/humanize-ms": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", @@ -5741,6 +6253,21 @@ "ms": "^2.0.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -6051,6 +6578,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -6080,6 +6617,15 @@ "node": ">=0.10.0" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/is-reference": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", @@ -6351,6 +6897,91 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "27.2.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.2.0.tgz", + "integrity": "sha512-454TI39PeRDW1LgpyLPyURtB4Zx1tklSr6+OFOipsxGUH1WMTvk6C65JQdrj455+DP2uJ1+veBEHTGFKWVLFoA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@acemir/cssom": "^0.9.23", + "@asamuzakjp/dom-selector": "^6.7.4", + "cssstyle": "^5.3.3", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/jsdom/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -6847,6 +7478,52 @@ "node": ">= 0.4" } }, + "node_modules/mdn-data": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "dev": true, + "license": "CC0-1.0", + "optional": true, + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -7185,6 +7862,21 @@ "node": ">=6" } }, + "node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7285,7 +7977,6 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.57.0.tgz", "integrity": "sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==", - "dev": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.57.0" @@ -7304,7 +7995,6 @@ "version": "1.57.0", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.57.0.tgz", "integrity": "sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==", - "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -7611,6 +8301,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -7787,6 +8498,17 @@ "node": ">=4" } }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.53.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", @@ -7829,6 +8551,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -7922,6 +8668,30 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", @@ -8630,6 +9400,15 @@ "@types/estree": "^1.0.6" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/tailwindcss": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", @@ -8743,6 +9522,43 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.19.tgz", + "integrity": "sha512-8PWx8tvC4jDB39BQw1m4x8y5MH1BcQ5xHeL2n7UVFulMPH/3Q0uiamahFJ3lXA0zO2SUyRXuVVbWSDmstlt9YA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tldts-core": "^7.0.19" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.0.19", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.19.tgz", + "integrity": "sha512-lJX2dEWx0SGH4O6p+7FPwYmJ/bu1JbcGJ8RLaG9b7liIgZ85itUVEPbMtWRVrde/0fnDPEPHW10ZsKW3kVsE9A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -8753,6 +9569,21 @@ "node": ">=6" } }, + "node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "peer": true, + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -9313,6 +10144,21 @@ "vitest": "^4.0.0" } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/web-streams-polyfill": { "version": "4.0.0-beta.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", @@ -9328,6 +10174,33 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "license": "BSD-2-Clause" }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", @@ -10042,6 +10915,27 @@ } } }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", diff --git a/package.json b/package.json index 9d6384f..28a1306 100644 --- a/package.json +++ b/package.json @@ -23,12 +23,13 @@ "@sveltejs/vite-plugin-svelte": "^6.2.1", "@tailwindcss/vite": "^4.1.17", "@types/node": "^22", + "@vite-pwa/sveltekit": "^0.3.0", "@vitest/browser-playwright": "^4.0.10", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.13.0", + "fast-glob": "^3.3.3", "globals": "^16.5.0", - "playwright": "^1.56.1", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "prettier-plugin-tailwindcss": "^0.7.1", @@ -39,11 +40,11 @@ "typescript-eslint": "^8.47.0", "vite": "^6.0.0", "vitest": "^4.0.10", - "vitest-browser-svelte": "^2.0.1", - "@vite-pwa/sveltekit": "^0.3.0" + "vitest-browser-svelte": "^2.0.1" }, "dependencies": { - "zod": "^3.23.0", - "openai": "^4.20.0" + "openai": "^4.20.0", + "playwright": "^1.56.1", + "zod": "^3.23.0" } -} \ No newline at end of file +} diff --git a/playwright-service/Dockerfile b/playwright-service/Dockerfile deleted file mode 100644 index 6d601f9..0000000 --- a/playwright-service/Dockerfile +++ /dev/null @@ -1,6 +0,0 @@ -FROM mcr.microsoft.com/playwright:v1.49.0-jammy -WORKDIR /app -RUN npm init -y && npm install playwright -COPY server.js . -EXPOSE 3000 -CMD ["node", "server.js"] \ No newline at end of file diff --git a/playwright-service/server.js b/playwright-service/server.js deleted file mode 100644 index 2663622..0000000 --- a/playwright-service/server.js +++ /dev/null @@ -1,10 +0,0 @@ -const { chromium } = require('playwright'); -(async () => { - const server = await chromium.launchServer({ - port: 3000, - headless: true, - args: ['--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage'] - }); - console.log('Browser Server running on port 3000...'); - await new Promise(() => {}); -})(); \ No newline at end of file diff --git a/scripts/gen-auth.js b/scripts/gen-auth.js index d6cd7b4..cf404d8 100644 --- a/scripts/gen-auth.js +++ b/scripts/gen-auth.js @@ -13,7 +13,7 @@ import path from 'path'; try { await page.waitForSelector('svg[aria-label="Home"]', { timeout: 120000 }); - const secretsDir = path.resolve('secrets'); + const secretsDir = path.resolve('../secrets'); if (!fs.existsSync(secretsDir)) fs.mkdirSync(secretsDir); await context.storageState({ path: path.join(secretsDir, 'auth.json') }); diff --git a/secrets/auth.json b/secrets/auth.json new file mode 100644 index 0000000..f6fe0ce --- /dev/null +++ b/secrets/auth.json @@ -0,0 +1,93 @@ +{ + "cookies": [ + { + "name": "csrftoken", + "value": "ykHk3KB03XrauXWLC-ptZt", + "domain": ".instagram.com", + "path": "/", + "expires": 1798994745.094861, + "httpOnly": false, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "datr", + "value": "IyMraZYVQ9HkYUYX3GxS_YQH", + "domain": ".instagram.com", + "path": "/", + "expires": 1798994725.55098, + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "ig_did", + "value": "C837AEE7-0829-4F5E-A1CB-26576A939240", + "domain": ".instagram.com", + "path": "/", + "expires": 1795970744.095018, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "mid", + "value": "aSsjIwALAAFWEdHviQtn-VWvZ8vX", + "domain": ".instagram.com", + "path": "/", + "expires": 1798994725.551027, + "httpOnly": true, + "secure": true, + "sameSite": "None" + }, + { + "name": "sessionid", + "value": "59661903731%3AXVkiiTq7Bfg03S%3A13%3AAYi2K9DS84etVK7mLwkdOxT_NCNWzuGM7pwyc-S2MQ", + "domain": ".instagram.com", + "path": "/", + "expires": 1795970744.094852, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + }, + { + "name": "ds_user_id", + "value": "59661903731", + "domain": ".instagram.com", + "path": "/", + "expires": 1772210745.094968, + "httpOnly": false, + "secure": true, + "sameSite": "None" + }, + { + "name": "rur", + "value": "\"CLN\\05459661903731\\0541795970747:01fe6e28c38cd9db21b75181598de0953055c6279b89492b332d16872ed81561f6513e4c\"", + "domain": ".instagram.com", + "path": "/", + "expires": -1, + "httpOnly": true, + "secure": true, + "sameSite": "Lax" + } + ], + "origins": [ + { + "origin": "https://www.instagram.com", + "localStorage": [ + { + "name": "Session", + "value": "vdz65y:1764434779842" + }, + { + "name": "chatd-deviceid", + "value": "13e8b058-6d14-418e-9b87-ccd98297098c" + }, + { + "name": "IGSession", + "value": "nrg2g0:1764436544843" + } + ] + } + ] +} \ No newline at end of file diff --git a/src/app.server.ts b/src/app.server.ts new file mode 100644 index 0000000..0b80bfa --- /dev/null +++ b/src/app.server.ts @@ -0,0 +1,27 @@ +import { initializeBrowser, closeBrowser } from '$lib/server/browser'; + +// Initialize browser when server starts +export async function init() { + try { + await initializeBrowser(); + } catch (error) { + console.error('Failed to initialize browser:', error); + process.exit(1); + } + + // Graceful shutdown + process.on('SIGTERM', async () => { + console.log('SIGTERM received, shutting down gracefully...'); + await closeBrowser(); + process.exit(0); + }); + + process.on('SIGINT', async () => { + console.log('SIGINT received, shutting down gracefully...'); + await closeBrowser(); + process.exit(0); + }); +} + +// Run initialization immediately +init().catch(console.error); diff --git a/src/lib/server/browser.ts b/src/lib/server/browser.ts new file mode 100644 index 0000000..bed2870 --- /dev/null +++ b/src/lib/server/browser.ts @@ -0,0 +1,52 @@ +import { chromium, type Browser, type BrowserContext } from 'playwright'; +import fs from 'fs'; + +let browser: Browser | null = null; + +export async function initializeBrowser(): Promise { + if (browser) { + return browser; + } + + console.log('Initializing Playwright browser...'); + browser = await chromium.launch({ + headless: true, + args: ['--disable-gpu', '--no-sandbox', '--disable-dev-shm-usage'] + }); + + console.log('Browser initialized successfully'); + return browser; +} + +export async function getBrowser(): Promise { + if (!browser) { + return initializeBrowser(); + } + return browser; +} + +export async function createBrowserContext( + authStoragePath?: string +): Promise { + const browserInstance = await getBrowser(); + + // Load auth if available + let context: BrowserContext; + if (authStoragePath && fs.existsSync(authStoragePath)) { + console.log('Loading authentication from:', authStoragePath); + context = await browserInstance.newContext({ storageState: authStoragePath }); + } else { + console.warn('No auth storage found. Running as guest.'); + context = await browserInstance.newContext(); + } + + return context; +} + +export async function closeBrowser(): Promise { + if (browser) { + console.log('Closing Playwright browser...'); + await browser.close(); + browser = null; + } +} diff --git a/src/lib/server/tandoor-config.ts b/src/lib/server/tandoor-config.ts new file mode 100644 index 0000000..ca748fa --- /dev/null +++ b/src/lib/server/tandoor-config.ts @@ -0,0 +1,12 @@ +import { env } from '$env/dynamic/private'; +/** + * Server-side environment configuration for Tandoor integration + * These variables should be set in your .env file or as environment variables + */ + +export const tandoorConfig = { + enabled: env.TANDOOR_ENABLED === 'true', + serverUrl: (env.TANDOOR_SERVER_URL || '').replace(/\/$/, ''), + space: env.TANDOOR_SPACE ? parseInt(env.TANDOOR_SPACE, 10) : 1, + token: env.TANDOOR_TOKEN || null +}; \ No newline at end of file diff --git a/src/lib/server/tandoor.ts b/src/lib/server/tandoor.ts new file mode 100644 index 0000000..e8cf854 --- /dev/null +++ b/src/lib/server/tandoor.ts @@ -0,0 +1,330 @@ +import { tandoorConfig } from '$lib/server/tandoor-config'; +import { z } from 'zod'; +/** + * Tandoor Recipe Export Format + * Based on the Default/JSON-LD Tandoor export format + */ +export const TandoorRecipeSchema = z.object({ + name: z.string(), + author: z.string().optional().nullable(), + description: z.string().optional().nullable(), + servings: z.number().optional().nullable(), + servings_text: z.string().optional().nullable(), + keywords: z.array(z.string()).optional(), + prep_time: z.string().optional(), + cook_time: z.string().optional(), + waiting_time: z.string().optional(), + steps: z.array( + z.object({ + step: z.number(), + instruction: z.string(), + ingredients: z.array( + z.object({ + food: z.object({ + id: z.number(), + name: z.string() + }), + unit: z.object({ + id: z.number(), + name: z.string() + }).nullable(), + amount: z.number(), + note: z.string().optional() + }) + ).optional() + }) + ).optional(), + ingredients: z.array( + z.object({ + food: z.object({ + name: z.string() + }), + unit: z.object({ + name: z.string() + }).nullable(), + amount: z.number(), + note: z.string().optional() + }) + ).optional() +}); + +export type TandoorRecipe = z.infer; + +interface ExtractedRecipe { + name: string; + servings: number | null; + description: string | null; + ingredients: Array<{ + item: string; + amount: string; + unit: string; + }> | null; + steps: string[] | null; +} + +/** + * DTO for Tandoor Recipe API (POST /api/recipe/) + * Matches the Tandoor endpoint schema for recipe creation + */ +interface TandoorRecipeDTO { + name: string; + description?: string; + keywords: Array<{ + name: string; + description?: string; + }>; + steps: Array<{ + name?: string; + instruction: string; + ingredients: Array<{ + food: { + name: string; + }; + unit: { + name: string; + } | null; + amount: string; + note?: string; + }>; + order?: number; + show_as_header?: boolean; + }>; + working_time?: number; + waiting_time?: number; + servings?: number; + servings_text?: string; + private?: boolean; + show_ingredient_overview?: boolean; +} + +/** + * Helper function to make authenticated API calls + */ +async function fetchFromTandoor( + url: string, + options: Partial = { method: 'GET' }, +): Promise<{ ok: boolean; data?: T; error?: string }> { + const headers = new Headers({ + 'Content-Type': 'application/json', + 'Accept': 'application/json', + Authorization: `Bearer ${tandoorConfig.token}` + }); + + // Merge any additional headers from options + if (options.headers) { + const optHeaders = new Headers(options.headers); + optHeaders.forEach((value, key) => { + headers.set(key, value); + }); + } + + console.debug(`Fetching from Tandoor: ${url}`, { + method: options.method, + headers: Object.fromEntries(headers), + body: options.body + }); + try { + const response = await fetch(`${tandoorConfig.serverUrl}${url}`, { + ...options, + headers + }); + + if (!response.ok) { + const errorBody = await response.json().catch(() => ({})); + console.error(`API Error ${response.status}: ${response.statusText}`, errorBody); + return { + ok: false, + error: `API Error: ${response.statusText} - ${JSON.stringify(errorBody)}` + }; + } + + const data = (await response.json()) as T; + console.debug(`Tandoor response OK:`, data); + return { ok: true, data }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.error(`Fetch error: ${errorMsg}`); + return { + ok: false, + error: `Fetch error: ${errorMsg}` + }; + } +} + + + +/** + * Partitions ingredients across steps by distributing them evenly + * When step association is unknown, this spreads ingredients proportionally + */ +function partitionIngredientsAcrossSteps( + ingredients: Array<{ + item: string; + amount: string; + unit: string; + }>, + stepCount: number +): Array> { + if (stepCount === 0 || !ingredients || ingredients.length === 0) { + return []; + } + + const partitions: Array> = Array.from( + { length: stepCount }, + () => [] + ); + + // Distribute ingredients round-robin across steps + ingredients.forEach((ingredient, index) => { + partitions[index % stepCount].push(ingredient); + }); + + console.debug(`Partitioned ${ingredients.length} ingredients across ${stepCount} steps:`, partitions); + return partitions; +} + +/** + * Parses amount string to a number, handling special cases + * Returns null if amount cannot be parsed to a valid number + */ +function parseAmount(amountStr: string): number | null { + if (!amountStr || typeof amountStr !== 'string') { + return null; + } + + const trimmed = amountStr.trim().toLowerCase(); + + // Skip special cases that can't be converted to numbers + if (!trimmed || trimmed === 'q.b.' || trimmed === 'qb' || trimmed === 'to taste') { + return null; + } + + // Try to extract the first number from the string + const numberMatch = trimmed.match(/^[\d.,]+/); + if (!numberMatch) { + return null; + } + + const numStr = numberMatch[0].replace(',', '.'); + const parsed = parseFloat(numStr); + + // Return null for zero or invalid numbers + if (isNaN(parsed) || parsed === 0) { + return null; + } + + return parsed; +} + +/** + * Builds a complete Tandoor Recipe DTO from extracted recipe data + * Includes ingredients partitioned across steps + */ +function buildTandoorRecipeDTO(recipe: ExtractedRecipe): TandoorRecipeDTO { + const stepCount = recipe.steps?.length || 1; + const ingredientPartitions = partitionIngredientsAcrossSteps( + recipe.ingredients || [], + stepCount + ); + + const steps: TandoorRecipeDTO['steps'] = (recipe.steps || []).map((instruction, index) => { + // Map ingredients, converting unparseable amounts to 1 q.b. + const mappedIngredients = (ingredientPartitions[index] || []).map((ing) => { + const amount = parseAmount(ing.amount); + + if (amount === null) { + console.debug(`Converting ingredient with unparseable amount to 1 q.b.: ${ing.item} (${ing.amount})`); + return { + food: { + name: ing.item + }, + unit: { name: 'q.b.' }, + amount: '1', + note: '' + }; + } + + return { + food: { + name: ing.item + }, + unit: ing.unit && ing.unit.trim() ? { name: ing.unit } : null, + amount: amount.toString(), + note: '' + }; + }); + + return { + instruction, + order: index, + ingredients: mappedIngredients + }; + }); + + return { + name: recipe.name, + description: recipe.description || undefined, + keywords: [], + steps, + servings: recipe.servings || undefined, + servings_text: recipe.servings ? `${recipe.servings} servings` : undefined, + private: false, + show_ingredient_overview: true + }; +} + +/** + * Uploads a recipe to Tandoor server using the DTO-based approach + * Creates recipe with ingredients partitioned across steps in a single request + */ +export async function uploadRecipeWithIngredientsDTO( + recipe: ExtractedRecipe +): Promise<{ success: boolean; recipeId?: number; error?: string }> { + try { + // Validate token + const token = tandoorConfig.token; + if (!token) { + return { + success: false, + error: 'TANDOOR_TOKEN environment variable not set' + }; + } + + // Build the complete DTO + const recipeDTO = buildTandoorRecipeDTO(recipe); + console.debug('Uploading recipe with ingredients DTO:', recipeDTO); + + // Call the API with the DTO + const recipeResult = await fetchFromTandoor<{ id: number }>( + `/api/recipe/`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(recipeDTO) + } + ); + + if (!recipeResult.ok || !recipeResult.data) { + console.error('Recipe creation failed:', recipeResult.error); + return { + success: false, + error: `Failed to create recipe: ${recipeResult.error}` + }; + } + + const createdRecipe = recipeResult.data; + console.debug('Successfully created recipe with ID:', createdRecipe.id); + + return { + success: true, + recipeId: createdRecipe.id + }; + } catch (error) { + const errorMsg = error instanceof Error ? error.message : 'Unknown error'; + console.error(`Error uploading recipe to Tandoor: ${errorMsg}`); + return { + success: false, + error: `Error uploading to Tandoor: ${errorMsg}` + }; + } +} diff --git a/src/routes/api/extract/+server.ts b/src/routes/api/extract/+server.ts index d5512c9..53428e5 100644 --- a/src/routes/api/extract/+server.ts +++ b/src/routes/api/extract/+server.ts @@ -1,74 +1,129 @@ -import { json } from '@sveltejs/kit'; +import { createBrowserContext } from '$lib/server/browser'; import { createLLM } from '$lib/server/llm'; -import { z } from 'zod'; +import { json } from '@sveltejs/kit'; +import fs, { writeFileSync } from 'fs'; import { zodResponseFormat } from 'openai/helpers/zod'; -import { chromium } from 'playwright'; -import fs from 'fs'; -import { env } from '$env/dynamic/private'; +import { z } from 'zod'; +import path from 'path'; const RecipeSchema = z.object({ name: z.string(), - description: z.string(), - steps: z.array(z.string()), + servings: z.number().nullable(), + description: z.string().nullable(), ingredients: z.array(z.object({ item: z.string(), amount: z.string(), unit: z.string() - })) + })).nullable(), + steps: z.array(z.string()).nullable() }); + export async function POST({ request }) { const { url } = await request.json(); - // 1. Browser Connection - // Fallback to localhost if env var not set (e.g. running outside docker) - const wsEndpoint = env.PLAYWRIGHT_WS_ENDPOINT || 'ws://127.0.0.1:3000'; - console.log('Connecting to browser at:', wsEndpoint); + // 1. Browser Connection - now managed by SvelteKit + console.log('Creating browser context for URL:', url); - const browser = await chromium.connect(wsEndpoint); - - // 2. Load Auth if available - const authPath = '/app/secrets/auth.json'; - let context; - // We check absolute path (Docker) or relative (Local) - if (fs.existsSync(authPath)) { - context = await browser.newContext({ storageState: authPath }); - } else if (fs.existsSync('./secrets/auth.json')) { - context = await browser.newContext({ storageState: './secrets/auth.json' }); - } else { - console.warn('No auth.json found. Running as guest.'); - context = await browser.newContext(); - } - + // Try to find auth storage + const authPathDocker = '/app/secrets/auth.json'; + const authPathLocal = './secrets/auth.json'; + const authPath = fs.existsSync(authPathDocker) ? authPathDocker : + fs.existsSync(authPathLocal) ? authPathLocal : + undefined; + + const context = await createBrowserContext(authPath); const page = await context.newPage(); let bodyText = ''; try { await page.goto(url, { waitUntil: 'domcontentloaded' }); - // Naive scraper attempt - bodyText = await page.evaluate(() => document.body.innerText); + // Extract HTML from the page + bodyText = (await page.evaluate(() => document.body.innerText)).replace(/^(?:.*\n){6}/, '').split('More posts from')[0].trim(); + + // Cleaning steps + // 1. Remove @tags and #hashtags + bodyText = bodyText.replace(/@\w+/g, '').replace(/#\w+/g, ''); + + writeFileSync(path.resolve('debug_page.txt'), bodyText); // Save for debugging, overwriting if exists } catch (e) { console.error('Scraping error:', e); return json({ error: 'Failed to scrape URL' }, { status: 500 }); } finally { await page.close(); await context.close(); - await browser.close(); } - // 3. LLM Processing + // 2. LLM Processing - Two-step validation try { const { client, model } = createLLM(); + + // STEP 1: Binary recipe detection (yes/no only) + const detectionResponse = await client.chat.completions.create({ + model, + messages: [ + { role: "system", content: "You are a recipe detector. Answer with ONLY 'yes' or 'no' - nothing else. A recipe MUST have: (1) name/title, (2) ingredients with quantities, (3) numbered cooking steps. If ANY are missing, answer 'no'." }, + { role: "user", content: `Does this text contain a recipe?\n\n${bodyText}` } + ], + max_tokens: 10, + }); + + const detectionResult = detectionResponse.choices[0].message.content?.toLowerCase() ?? ''; + const hasRecipe = detectionResult.includes("yes"); + + if (!hasRecipe) { + return json({ error: "No recipe found in provided text" }, { status: 400 }); + } + + // STEP 2: Extract recipe (only proceeds if recipe detected) const completion = await client.beta.chat.completions.parse({ model, messages: [ - { role: "system", content: "Extract a recipe structure from this text. If it is not a recipe, return empty arrays." }, - { role: "user", content: bodyText.substring(0, 8000) } // Limit context window + { role: "system", content: `You are a RECIPE EXTRACTOR. Extract the recipe from the provided text. + +✅ REQUIREMENTS: +1. Extract the exact recipe name from the text +2. List all ingredients with their quantities and units +3. List all cooking steps in order +4. Translate everything to Italian +5. Convert measurements to SI units (g, mL, °C) + +📋 CONVERSION TABLE: +- 1 cup = 240 mL, 1 tbsp = 15 mL, 1 tsp = 5 mL +- 1 oz = 28.35 g, 1 lb = 453.59 g +- 1 stick butter = 113 g +- °F→°C: (°F–32)×5/9 + +🔄 OUTPUT FORMAT: +{ + "name": "recipe name in Italian", + "servings": number or null, + "description": "description in Italian or null", + "ingredients": [{"item": "ingredient name", "amount": "quantity", "unit": "SI unit"}], + "steps": ["1. First step", "2. Second step", ...] +} + +Extract ONLY what's explicitly in the text. Be accurate and literal. + ` }, + { role: "user", content: `Extract the recipe from this text:\n\n${bodyText}` } ], response_format: zodResponseFormat(RecipeSchema, "recipe") }); - - return json({ recipe: completion.choices[0].message.parsed }); + console.log('LLM extraction successful:', completion.choices[0].message); + + const recipe = completion.choices[0].message.parsed; + if (!recipe || !recipe.name) { + return json({ error: "Failed to extract recipe" }, { status: 400 }); + } + + // Append original Instagram link to description + if (recipe.description) { + recipe.description += `\n\nLink: ${url}`; + } else { + recipe.description = `Link: ${url}`; + } + + return json({ recipe }); } catch (e) { console.error('LLM error:', e); return json({ error: 'Failed to parse recipe' }, { status: 500 }); diff --git a/src/routes/api/tandoor-config/+server.ts b/src/routes/api/tandoor-config/+server.ts new file mode 100644 index 0000000..505bf4e --- /dev/null +++ b/src/routes/api/tandoor-config/+server.ts @@ -0,0 +1,5 @@ +import { json } from '@sveltejs/kit'; +import {tandoorConfig} from '$lib/server/tandoor-config'; +export async function GET() { + return json({...tandoorConfig, token: ''}); +} diff --git a/src/routes/api/tandoor/+server.ts b/src/routes/api/tandoor/+server.ts new file mode 100644 index 0000000..c2f0d8b --- /dev/null +++ b/src/routes/api/tandoor/+server.ts @@ -0,0 +1,32 @@ +import { json } from '@sveltejs/kit'; +import { uploadRecipeWithIngredientsDTO } from '$lib/server/tandoor'; + +export async function POST({ request }) { + const { recipe } = await request.json(); + + if (!recipe) { + return json({ error: 'No recipe provided' }, { status: 400 }); + } + + try { + const result = await uploadRecipeWithIngredientsDTO(recipe); + + if (!result.success) { + return json({ error: result.error || 'Failed to upload recipe' }, { status: 500 }); + } + + return json({ + success: true, + message: 'Recipe successfully imported to Tandoor', + recipeId: result.recipeId + }); + } catch (error) { + console.error('Tandoor upload error:', error); + return json( + { + error: error instanceof Error ? error.message : 'Unknown error occurred' + }, + { status: 500 } + ); + } +} diff --git a/src/routes/share/+page.svelte b/src/routes/share/+page.svelte index c5f15a4..b16509b 100644 --- a/src/routes/share/+page.svelte +++ b/src/routes/share/+page.svelte @@ -4,6 +4,9 @@ let status = $state('idle'); let logs = $state([]); let recipe = $state(null); + let tandoorEnabled = $state(false); + let tandoorImporting = $state(false); + let tandoorError = $state(null); // URL param parsing for Share Target // Instagram typically shares text that contains the URL, so we might need to parse it out @@ -17,6 +20,22 @@ let targetUrl = $derived(sharedUrl || extractUrl(sharedText)); + $effect.pre(() => { + loadTandoorConfig(); + }); + + // Load Tandoor config on mount + async function loadTandoorConfig() { + try { + const res = await fetch('/api/tandoor-config'); + const config = await res.json(); + tandoorEnabled = config.enabled; + logs = [...logs, `Tandoor integration ${config.enabled ? 'enabled' : 'disabled'}`]; + } catch(e) { + logs = [...logs, 'Failed to load Tandoor config']; + } + } + async function process() { if(!targetUrl) return; status = 'extracting'; @@ -33,6 +52,7 @@ if (data.recipe) { recipe = data.recipe; status = 'done'; + logs = [...logs, 'Recipe extraction successful']; } else { logs = [...logs, 'Error: ' + JSON.stringify(data)]; status = 'error'; @@ -42,6 +62,38 @@ status = 'error'; } } + + async function importToTandoor() { + if (!recipe) return; + + tandoorImporting = true; + tandoorError = null; + logs = [...logs, 'Importing recipe to Tandoor...']; + + try { + const res = await fetch('/api/tandoor', { + method: 'POST', + body: JSON.stringify({ recipe }), + headers: { 'Content-Type': 'application/json' } + }); + + const data = await res.json(); + + if (data.success) { + logs = [...logs, `✓ Recipe imported successfully (ID: ${data.recipeId})`]; + tandoorError = null; + } else { + logs = [...logs, `✗ Import failed: ${data.error}`]; + tandoorError = data.error; + } + } catch(e) { + const errorMsg = e instanceof Error ? e.message : 'Unknown error'; + logs = [...logs, `✗ Network error: ${errorMsg}`]; + tandoorError = errorMsg; + } finally { + tandoorImporting = false; + } + }
@@ -68,12 +120,37 @@

{recipe.name}

{recipe.description}

+

Servings: {recipe.servings}

Ingredients

    {#each recipe.ingredients as ing}
  • {ing.amount} {ing.unit} {ing.item}
  • {/each}
+

Steps

+
    + {#each recipe.steps as step} +
  1. {step}
  2. + {/each} +
+ + {#if tandoorEnabled} +
+

Tandoor Integration

+ {#if tandoorError} +
+ Error: {tandoorError} +
+ {/if} + +
+ {/if}
{/if} diff --git a/vite.config.ts b/vite.config.ts index d14deb2..d7b319c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -5,6 +5,11 @@ import { sveltekit } from '@sveltejs/kit/vite'; import { SvelteKitPWA } from '@vite-pwa/sveltekit'; export default defineConfig({ + server: { + watch: { + ignored: ['**/debug_page.txt'] + } + }, plugins: [ SvelteKitPWA({ srcDir: './src',