From 2e2ba12cf34dd231bf3d87b293ebed2e9f9d601a Mon Sep 17 00:00:00 2001 From: Daniel Date: Tue, 19 May 2026 19:47:30 +0200 Subject: [PATCH] Update web model management and meal diary --- web/package-lock.json | 1059 ++++++++++++++++++++++++++++++++++- web/package.json | 5 +- web/server.js | 218 +++++-- web/src/App.svelte | 408 +++++++++++--- web/src/DiaryPage.svelte | 76 +++ web/src/MealForm.svelte | 57 ++ web/src/SettingsPage.svelte | 85 +++ web/src/app.css | 11 + web/tests/app.spec.js | 56 +- 9 files changed, 1852 insertions(+), 123 deletions(-) create mode 100644 web/src/DiaryPage.svelte create mode 100644 web/src/MealForm.svelte create mode 100644 web/src/SettingsPage.svelte diff --git a/web/package-lock.json b/web/package-lock.json index 0eb16f4..8dcf3fb 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8,20 +8,23 @@ "name": "calorie-ai-web", "version": "0.1.0", "dependencies": { + "argon2": "^0.44.0", + "express": "^5.2.1" + }, + "devDependencies": { + "@playwright/test": "^1.56.1", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@tailwindcss/vite": "^4.3.0", "svelte": "^5.55.8", "tailwindcss": "^4.3.0", "vite": "^8.0.13" - }, - "devDependencies": { - "@playwright/test": "^1.56.1" } }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -33,6 +36,7 @@ "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -43,16 +47,24 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" } }, + "node_modules/@epic-web/invariant": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@epic-web/invariant/-/invariant-1.0.0.tgz", + "integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==", + "license": "MIT" + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", @@ -63,6 +75,7 @@ "version": "2.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", @@ -73,6 +86,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -82,12 +96,14 @@ "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/@jridgewell/trace-mapping": { "version": "0.3.31", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -98,6 +114,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -116,11 +133,21 @@ "version": "0.130.0", "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.130.0.tgz", "integrity": "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/@phc/format": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@phc/format/-/format-1.0.0.tgz", + "integrity": "sha512-m7X9U6BG2+J+R1lSOdCiITLLrxm+cWlNI3HUFA92oLO77ObGNzaKdh8pMLqdZcshtkKuV84olNNXDfMc4FezBQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/@playwright/test": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", @@ -144,6 +171,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -160,6 +188,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -176,6 +205,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -192,6 +222,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -208,6 +239,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -224,6 +256,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -240,6 +273,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -256,6 +290,7 @@ "cpu": [ "ppc64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -272,6 +307,7 @@ "cpu": [ "s390x" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -288,6 +324,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -304,6 +341,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -320,6 +358,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -336,6 +375,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -354,6 +394,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -370,6 +411,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -383,12 +425,14 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "dev": true, "license": "MIT", "peerDependencies": { "acorn": "^8.9.0" @@ -398,6 +442,7 @@ "version": "7.1.2", "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.1.2.tgz", "integrity": "sha512-DrUBA2UXRfDmUX/ZTiEopd3X40yavsJF1FX2RygcuIScHL7o5YX1fMvoYnDhjeJQC4weCOklirpNWlcb2NiSeA==", + "dev": true, "license": "MIT", "dependencies": { "deepmerge": "^4.3.1", @@ -417,6 +462,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.3.0.tgz", "integrity": "sha512-aFb4gUhFOgdh9AXo4IzBEOzBkkAxm9VigwDJnMIYv3lcfXCJVesNfbEaBl4BNgVRyid92AmdviqwBUBRKSeY3g==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.5", @@ -432,6 +478,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.3.0.tgz", "integrity": "sha512-F7HZGBeN9I0/AuuJS5PwcD8xayx5ri5GhjYUDBEVYUkexyA/giwbDNjRVrxSezE3T250OU2K/wp/ltWx3UOefg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 20" @@ -458,6 +505,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -474,6 +522,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -490,6 +539,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -506,6 +556,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -522,6 +573,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -538,6 +590,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -554,6 +607,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -570,6 +624,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -586,6 +641,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -610,6 +666,7 @@ "cpu": [ "wasm32" ], + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -631,6 +688,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -647,6 +705,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MIT", "optional": true, "os": [ @@ -660,6 +719,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.3.0.tgz", "integrity": "sha512-t6J3OrB5Fc0ExuhohouH0fWUGMYL6PTLhW+E7zIk/pdbnJARZDCwjBznFnkh5ynRnIRSI4YjtTH0t6USjJISrw==", + "dev": true, "license": "MIT", "dependencies": { "@tailwindcss/node": "4.3.0", @@ -674,6 +734,7 @@ "version": "0.10.2", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -684,18 +745,34 @@ "version": "1.0.9", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, "license": "MIT" }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, "license": "MIT" }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "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", "bin": { "acorn": "bin/acorn" @@ -704,10 +781,27 @@ "node": ">=0.4.0" } }, + "node_modules/argon2": { + "version": "0.44.0", + "resolved": "https://registry.npmjs.org/argon2/-/argon2-0.44.0.tgz", + "integrity": "sha512-zHPGN3S55sihSQo0dBbK0A5qpi2R31z7HZDZnry3ifOyj8bZZnpZND2gpmhnRGO1V/d555RwBqIK5W4Mrmv3ig==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@phc/format": "^1.0.0", + "cross-env": "^10.0.0", + "node-addon-api": "^8.5.0", + "node-gyp-build": "^4.8.4" + }, + "engines": { + "node": ">=16.17.0" + } + }, "node_modules/aria-query": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" @@ -717,33 +811,196 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, "license": "Apache-2.0", "engines": { "node": ">= 0.4" } }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" } }, + "node_modules/content-disposition": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz", + "integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cross-env": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-10.1.0.tgz", + "integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==", + "license": "MIT", + "dependencies": { + "@epic-web/invariant": "^1.0.0", + "cross-spawn": "^7.0.6" + }, + "bin": { + "cross-env": "dist/bin/cross-env.js", + "cross-env-shell": "dist/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/deepmerge": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "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" @@ -753,12 +1010,43 @@ "version": "5.8.1", "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.8.1.tgz", "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", + "dev": true, "license": "MIT" }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/enhanced-resolve": { "version": "5.21.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.21.5.tgz", "integrity": "sha512-mLCNbrQli11K1ySUmuNt4ZUB3OpGIDq4q2vTBTf5cL2lpsRjI9QKqSD0ndjW8FyvcW/Jj46gMe9syyHAsvMa/A==", + "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", @@ -768,16 +1056,54 @@ "node": ">=10.13.0" } }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "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-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/esm-env": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz", "integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==", + "dev": true, "license": "MIT" }, "node_modules/esrap": { "version": "2.2.9", "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.9.tgz", "integrity": "sha512-4KijP+NxCWthMCUC3qHbE6n4vCjqgJS1uAYKhuT/GWfFTf1Qyive2TgOjep+gzbSzRfnNyaN/UU9YmdOt8Eg0A==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.15" @@ -791,10 +1117,63 @@ } } }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "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" @@ -808,6 +1187,45 @@ } } }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -823,25 +1241,173 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, "license": "ISC" }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", "integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==", + "dev": true, "license": "MIT", "dependencies": { "@types/estree": "^1.0.6" } }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, "node_modules/jiti": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -851,6 +1417,7 @@ "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" @@ -883,6 +1450,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -903,6 +1471,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -923,6 +1492,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -943,6 +1513,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -963,6 +1534,7 @@ "cpu": [ "arm" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -983,6 +1555,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1003,6 +1576,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1023,6 +1597,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1043,6 +1618,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1063,6 +1639,7 @@ "cpu": [ "arm64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1083,6 +1660,7 @@ "cpu": [ "x64" ], + "dev": true, "license": "MPL-2.0", "optional": true, "os": [ @@ -1100,21 +1678,85 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==", + "dev": true, "license": "MIT" }, "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", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.12", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, "funding": [ { "type": "github", @@ -1129,26 +1771,119 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-addon-api": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.7.0.tgz", + "integrity": "sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz", + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "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", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "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.4", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, "license": "MIT", "engines": { "node": ">=12" @@ -1193,6 +1928,7 @@ "version": "8.5.15", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, "funding": [ { "type": "opencollective", @@ -1217,10 +1953,63 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/rolldown": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.1.tgz", "integrity": "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ==", + "dev": true, "license": "MIT", "dependencies": { "@oxc-project/types": "=0.130.0", @@ -1250,19 +2039,196 @@ "@rolldown/binding-win32-x64-msvc": "1.0.1" } }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "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==", + "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==", + "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", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/svelte": { "version": "5.55.8", "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.55.8.tgz", "integrity": "sha512-4D6lyrMHmDaZalQOEBMCWCCidyZjSnec14/oPn0k627G6goxcck9xqMwz1tFLlQz+ZFvtTTHfFOlUayuAz0z6Q==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/remapping": "^2.3.4", @@ -1290,12 +2256,14 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.3.0.tgz", "integrity": "sha512-y6nxMGB1nMW9R6k96e5gdIFzcfL/gTJRNaqGes1YvkLnPVXzWgbqFF2yLC0T8G774n24cx3Pe8XrKoniCOAH+Q==", + "dev": true, "license": "MIT" }, "node_modules/tapable": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.3.tgz", "integrity": "sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==", + "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1309,6 +2277,7 @@ "version": "0.2.16", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, "license": "MIT", "dependencies": { "fdir": "^6.5.0", @@ -1321,17 +2290,77 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "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-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz", + "integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, "node_modules/vite": { "version": "8.0.13", "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.13.tgz", "integrity": "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw==", + "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.32.0", @@ -1409,6 +2438,7 @@ "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, @@ -1423,6 +2453,7 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "dev": true, "license": "MIT", "workspaces": [ "tests/deps/*", @@ -1438,10 +2469,32 @@ } } }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, "node_modules/zimmerframe": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.4.tgz", "integrity": "sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==", + "dev": true, "license": "MIT" } } diff --git a/web/package.json b/web/package.json index f44e2da..239239b 100644 --- a/web/package.json +++ b/web/package.json @@ -16,5 +16,8 @@ "tailwindcss": "^4.3.0", "vite": "^8.0.13" }, - "dependencies": {} + "dependencies": { + "argon2": "^0.44.0", + "express": "^5.2.1" + } } diff --git a/web/server.js b/web/server.js index 7becc95..02b2c1e 100644 --- a/web/server.js +++ b/web/server.js @@ -2,15 +2,18 @@ const http = require('http'); const crypto = require('crypto'); const fs = require('fs'); const path = require('path'); +const argon2 = require('argon2'); const port = Number(process.env.PORT || 8080); const publicDir = fs.existsSync(path.join(__dirname, 'dist')) ? path.join(__dirname, 'dist') : path.join(__dirname, 'public'); const publicRoot = publicDir + path.sep; const dataDir = process.env.CALORIE_AI_DATA_DIR || path.join(__dirname, 'data'); const authFile = path.join(dataDir, 'auth.json'); +const modelsFile = path.join(dataDir, 'models.json'); const sessionCookieName = 'calorie_ai_session'; const sessionSeconds = 60 * 60 * 24 * 7; -const auth = loadAuthConfig(); +let auth; +let modelConfig; const aiConfig = { baseUrl: (process.env.CALORIE_AI_API_BASE_URL || process.env.LITELLM_API_BASE || 'https://llm.danvics.com/v1').replace(/\/+$/, ''), apiKey: process.env.CALORIE_AI_API_KEY || process.env.LITELLM_API_KEY || process.env.OPENAI_API_KEY || '', @@ -41,26 +44,74 @@ function readBody(req) { }); } -function loadAuthConfig() { +function readJsonFile(file, fallback) { + try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return fallback; } +} + +function writePrivateJson(file, value) { + fs.mkdirSync(path.dirname(file), { recursive: true }); + fs.writeFileSync(file, JSON.stringify(value, null, 2)); + fs.chmodSync(file, 0o600); +} + +async function hashPassword(password) { + return argon2.hash(password, { type: argon2.argon2id }); +} + +async function loadAuthConfig() { let stored = {}; - try { - stored = JSON.parse(fs.readFileSync(authFile, 'utf8')); - } catch { - stored = {}; - } + stored = readJsonFile(authFile, {}); const user = process.env.CALORIE_AI_WEB_USER || stored.user || 'admin'; - const password = process.env.CALORIE_AI_WEB_PASSWORD || stored.password || crypto.randomBytes(18).toString('base64url'); + if (process.env.CALORIE_AI_WEB_PASSWORD && process.env.CALORIE_AI_SESSION_SECRET) { + return { + user, + passwordHash: await hashPassword(process.env.CALORIE_AI_WEB_PASSWORD), + sessionSecret: process.env.CALORIE_AI_SESSION_SECRET, + }; + } + + const generatedPassword = crypto.randomBytes(18).toString('base64url'); + const plainPassword = stored.password || (!stored.passwordHash ? generatedPassword : ''); + const passwordHash = stored.passwordHash || await hashPassword(plainPassword); const sessionSecret = process.env.CALORIE_AI_SESSION_SECRET || stored.sessionSecret || crypto.randomBytes(32).toString('hex'); - if (!process.env.CALORIE_AI_WEB_PASSWORD || !process.env.CALORIE_AI_SESSION_SECRET) { - fs.mkdirSync(dataDir, { recursive: true }); - fs.writeFileSync(authFile, JSON.stringify({ user, password, sessionSecret, createdAt: new Date().toISOString() }, null, 2)); - fs.chmodSync(authFile, 0o600); + if (!stored.passwordHash || !stored.sessionSecret || stored.password) { + writePrivateJson(authFile, { + user, + passwordHash, + sessionSecret, + createdAt: stored.createdAt || new Date().toISOString(), + updatedAt: new Date().toISOString(), + ...(plainPassword && !stored.passwordHash ? { initialPassword: plainPassword } : {}), + }); console.log(`Calorie AI auth is stored at ${authFile}`); } - return { user, password, sessionSecret }; + return { user, passwordHash, sessionSecret }; +} + +function loadModelConfig() { + const stored = readJsonFile(modelsFile, {}); + const defaultModel = stored.defaultModel || aiConfig.defaultModel; + const models = Array.isArray(stored.models) && stored.models.length + ? stored.models + : [{ id: defaultModel, name: defaultModel, tag: 'DEFAULT' }]; + const config = { defaultModel, models: dedupeModels(models) }; + if (!fs.existsSync(modelsFile)) saveModelConfig(config); + return config; +} + +function saveModelConfig(config = modelConfig) { + writePrivateJson(modelsFile, { ...config, updatedAt: new Date().toISOString() }); +} + +function dedupeModels(models) { + return Array.from(new Map((models || []).filter(model => model && model.id).map(model => [model.id, { + id: String(model.id), + name: String(model.name || model.id), + tag: model.tag || 'ADDED', + }])).values()).sort((a, b) => a.name.localeCompare(b.name)); } function send(res, status, body, contentType = 'application/json; charset=utf-8', headOnly = false, headers = {}) { @@ -173,35 +224,78 @@ async function proxyChat(req, res) { } } +async function fetchProviderModels(search = '') { + if (!/^https?:\/\//.test(aiConfig.baseUrl)) throw new Error('AI endpoint is not configured'); + + const headers = {}; + if (aiConfig.apiKey) headers.authorization = `Bearer ${aiConfig.apiKey}`; + const infoBase = aiConfig.baseUrl.replace(/\/v1$/, ''); + + let models = []; + try { + const info = await fetch(`${infoBase}/model/info`, { headers }); + if (info.ok) { + const data = await info.json(); + models = (data.data || []) + .filter(item => item.model_name && (!item.model_info || !item.model_info.mode || item.model_info.mode === 'chat')) + .map(item => ({ id: item.model_name, name: item.model_name })); + } + } catch {} + + if (!models.length) { + const upstream = await fetch(`${aiConfig.baseUrl}/models`, { headers }); + const data = await upstream.json(); + models = (data.data || []).map(item => ({ id: item.id, name: item.id })); + } + + const query = search.trim().toLowerCase(); + return dedupeModels(models) + .filter(model => !query || `${model.name} ${model.id}`.toLowerCase().includes(query)) + .slice(0, 100); +} + async function proxyModels(req, res) { try { - if (!/^https?:\/\//.test(aiConfig.baseUrl)) throw new Error('AI endpoint is not configured'); + send(res, 200, JSON.stringify({ models: modelConfig.models, defaultModel: modelConfig.defaultModel })); + } catch (error) { + send(res, 400, JSON.stringify({ error: error.message })); + } +} - const headers = {}; - if (aiConfig.apiKey) headers.authorization = `Bearer ${aiConfig.apiKey}`; - const infoBase = aiConfig.baseUrl.replace(/\/v1$/, ''); - - let models = []; - try { - const info = await fetch(`${infoBase}/model/info`, { headers }); - if (info.ok) { - const data = await info.json(); - models = (data.data || []) - .filter(item => item.model_name && (!item.model_info || !item.model_info.mode || item.model_info.mode === 'chat')) - .map(item => ({ id: item.model_name, name: item.model_name })); - } - } catch {} - - if (!models.length) { - const upstream = await fetch(`${aiConfig.baseUrl}/models`, { headers }); - const data = await upstream.json(); - models = (data.data || []).map(item => ({ id: item.id, name: item.id })); +async function searchModels(req, res) { + try { + const payload = req.method === 'POST' ? JSON.parse(await readBody(req) || '{}') : {}; + if (!payload.query || String(payload.query).trim().length < 2) { + return send(res, 200, JSON.stringify({ models: [], count: 0 })); } + const models = await fetchProviderModels(payload.query || ''); + send(res, 200, JSON.stringify({ models, count: models.length })); + } catch (error) { + send(res, 400, JSON.stringify({ error: error.message })); + } +} - const deduped = Array.from(new Map(models.filter(m => m.id).map(m => [m.id, m])).values()) - .sort((a, b) => a.name.localeCompare(b.name)); - if (!deduped.some(model => model.id === aiConfig.defaultModel)) deduped.unshift({ id: aiConfig.defaultModel, name: aiConfig.defaultModel }); - send(res, 200, JSON.stringify({ models: deduped, defaultModel: aiConfig.defaultModel })); +async function addModel(req, res) { + try { + const payload = JSON.parse(await readBody(req)); + if (!payload.id) return send(res, 400, JSON.stringify({ error: 'Model ID required' })); + modelConfig.models = dedupeModels([...modelConfig.models, { id: payload.id, name: payload.name || payload.id, tag: 'ADDED' }]); + if (!modelConfig.defaultModel) modelConfig.defaultModel = payload.id; + saveModelConfig(); + send(res, 200, JSON.stringify({ ok: true, models: modelConfig.models, defaultModel: modelConfig.defaultModel })); + } catch (error) { + send(res, 400, JSON.stringify({ error: error.message })); + } +} + +async function removeModel(req, res) { + try { + const payload = JSON.parse(await readBody(req)); + modelConfig.models = modelConfig.models.filter(model => model.id !== payload.id); + if (!modelConfig.models.length) modelConfig.models = [{ id: aiConfig.defaultModel, name: aiConfig.defaultModel, tag: 'DEFAULT' }]; + if (!modelConfig.models.some(model => model.id === modelConfig.defaultModel)) modelConfig.defaultModel = modelConfig.models[0].id; + saveModelConfig(); + send(res, 200, JSON.stringify({ ok: true, models: modelConfig.models, defaultModel: modelConfig.defaultModel })); } catch (error) { send(res, 400, JSON.stringify({ error: error.message })); } @@ -210,7 +304,9 @@ async function proxyModels(req, res) { async function login(req, res) { try { const payload = JSON.parse(await readBody(req)); - if (!safeEqual(payload.username || '', auth.user) || !safeEqual(payload.password || '', auth.password)) { + const validUser = safeEqual(payload.username || '', auth.user); + const validPassword = validUser && await argon2.verify(auth.passwordHash, payload.password || ''); + if (!validUser || !validPassword) { return send(res, 401, JSON.stringify({ error: 'Invalid username or password' })); } @@ -222,13 +318,37 @@ async function login(req, res) { } } +async function changePassword(req, res) { + try { + const payload = JSON.parse(await readBody(req)); + if (!await argon2.verify(auth.passwordHash, payload.currentPassword || '')) return send(res, 401, JSON.stringify({ error: 'Current password is incorrect' })); + if (!payload.newPassword || payload.newPassword.length < 12) return send(res, 400, JSON.stringify({ error: 'Use at least 12 characters for the new password' })); + auth.passwordHash = await hashPassword(payload.newPassword); + writePrivateJson(authFile, { user: auth.user, passwordHash: auth.passwordHash, sessionSecret: auth.sessionSecret, updatedAt: new Date().toISOString() }); + send(res, 200, JSON.stringify({ ok: true })); + } catch (error) { + send(res, 400, JSON.stringify({ error: error.message })); + } +} + +async function resetPassword(req, res) { + try { + const generatedPassword = crypto.randomBytes(18).toString('base64url'); + auth.passwordHash = await hashPassword(generatedPassword); + writePrivateJson(authFile, { user: auth.user, passwordHash: auth.passwordHash, sessionSecret: auth.sessionSecret, updatedAt: new Date().toISOString() }); + send(res, 200, JSON.stringify({ ok: true, password: generatedPassword })); + } catch (error) { + send(res, 400, JSON.stringify({ error: error.message })); + } +} + function logout(req, res) { send(res, 200, JSON.stringify({ ok: true }), undefined, false, { 'set-cookie': `${sessionCookieName}=; ${cookieOptions(req, 0)}`, }); } -http.createServer((req, res) => { +async function handleRequest(req, res) { const url = req.url.split('?')[0]; const authenticated = isAuthenticated(req); @@ -240,11 +360,27 @@ http.createServer((req, res) => { if (authenticated && (req.method === 'GET' || req.method === 'HEAD') && url === '/login') return redirect(res, '/'); if (req.method === 'POST' && url === '/api/login') return login(req, res); if (req.method === 'POST' && url === '/api/logout') return logout(req, res); + if (req.method === 'POST' && url === '/api/password/change') return changePassword(req, res); + if (req.method === 'POST' && url === '/api/password/reset') return resetPassword(req, res); if (req.method === 'GET' && url === '/api/session') return send(res, 200, JSON.stringify({ user: auth.user })); if (req.method === 'POST' && url === '/api/models') return proxyModels(req, res); + if (req.method === 'POST' && url === '/api/models/search') return searchModels(req, res); + if (req.method === 'POST' && url === '/api/models/add') return addModel(req, res); + if (req.method === 'POST' && url === '/api/models/remove') return removeModel(req, res); if (req.method === 'POST' && req.url === '/api/chat') return proxyChat(req, res); if (req.method === 'GET' || req.method === 'HEAD') return serveStatic(req, res); send(res, 405, 'Method not allowed', 'text/plain; charset=utf-8'); -}).listen(port, () => { - console.log(`Calorie AI web listening on ${port}`); +} + +async function start() { + auth = await loadAuthConfig(); + modelConfig = loadModelConfig(); + http.createServer((req, res) => handleRequest(req, res).catch(error => send(res, 500, JSON.stringify({ error: error.message })))).listen(port, () => { + console.log(`Calorie AI web listening on ${port}`); + }); +} + +start().catch(error => { + console.error(error); + process.exit(1); }); diff --git a/web/src/App.svelte b/web/src/App.svelte index c64b434..388ad0d 100644 --- a/web/src/App.svelte +++ b/web/src/App.svelte @@ -2,16 +2,20 @@ import { onMount, tick } from 'svelte'; import Field from './Field.svelte'; import Stat from './Stat.svelte'; + import MealForm from './MealForm.svelte'; + import DiaryPage from './DiaryPage.svelte'; + import SettingsPage from './SettingsPage.svelte'; const entriesKey = 'calorie-ai-web-entries'; + const trashKey = 'calorie-ai-web-trash'; const settingsKey = 'calorie-ai-web-settings'; const pages = [ - { id: 'dashboard', label: 'Dashboard', group: 'Tracker' }, - { id: 'log', label: 'Log meal', group: 'Tracker' }, - { id: 'analytics', label: 'Analytics', group: 'Tracker' }, - { id: 'diary', label: 'Diary', group: 'Tracker' }, - { id: 'models', label: 'Models', group: 'Admin' }, - { id: 'settings', label: 'Settings', group: 'Admin' }, + { id: 'dashboard', label: 'Dashboard', icon: '⌂', group: 'Tracker' }, + { id: 'log', label: 'Log meal', icon: '+', group: 'Tracker' }, + { id: 'analytics', label: 'Analytics', icon: '↗', group: 'Tracker' }, + { id: 'diary', label: 'Diary', icon: '☰', group: 'Tracker' }, + { id: 'trash', label: 'Trash', icon: '×', group: 'Tracker' }, + { id: 'settings', label: 'Settings', icon: '⚙', group: 'Admin' }, ]; const mealTypes = ['Breakfast', 'Lunch', 'Dinner', 'Snack', 'Other']; const fallbackModels = [ @@ -30,14 +34,21 @@ let page = 'dashboard'; let menuSearch = ''; let menuOpen = false; - let sidebarCollapsed = false; let entries = []; let models = fallbackModels; + let discoveredModels = []; let modelSearch = ''; let modelStatus = ''; let modelError = false; - let settings = { visionModel: fallbackModels[0].id, taskModel: fallbackModels[0].id }; + let modelSearching = false; + let settings = { visionModel: fallbackModels[0].id, taskModel: fallbackModels[0].id, heightCm: '', weightKg: '', targetWeightKg: '', activityLevel: 'Moderate', pace: 'Steady' }; + let currentPassword = ''; + let newPassword = ''; + let confirmPassword = ''; + let passwordStatus = ''; + let passwordError = false; + let resetPasswordValue = ''; let mealDate = ''; let mealTime = ''; @@ -49,11 +60,18 @@ let status = ''; let statusError = false; let savingMeal = false; + let editingEntryId = ''; + let estimateDraft = emptyEstimate(); + let planStatus = ''; + let planError = false; + let planLoading = false; + let planAdvice = ''; let diarySearch = ''; let diaryType = ''; let diaryDate = ''; - let clearArmed = false; + let selectedEntryIds = []; + let trash = []; let macroCanvas; let calorieCanvas; @@ -66,7 +84,6 @@ $: week = last7Days(); $: weekAverage = Math.round(week.reduce((sum, day) => sum + day.calories, 0) / week.length); $: produceToday = today.fruit + today.vegetables; - $: filteredModels = models.filter(model => !modelSearch || `${model.name} ${model.id}`.toLowerCase().includes(modelSearch.toLowerCase())); $: diaryRows = entries .filter(entry => !diaryType || entry.mealType === diaryType) .filter(entry => !diaryDate || entry.date === diaryDate) @@ -76,11 +93,12 @@ .some(value => String(value || '').toLowerCase().includes(query)); }) .sort((a, b) => `${b.date} ${b.time}`.localeCompare(`${a.date} ${a.time}`)); + $: diaryDays = Array.from(new Set(diaryRows.map(entry => entry.date))).sort((a, b) => b.localeCompare(a)); onMount(async () => { entries = readJson(entriesKey, []); + trash = readJson(trashKey, []); settings = { ...settings, ...readJson(settingsKey, settings) }; - sidebarCollapsed = localStorage.getItem('calorie_ai_sidebar_collapsed') === '1'; page = localStorage.getItem('calorie_ai_last_tab') || 'dashboard'; setDefaultMealDateTime(); await loadModels(); @@ -151,11 +169,6 @@ localStorage.setItem('calorie_ai_last_tab', id); } - function toggleSidebarCollapse() { - sidebarCollapsed = !sidebarCollapsed; - localStorage.setItem('calorie_ai_sidebar_collapsed', sidebarCollapsed ? '1' : '0'); - } - async function login() { loginStatus = 'Checking credentials...'; loginError = false; @@ -190,10 +203,10 @@ if (!models.some(model => model.id === settings.visionModel)) settings.visionModel = defaultModel; if (!models.some(model => model.id === settings.taskModel)) settings.taskModel = defaultModel; saveSettings(); - modelStatus = `${models.length} models loaded.`; + modelStatus = `${models.length} saved model${models.length === 1 ? '' : 's'} loaded.`; } catch { models = fallbackModels; - modelStatus = 'Could not load server models. Showing defaults.'; + modelStatus = 'Could not load saved models. Showing defaults.'; modelError = true; } } @@ -202,6 +215,10 @@ saveJson(settingsKey, settings); } + function emptyEstimate() { + return { mealName: '', calories: 0, proteinGrams: 0, carbsGrams: 0, fatGrams: 0, fruitServings: 0, vegetableServings: 0, foodGroups: '', notes: '' }; + } + function saveSelectedModels() { saveSettings(); modelStatus = 'Models saved.'; @@ -214,6 +231,116 @@ saveSelectedModels(); } + async function searchModels() { + if (modelSearch.trim().length < 2) { + discoveredModels = []; + modelStatus = 'Type at least 2 characters to search provider models.'; + modelError = true; + return; + } + modelSearching = true; + modelStatus = 'Searching provider...'; + modelError = false; + try { + const response = await fetch('/api/models/search', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ query: modelSearch }), + }); + if (!response.ok) throw new Error(await response.text()); + const data = await response.json(); + discoveredModels = data.models || []; + modelStatus = discoveredModels.length ? `Found ${data.count} model${data.count === 1 ? '' : 's'}.` : 'No models found.'; + } catch (error) { + discoveredModels = []; + modelStatus = error.message || 'Model search failed.'; + modelError = true; + } finally { + modelSearching = false; + } + } + + async function addModel(model) { + const response = await fetch('/api/models/add', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(model), + }); + if (!response.ok) { + modelStatus = await response.text(); + modelError = true; + return; + } + const data = await response.json(); + models = data.models?.length ? data.models : models; + settings.visionModel = models.some(item => item.id === settings.visionModel) ? settings.visionModel : models[0].id; + settings.taskModel = model.id; + saveSettings(); + modelStatus = `Added ${model.name || model.id}.`; + modelError = false; + } + + async function removeModel(modelId) { + const response = await fetch('/api/models/remove', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ id: modelId }), + }); + if (!response.ok) { + modelStatus = await response.text(); + modelError = true; + return; + } + const data = await response.json(); + models = data.models?.length ? data.models : fallbackModels; + if (!models.some(model => model.id === settings.visionModel)) settings.visionModel = data.defaultModel || models[0].id; + if (!models.some(model => model.id === settings.taskModel)) settings.taskModel = data.defaultModel || models[0].id; + saveSettings(); + modelStatus = 'Model removed.'; + modelError = false; + } + + async function changePassword() { + passwordStatus = 'Updating password...'; + passwordError = false; + resetPasswordValue = ''; + if (newPassword !== confirmPassword) { + passwordStatus = 'New passwords do not match.'; + passwordError = true; + return; + } + const response = await fetch('/api/password/change', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ currentPassword, newPassword }), + }); + if (!response.ok) { + const error = await response.json().catch(() => ({ error: 'Password update failed.' })); + passwordStatus = error.error || 'Password update failed.'; + passwordError = true; + return; + } + currentPassword = ''; + newPassword = ''; + confirmPassword = ''; + passwordStatus = 'Password updated.'; + } + + async function resetPassword() { + passwordStatus = 'Generating a new password...'; + passwordError = false; + const response = await fetch('/api/password/reset', { method: 'POST' }); + if (!response.ok) { + const error = await response.json().catch(() => ({ error: 'Password reset failed.' })); + passwordStatus = error.error || 'Password reset failed.'; + passwordError = true; + return; + } + const data = await response.json(); + resetPasswordValue = data.password || ''; + passwordStatus = 'Password reset. Save the generated password now.'; + } + async function fileToDataUrl(file) { return new Promise((resolve, reject) => { const reader = new FileReader(); @@ -290,6 +417,10 @@ Image estimate: ${imageEstimate}` }, }; } + function entryId() { + return crypto.randomUUID ? crypto.randomUUID() : `${Date.now()}-${Math.random().toString(16).slice(2)}`; + } + async function saveMeal() { if (!mealDate || !mealTime || !mealType) return setMealStatus('Choose a date, time, and meal type.', true); if (!description.trim() && !selectedImageDataUrl) return setMealStatus('Describe the meal or upload an image.', true); @@ -298,8 +429,10 @@ Image estimate: ${imageEstimate}` }, setMealStatus(selectedImageDataUrl ? 'Analyzing image...' : 'Estimating meal...'); const imageEstimate = await requestImageEstimate(); const estimate = parseEstimate(await requestMealEstimate(imageEstimate)); - entries = [...entries, { + estimateDraft = estimate; + const meal = { ...estimate, + id: editingEntryId || entryId(), date: mealDate, time: mealTime, mealType, @@ -307,14 +440,18 @@ Image estimate: ${imageEstimate}` }, measure, imageIncluded: Boolean(selectedImageDataUrl), imageName: selectedImageName, - }]; + updatedAt: new Date().toISOString(), + }; + entries = editingEntryId ? entries.map(entry => entry.id === editingEntryId ? { ...entry, ...meal } : entry) : [...entries, meal]; saveJson(entriesKey, entries); description = ''; measure = ''; selectedImageDataUrl = ''; selectedImageName = ''; + editingEntryId = ''; + estimateDraft = emptyEstimate(); setDefaultMealDateTime(); - setMealStatus('Saved.'); + setMealStatus('Saved. You can edit it from Diary or run analysis again.'); } catch (error) { setMealStatus(error.message, true); } finally { @@ -327,14 +464,100 @@ Image estimate: ${imageEstimate}` }, statusError = error; } - function clearDiary() { - if (!clearArmed) { - clearArmed = true; - return; - } - entries = []; + function editEntry(entry) { + editingEntryId = entry.id; + mealDate = entry.date; + mealTime = entry.time; + mealType = entry.mealType; + description = entry.description || ''; + measure = entry.measure || ''; + estimateDraft = { ...emptyEstimate(), ...entry }; + setMealStatus('Editing saved meal. Change details, then save edits or analyze again.'); + selectPage('log'); + } + + function saveMealEdits() { + if (!editingEntryId) return; + entries = entries.map(entry => entry.id === editingEntryId ? { + ...entry, + ...estimateDraft, + date: mealDate, + time: mealTime, + mealType, + description, + measure, + updatedAt: new Date().toISOString(), + } : entry); saveJson(entriesKey, entries); - clearArmed = false; + editingEntryId = ''; + estimateDraft = emptyEstimate(); + description = ''; + measure = ''; + setDefaultMealDateTime(); + setMealStatus('Edits saved.'); + } + + function cancelEdit() { + editingEntryId = ''; + estimateDraft = emptyEstimate(); + description = ''; + measure = ''; + selectedImageDataUrl = ''; + selectedImageName = ''; + setDefaultMealDateTime(); + setMealStatus(''); + } + + function toggleSelected(id) { + selectedEntryIds = selectedEntryIds.includes(id) ? selectedEntryIds.filter(item => item !== id) : [...selectedEntryIds, id]; + } + + function moveEntriesToTrash(ids) { + const idSet = new Set(ids); + const moving = entries.filter(entry => idSet.has(entry.id)).map(entry => ({ ...entry, trashedAt: new Date().toISOString() })); + if (!moving.length) return; + trash = [...moving, ...trash]; + entries = entries.filter(entry => !idSet.has(entry.id)); + selectedEntryIds = selectedEntryIds.filter(id => !idSet.has(id)); + saveJson(entriesKey, entries); + saveJson(trashKey, trash); + } + + function moveDayToTrash(date) { + moveEntriesToTrash(entries.filter(entry => entry.date === date).map(entry => entry.id)); + } + + function restoreEntry(id) { + const found = trash.find(entry => entry.id === id); + if (!found) return; + entries = [{ ...found, trashedAt: undefined }, ...entries]; + trash = trash.filter(entry => entry.id !== id); + saveJson(entriesKey, entries); + saveJson(trashKey, trash); + } + + async function generatePlan() { + planLoading = true; + planStatus = 'Generating plan...'; + planError = false; + try { + if (!settings.heightCm || !settings.weightKg) throw new Error('Enter height and current weight first.'); + planAdvice = await chatCompletion({ + model: settings.taskModel, + temperature: 0.2, + messages: [ + { role: 'system', content: 'Give safe, concise nutrition and weight-loss guidance. Do not diagnose disease. Recommend medical review for pregnancy, eating disorder history, chronic disease, or aggressive goals.' }, + { role: 'user', content: `Create a personalized weight-loss plan using these details: height ${settings.heightCm} cm, current weight ${settings.weightKg} kg, target weight ${settings.targetWeightKg || 'not set'} kg, activity ${settings.activityLevel}, pace ${settings.pace}. Include daily calorie range, protein target, habits, and safety notes.` }, + ], + }); + planStatus = 'Plan generated.'; + saveSettings(); + } catch (error) { + planStatus = error.message; + planError = true; + } finally { + planLoading = false; + } } async function drawCharts() { @@ -446,33 +669,29 @@ Image estimate: ${imageEstimate}` },
- +
-
-

Quick actions

+

Quick actions

Macros today

{:else if page === 'log'} -

Log meal

Backfill meals for any date and time.

-
-

Meal details

-
- - - - - - -
- {#if selectedImageDataUrl}Selected meal preview{/if} -
{#if status}

{status}

{/if}
-
-
+ {:else if page === 'analytics'}

Analytics

Seven-day calorie and produce trends.

Calories, last 7 days

Fruit and vegetables

{:else if page === 'diary'} -

Diary

Search and filter local meal entries.

{#if diaryRows.length}{#each diaryRows as entry}
{entry.mealName}
{entry.date} {entry.time} · {entry.mealType}
{entry.calories} kcal
P {entry.proteinGrams}g · C {entry.carbsGrams}g · F {entry.fatGrams}g · Fruit {Number(entry.fruitServings || 0).toFixed(1)} · Vegetables {Number(entry.vegetableServings || 0).toFixed(1)}
{#if entry.description}
{entry.description}
{/if}{#if entry.foodGroups}
Groups: {entry.foodGroups}
{/if}{#if entry.notes}
{entry.notes}
{/if}
{/each}{:else}
No meals match the current filters.
{/if}
- {:else if page === 'models'} -

Models

Choose from the models configured on the server. Model IDs are not typed manually.

Selected models

{#if modelStatus}

{modelStatus}

{/if}

Available models

{#each filteredModels as model}{/each}
+ + {:else if page === 'trash'} + {:else if page === 'settings'} -

Settings

Connection settings are managed on the server.

AI connection

API base URL and API key are read from environment variables.

Use the Models page to refresh and select available models.

+ {/if} @@ -521,17 +798,8 @@ Image estimate: ${imageEstimate}` }, {/if} diff --git a/web/src/DiaryPage.svelte b/web/src/DiaryPage.svelte new file mode 100644 index 0000000..aa6b923 --- /dev/null +++ b/web/src/DiaryPage.svelte @@ -0,0 +1,76 @@ + + +
+
+

{trashMode ? 'Trash' : 'Diary'}

+

{trashMode ? 'Restore meals moved out of the diary.' : 'Search, edit, select, or move meals to trash.'}

+
+ + {#if trashMode} +
+ {#if trash.length} + {#each trash as entry} +
+
+
{entry.mealName}
{entry.date} {entry.time} · {entry.mealType}
+ +
+
+ {/each} + {:else}
Trash is empty.
{/if} +
+ {:else} +
+
+ + + +
+
+
+ +
+ {#if diaryRows.length} + {#each diaryDays as day} +
+
+

{day}

+ +
+
+ {#each diaryRows.filter(entry => entry.date === day) as entry} +
+
+ +
{entry.calories} kcal
+
+
P {entry.proteinGrams}g · C {entry.carbsGrams}g · F {entry.fatGrams}g · Fruit {Number(entry.fruitServings || 0).toFixed(1)} · Vegetables {Number(entry.vegetableServings || 0).toFixed(1)}
+ {#if entry.description}
{entry.description}
{/if} + {#if entry.foodGroups}
Groups: {entry.foodGroups}
{/if} + {#if entry.notes}
{entry.notes}
{/if} +
+ {/each} +
+
+ {/each} + {:else}
No meals match the current filters.
{/if} +
+ {/if} +
diff --git a/web/src/MealForm.svelte b/web/src/MealForm.svelte new file mode 100644 index 0000000..a2e91cc --- /dev/null +++ b/web/src/MealForm.svelte @@ -0,0 +1,57 @@ + + +
+
+

{editingEntryId ? 'Edit meal' : 'Log meal'}

+

Backfill meals for any date and time. After analysis, edit the values or run analysis again.

+
+
+

Meal details

+
+ + + + + + +
+ {#if selectedImageDataUrl}Selected meal preview{/if} + {#if editingEntryId} +
+ + + + + + + + + +
+ {/if} +
+ + {#if editingEntryId}{/if} + {#if status}

{status}

{/if} +
+
+
diff --git a/web/src/SettingsPage.svelte b/web/src/SettingsPage.svelte new file mode 100644 index 0000000..677b29e --- /dev/null +++ b/web/src/SettingsPage.svelte @@ -0,0 +1,85 @@ + + +
+

Settings

Account, profile, and model controls.

+
+
+

Password

+
+ + + +
+ {#if passwordStatus}

{passwordStatus}

{/if} + {#if resetPasswordValue}
{resetPasswordValue}
{/if} +
+
+ +
+

Weight-loss plan

+
+ + + + + +
+ {#if planStatus}

{planStatus}

{/if} + {#if planAdvice}
{planAdvice}
{/if} +
+
+ +
+

Models

+
+
+ + +
+ {#if modelStatus}

{modelStatus}

{/if} +
+
Saved models
+
+ {#each models as model}
{model.name} {model.id}
{/each} +
+
+
+
+ +
event.key === 'Enter' && searchModels()}>
+
+ {#if discoveredModels.length}{#each discoveredModels as model}
{model.name} {model.id}
{/each}{:else}

Search to query the current provider. Results stay in this scroll panel.

{/if} +
+
+
+
+
+
diff --git a/web/src/app.css b/web/src/app.css index f51c733..2dd2d01 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -8,3 +8,14 @@ html { background: #f8fafc; } body { margin: 0; min-height: 100vh; font-family: var(--font-sans); color: #0f172a; } button, input, select, textarea { font: inherit; } canvas { max-width: 100%; } + +.card { border-radius: 0.9rem; border: 1px solid #e2e8f0; background: white; box-shadow: 0 1px 3px rgba(15,23,42,.06); overflow: hidden; } +.card-title { border-bottom: 1px solid #e2e8f0; background: #f8fafc; padding: 0.75rem 1rem; font-size: 0.9rem; font-weight: 800; color: #334155; } +.input { width: 100%; border-radius: 0.65rem; border: 1px solid #cbd5e1; padding: 0.6rem 0.75rem; font-size: 0.9rem; outline: none; } +.input:focus { border-color: #2563eb; box-shadow: 0 0 0 4px #dbeafe; } +.btn-primary { border-radius: 0.65rem; background: #2563eb; padding: 0.65rem 1rem; font-size: 0.9rem; font-weight: 800; color: white; } +.btn-primary:hover { background: #1d4ed8; } +.btn-primary:disabled { opacity: .55; } +.btn-secondary { border-radius: 0.65rem; border: 1px solid #cbd5e1; background: white; padding: 0.65rem 1rem; font-size: 0.9rem; font-weight: 800; color: #334155; } +.btn-secondary:hover { border-color: #2563eb; color: #2563eb; } +.empty-state { border-radius: 0.9rem; border: 1px dashed #cbd5e1; background: white; padding: 2rem; text-align: center; color: #64748b; } diff --git a/web/tests/app.spec.js b/web/tests/app.spec.js index e60ce80..cfdb084 100644 --- a/web/tests/app.spec.js +++ b/web/tests/app.spec.js @@ -14,12 +14,40 @@ async function mockModels(page) { defaultModel: 'claude-haiku-4.5', models: [ { id: 'claude-haiku-4.5', name: 'Claude Haiku 4.5' }, + ], + }), + }); + }); + + await page.route('**/api/models/search', async (route) => { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + count: 2, + models: [ { id: 'claude-sonnet-4.6', name: 'Claude Sonnet 4.6' }, { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' }, ], }), }); }); + + await page.route('**/api/models/add', async (route) => { + const body = route.request().postDataJSON(); + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ + ok: true, + defaultModel: 'claude-haiku-4.5', + models: [ + { id: 'claude-haiku-4.5', name: 'Claude Haiku 4.5' }, + { id: body.id, name: body.name || body.id }, + ], + }), + }); + }); } async function login(page) { @@ -60,7 +88,7 @@ test.describe('authenticated app', () => { await expect(page.locator('#status')).toContainText('Describe the meal or upload an image.'); }); - test('selects models from dropdowns, backfills a meal, updates charts, and clears diary', async ({ page }) => { + test('adds curated models in settings, backfills a meal, edits, trashes, and restores', async ({ page }) => { let calls = 0; await page.route('**/api/chat', async (route) => { calls += 1; @@ -75,9 +103,13 @@ test.describe('authenticated app', () => { }); }); - await page.getByRole('button', { name: 'Models', exact: true }).click(); + await page.getByRole('banner').getByRole('button', { name: 'Settings' }).click(); + await page.locator('#modelSearch').fill('gemini'); + await page.getByRole('button', { name: 'Search' }).click(); + await expect(page.getByText('Gemini 2.5 Flash')).toBeVisible(); + await page.getByRole('button', { name: '+' }).last().click(); await page.locator('#visionModel').selectOption('gemini-2.5-flash'); - await page.locator('#taskModel').selectOption('claude-sonnet-4.6'); + await page.locator('#taskModel').selectOption('gemini-2.5-flash'); await page.getByRole('button', { name: 'Save selected models' }).click(); await expect(page.locator('#modelStatus')).toContainText('Models saved.'); @@ -93,17 +125,25 @@ test.describe('authenticated app', () => { await page.getByRole('button', { name: 'Analyze and save' }).click(); await expect(page.locator('#status')).toContainText('Saved.'); - await page.getByRole('button', { name: 'Diary', exact: true }).click(); + await page.locator('nav').getByRole('button').filter({ hasText: 'Diary' }).click(); await expect(page.locator('.entry')).toContainText('Salmon rice bowl'); await expect(page.locator('.entry')).toContainText('2026-05-18 08:15 · Breakfast'); + await page.getByRole('button', { name: 'Edit' }).click(); + await expect(page.getByRole('heading', { name: 'Edit meal' })).toBeVisible(); + await page.getByLabel('Calories').fill('610'); + await page.getByRole('button', { name: 'Save edits' }).click(); - await page.getByRole('button', { name: 'Analytics' }).click(); + await page.locator('nav').getByRole('button').filter({ hasText: 'Analytics' }).click(); await expect(page.locator('#calorieChart')).toBeVisible(); await expect(page.locator('#produceChart')).toBeVisible(); - await page.getByRole('button', { name: 'Diary', exact: true }).click(); - await page.getByRole('button', { name: 'Clear diary' }).click(); - await page.getByRole('button', { name: 'Click again' }).click(); + await page.locator('nav').getByRole('button').filter({ hasText: 'Diary' }).click(); + await page.getByRole('checkbox').check(); + await page.getByRole('button', { name: 'Move selected to trash' }).click(); await expect(page.locator('#entries')).toContainText('No meals match the current filters.'); + await page.locator('nav').getByRole('button').filter({ hasText: 'Trash' }).click(); + await expect(page.locator('#trashEntries')).toContainText('Salmon rice bowl'); + await page.getByRole('button', { name: 'Restore' }).click(); + await expect(page.locator('#trashEntries')).toContainText('Trash is empty.'); }); });