From 12d59bb42f469aa378a63563d8562ed8473f72a0 Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Mon, 9 Feb 2026 01:49:19 -0700 Subject: [PATCH] Refactored a bunch of shit --- package.json | 14 +- pnpm-lock.yaml | 664 +++++++++--------- src/components/StatCard.astro | 25 + src/lib/formatTime.ts | 13 + src/lib/getCurrentTeam.ts | 24 + src/lib/validation.ts | 30 + src/pages/api/auth/passkey/login/finish.ts | 3 +- src/pages/api/auth/passkey/login/start.ts | 5 + src/pages/api/auth/passkey/register/finish.ts | 3 +- src/pages/api/auth/passkey/register/start.ts | 6 +- src/pages/api/auth/signup.ts | 15 +- src/pages/api/clients/[id]/delete.ts | 11 + src/pages/api/clients/[id]/update.ts | 31 + src/pages/api/clients/create.ts | 31 + src/pages/api/invoices/[id]/convert.ts | 5 + src/pages/api/invoices/[id]/generate.ts | 2 +- src/pages/api/invoices/[id]/import-time.ts | 81 +-- src/pages/api/invoices/[id]/items/add.ts | 6 + src/pages/api/invoices/[id]/status.ts | 7 + src/pages/api/invoices/[id]/update.ts | 9 + src/pages/api/invoices/delete.ts | 5 + src/pages/api/organizations/update-name.ts | 17 +- src/pages/api/reports/export.ts | 15 +- src/pages/api/team/invite.ts | 5 + src/pages/api/time-entries/manual.ts | 8 + src/pages/api/time-entries/start.ts | 6 +- src/pages/api/user/change-password.ts | 38 +- src/pages/dashboard/clients.astro | 21 +- src/pages/dashboard/clients/[id]/edit.astro | 19 +- src/pages/dashboard/clients/[id]/index.astro | 51 +- src/pages/dashboard/index.astro | 68 +- src/pages/dashboard/invoices/[id].astro | 20 +- src/pages/dashboard/invoices/index.astro | 79 +-- src/pages/dashboard/invoices/new.astro | 19 +- src/pages/dashboard/reports.astro | 87 +-- src/pages/dashboard/settings.astro | 4 +- src/pages/dashboard/team.astro | 17 +- src/pages/dashboard/team/settings.astro | 19 +- src/pages/dashboard/tracker.astro | 33 +- src/pages/uploads/[...path].ts | 6 +- 40 files changed, 844 insertions(+), 678 deletions(-) create mode 100644 src/components/StatCard.astro create mode 100644 src/lib/getCurrentTeam.ts diff --git a/package.json b/package.json index 27da648..30435e6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "chronus", "type": "module", - "version": "2.3.0", + "version": "2.4.0", "scripts": { "dev": "astro dev", "build": "astro build", @@ -13,7 +13,7 @@ }, "dependencies": { "@astrojs/check": "0.9.6", - "@astrojs/node": "10.0.0-beta.0", + "@astrojs/node": "10.0.0-beta.2", "@astrojs/vue": "6.0.0-beta.0", "@ceereals/vue-pdf": "^0.2.1", "@iconify/vue": "^5.0.0", @@ -21,23 +21,23 @@ "@simplewebauthn/browser": "^13.2.2", "@simplewebauthn/server": "^13.2.2", "@tailwindcss/vite": "^4.1.18", - "astro": "6.0.0-beta.6", + "astro": "6.0.0-beta.9", "astro-icon": "^1.1.5", "bcryptjs": "^3.0.3", "chart.js": "^4.5.1", - "daisyui": "^5.5.17", - "dotenv": "^17.2.3", + "daisyui": "^5.5.18", + "dotenv": "^17.2.4", "drizzle-orm": "0.45.1", "nanoid": "^5.1.6", "tailwindcss": "^4.1.18", "typescript": "^5.9.3", - "vue": "^3.5.27", + "vue": "^3.5.28", "vue-chartjs": "^5.3.3" }, "devDependencies": { "@catppuccin/daisyui": "^2.1.1", "@iconify-json/heroicons": "^1.2.3", "@react-pdf/types": "^2.9.2", - "drizzle-kit": "0.31.8" + "drizzle-kit": "0.31.9" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 724fa38..1284a4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,17 +12,17 @@ importers: specifier: 0.9.6 version: 0.9.6(prettier@3.8.1)(typescript@5.9.3) '@astrojs/node': - specifier: 10.0.0-beta.0 - version: 10.0.0-beta.0(astro@6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2)) + specifier: 10.0.0-beta.2 + version: 10.0.0-beta.2(astro@6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2)) '@astrojs/vue': specifier: 6.0.0-beta.0 - version: 6.0.0-beta.0(@types/node@25.2.0)(astro@6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(vue@3.5.27(typescript@5.9.3))(yaml@2.8.2) + version: 6.0.0-beta.0(@types/node@25.2.2)(astro@6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(vue@3.5.28(typescript@5.9.3))(yaml@2.8.2) '@ceereals/vue-pdf': specifier: ^0.2.1 - version: 0.2.1(vue@3.5.27(typescript@5.9.3)) + version: 0.2.1(vue@3.5.28(typescript@5.9.3)) '@iconify/vue': specifier: ^5.0.0 - version: 5.0.0(vue@3.5.27(typescript@5.9.3)) + version: 5.0.0(vue@3.5.28(typescript@5.9.3)) '@libsql/client': specifier: ^0.17.0 version: 0.17.0 @@ -34,10 +34,10 @@ importers: version: 13.2.2 '@tailwindcss/vite': specifier: ^4.1.18 - version: 4.1.18(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + version: 4.1.18(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) astro: - specifier: 6.0.0-beta.6 - version: 6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) + specifier: 6.0.0-beta.9 + version: 6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) astro-icon: specifier: ^1.1.5 version: 1.1.5 @@ -48,11 +48,11 @@ importers: specifier: ^4.5.1 version: 4.5.1 daisyui: - specifier: ^5.5.17 - version: 5.5.17 + specifier: ^5.5.18 + version: 5.5.18 dotenv: - specifier: ^17.2.3 - version: 17.2.3 + specifier: ^17.2.4 + version: 17.2.4 drizzle-orm: specifier: 0.45.1 version: 0.45.1(@libsql/client@0.17.0)(@types/better-sqlite3@7.6.13)(better-sqlite3@12.6.0) @@ -66,11 +66,11 @@ importers: specifier: ^5.9.3 version: 5.9.3 vue: - specifier: ^3.5.27 - version: 3.5.27(typescript@5.9.3) + specifier: ^3.5.28 + version: 3.5.28(typescript@5.9.3) vue-chartjs: specifier: ^5.3.3 - version: 5.3.3(chart.js@4.5.1)(vue@3.5.27(typescript@5.9.3)) + version: 5.3.3(chart.js@4.5.1)(vue@3.5.28(typescript@5.9.3)) devDependencies: '@catppuccin/daisyui': specifier: ^2.1.1 @@ -82,8 +82,8 @@ importers: specifier: ^2.9.2 version: 2.9.2 drizzle-kit: - specifier: 0.31.8 - version: 0.31.8 + specifier: 0.31.9 + version: 0.31.9 packages: @@ -105,11 +105,11 @@ packages: '@astrojs/compiler@0.0.0-render-script-20251003120459': resolution: {integrity: sha512-HWimO47p1zcg/H7/OtiABemJtvFxXDJ7r551Xkwq6c+FIZTps2/sIN1/qAEiuW5UmGChqaI+ILPMcSzFOWidSA==} - '@astrojs/compiler@2.13.0': - resolution: {integrity: sha512-mqVORhUJViA28fwHYaWmsXSzLO9osbdZ5ImUfxBarqsYdMlPbqAqGJCxsNzvppp1BEzc1mJNjOVvQqeDN8Vspw==} + '@astrojs/compiler@2.13.1': + resolution: {integrity: sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg==} - '@astrojs/internal-helpers@0.7.5': - resolution: {integrity: sha512-vreGnYSSKhAjFJCWAwe/CNhONvoc5lokxtRoZims+0wa3KbHBdPHSSthJsKxPd8d/aic6lWKpRTYGY/hsgK6EA==} + '@astrojs/internal-helpers@0.8.0-beta.0': + resolution: {integrity: sha512-fP5ZSwFHsJu4NMHhvzfKLUIuXwQ0OdfKPkpQAxRNFIKpiwolTOdYEvP0RGAlnhRfVciUYZNnwc1dMIFvv1lUvw==} '@astrojs/language-server@2.16.3': resolution: {integrity: sha512-yO5K7RYCMXUfeDlnU6UnmtnoXzpuQc0yhlaCNZ67k1C/MiwwwvMZz+LGa+H35c49w5QBfvtr4w4Zcf5PcH8uYA==} @@ -123,11 +123,11 @@ packages: prettier-plugin-astro: optional: true - '@astrojs/markdown-remark@7.0.0-beta.3': - resolution: {integrity: sha512-QwkD+ZrcHzyR80Tx79uhudn3gOM3jZNDKk7Ig0Y3SvryjV4sYz+9HGdp6kmxQ7I01Kz19PLxKX/Qh3SalacMGg==} + '@astrojs/markdown-remark@7.0.0-beta.5': + resolution: {integrity: sha512-WD2SdhgNWPxsgsHg4oN+imw4+BEDoJ3vJOIGowL0o4yDeyeZsfFOoMbffPKRqG2boFXqRMpauZO7Wfq9w/WfUA==} - '@astrojs/node@10.0.0-beta.0': - resolution: {integrity: sha512-VkSzpIaTu7fa2wLZ1RsdYVJet3DRYFCxbL/aXJ+EQmwsrs8HcXaq3wat7ujFb5rtT5hUOSf8oRIvdgwMLAbxBg==} + '@astrojs/node@10.0.0-beta.2': + resolution: {integrity: sha512-7N+bQb/NT4dXpEyEHr81VVTEGfcyO1QDa31lkSWfgh7sLQYCjNp6ASSpnSo04574ZJHjQyCeJsyxjfSoHs8evQ==} peerDependencies: astro: ^6.0.0-alpha.0 @@ -161,8 +161,8 @@ packages: resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/generator@7.29.0': - resolution: {integrity: sha512-vSH118/wwM/pLR38g/Sgk05sNtro6TlTJKuiMXDaZqPUfjTFcudpCOt00IhOfj+1BFAX+UFAlzCU+6WXr3GLFQ==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} '@babel/helper-annotate-as-pure@7.27.3': @@ -351,8 +351,8 @@ packages: cpu: [ppc64] os: [aix] - '@esbuild/aix-ppc64@0.27.2': - resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + '@esbuild/aix-ppc64@0.27.3': + resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} engines: {node: '>=18'} cpu: [ppc64] os: [aix] @@ -369,8 +369,8 @@ packages: cpu: [arm64] os: [android] - '@esbuild/android-arm64@0.27.2': - resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + '@esbuild/android-arm64@0.27.3': + resolution: {integrity: sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg==} engines: {node: '>=18'} cpu: [arm64] os: [android] @@ -387,8 +387,8 @@ packages: cpu: [arm] os: [android] - '@esbuild/android-arm@0.27.2': - resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + '@esbuild/android-arm@0.27.3': + resolution: {integrity: sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA==} engines: {node: '>=18'} cpu: [arm] os: [android] @@ -405,8 +405,8 @@ packages: cpu: [x64] os: [android] - '@esbuild/android-x64@0.27.2': - resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + '@esbuild/android-x64@0.27.3': + resolution: {integrity: sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ==} engines: {node: '>=18'} cpu: [x64] os: [android] @@ -423,8 +423,8 @@ packages: cpu: [arm64] os: [darwin] - '@esbuild/darwin-arm64@0.27.2': - resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + '@esbuild/darwin-arm64@0.27.3': + resolution: {integrity: sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg==} engines: {node: '>=18'} cpu: [arm64] os: [darwin] @@ -441,8 +441,8 @@ packages: cpu: [x64] os: [darwin] - '@esbuild/darwin-x64@0.27.2': - resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + '@esbuild/darwin-x64@0.27.3': + resolution: {integrity: sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg==} engines: {node: '>=18'} cpu: [x64] os: [darwin] @@ -459,8 +459,8 @@ packages: cpu: [arm64] os: [freebsd] - '@esbuild/freebsd-arm64@0.27.2': - resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + '@esbuild/freebsd-arm64@0.27.3': + resolution: {integrity: sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w==} engines: {node: '>=18'} cpu: [arm64] os: [freebsd] @@ -477,8 +477,8 @@ packages: cpu: [x64] os: [freebsd] - '@esbuild/freebsd-x64@0.27.2': - resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + '@esbuild/freebsd-x64@0.27.3': + resolution: {integrity: sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA==} engines: {node: '>=18'} cpu: [x64] os: [freebsd] @@ -495,8 +495,8 @@ packages: cpu: [arm64] os: [linux] - '@esbuild/linux-arm64@0.27.2': - resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + '@esbuild/linux-arm64@0.27.3': + resolution: {integrity: sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg==} engines: {node: '>=18'} cpu: [arm64] os: [linux] @@ -513,8 +513,8 @@ packages: cpu: [arm] os: [linux] - '@esbuild/linux-arm@0.27.2': - resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + '@esbuild/linux-arm@0.27.3': + resolution: {integrity: sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw==} engines: {node: '>=18'} cpu: [arm] os: [linux] @@ -531,8 +531,8 @@ packages: cpu: [ia32] os: [linux] - '@esbuild/linux-ia32@0.27.2': - resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + '@esbuild/linux-ia32@0.27.3': + resolution: {integrity: sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg==} engines: {node: '>=18'} cpu: [ia32] os: [linux] @@ -549,8 +549,8 @@ packages: cpu: [loong64] os: [linux] - '@esbuild/linux-loong64@0.27.2': - resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + '@esbuild/linux-loong64@0.27.3': + resolution: {integrity: sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA==} engines: {node: '>=18'} cpu: [loong64] os: [linux] @@ -567,8 +567,8 @@ packages: cpu: [mips64el] os: [linux] - '@esbuild/linux-mips64el@0.27.2': - resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + '@esbuild/linux-mips64el@0.27.3': + resolution: {integrity: sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw==} engines: {node: '>=18'} cpu: [mips64el] os: [linux] @@ -585,8 +585,8 @@ packages: cpu: [ppc64] os: [linux] - '@esbuild/linux-ppc64@0.27.2': - resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + '@esbuild/linux-ppc64@0.27.3': + resolution: {integrity: sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA==} engines: {node: '>=18'} cpu: [ppc64] os: [linux] @@ -603,8 +603,8 @@ packages: cpu: [riscv64] os: [linux] - '@esbuild/linux-riscv64@0.27.2': - resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + '@esbuild/linux-riscv64@0.27.3': + resolution: {integrity: sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ==} engines: {node: '>=18'} cpu: [riscv64] os: [linux] @@ -621,8 +621,8 @@ packages: cpu: [s390x] os: [linux] - '@esbuild/linux-s390x@0.27.2': - resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + '@esbuild/linux-s390x@0.27.3': + resolution: {integrity: sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw==} engines: {node: '>=18'} cpu: [s390x] os: [linux] @@ -639,8 +639,8 @@ packages: cpu: [x64] os: [linux] - '@esbuild/linux-x64@0.27.2': - resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + '@esbuild/linux-x64@0.27.3': + resolution: {integrity: sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA==} engines: {node: '>=18'} cpu: [x64] os: [linux] @@ -651,8 +651,8 @@ packages: cpu: [arm64] os: [netbsd] - '@esbuild/netbsd-arm64@0.27.2': - resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + '@esbuild/netbsd-arm64@0.27.3': + resolution: {integrity: sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA==} engines: {node: '>=18'} cpu: [arm64] os: [netbsd] @@ -669,8 +669,8 @@ packages: cpu: [x64] os: [netbsd] - '@esbuild/netbsd-x64@0.27.2': - resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + '@esbuild/netbsd-x64@0.27.3': + resolution: {integrity: sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA==} engines: {node: '>=18'} cpu: [x64] os: [netbsd] @@ -681,8 +681,8 @@ packages: cpu: [arm64] os: [openbsd] - '@esbuild/openbsd-arm64@0.27.2': - resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + '@esbuild/openbsd-arm64@0.27.3': + resolution: {integrity: sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw==} engines: {node: '>=18'} cpu: [arm64] os: [openbsd] @@ -699,8 +699,8 @@ packages: cpu: [x64] os: [openbsd] - '@esbuild/openbsd-x64@0.27.2': - resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + '@esbuild/openbsd-x64@0.27.3': + resolution: {integrity: sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ==} engines: {node: '>=18'} cpu: [x64] os: [openbsd] @@ -711,8 +711,8 @@ packages: cpu: [arm64] os: [openharmony] - '@esbuild/openharmony-arm64@0.27.2': - resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + '@esbuild/openharmony-arm64@0.27.3': + resolution: {integrity: sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g==} engines: {node: '>=18'} cpu: [arm64] os: [openharmony] @@ -729,8 +729,8 @@ packages: cpu: [x64] os: [sunos] - '@esbuild/sunos-x64@0.27.2': - resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + '@esbuild/sunos-x64@0.27.3': + resolution: {integrity: sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA==} engines: {node: '>=18'} cpu: [x64] os: [sunos] @@ -747,8 +747,8 @@ packages: cpu: [arm64] os: [win32] - '@esbuild/win32-arm64@0.27.2': - resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + '@esbuild/win32-arm64@0.27.3': + resolution: {integrity: sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA==} engines: {node: '>=18'} cpu: [arm64] os: [win32] @@ -765,8 +765,8 @@ packages: cpu: [ia32] os: [win32] - '@esbuild/win32-ia32@0.27.2': - resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + '@esbuild/win32-ia32@0.27.3': + resolution: {integrity: sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q==} engines: {node: '>=18'} cpu: [ia32] os: [win32] @@ -783,8 +783,8 @@ packages: cpu: [x64] os: [win32] - '@esbuild/win32-x64@0.27.2': - resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + '@esbuild/win32-x64@0.27.3': + resolution: {integrity: sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA==} engines: {node: '>=18'} cpu: [x64] os: [win32] @@ -1108,8 +1108,8 @@ packages: '@react-pdf/types@2.9.2': resolution: {integrity: sha512-dufvpKId9OajLLbgn9q7VLUmyo1Jf+iyGk2ZHmCL8nIDtL8N1Ejh9TH7+pXXrR0tdie1nmnEb5Bz9U7g4hI4/g==} - '@rolldown/pluginutils@1.0.0-rc.2': - resolution: {integrity: sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw==} + '@rolldown/pluginutils@1.0.0-rc.3': + resolution: {integrity: sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==} '@rollup/pluginutils@5.3.0': resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} @@ -1398,8 +1398,8 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} - '@types/node@25.2.0': - resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} + '@types/node@25.2.2': + resolution: {integrity: sha512-BkmoP5/FhRYek5izySdkOneRyXYN35I860MFAGupTdebyE66uZaR+bXLHq8k4DirE5DwQi3NuhvRU1jqTVwUrQ==} '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} @@ -1472,20 +1472,20 @@ packages: peerDependencies: '@babel/core': ^7.0.0-0 - '@vue/compiler-core@3.5.27': - resolution: {integrity: sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==} + '@vue/compiler-core@3.5.28': + resolution: {integrity: sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==} - '@vue/compiler-dom@3.5.27': - resolution: {integrity: sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==} + '@vue/compiler-dom@3.5.28': + resolution: {integrity: sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==} - '@vue/compiler-sfc@3.5.27': - resolution: {integrity: sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==} + '@vue/compiler-sfc@3.5.28': + resolution: {integrity: sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==} - '@vue/compiler-ssr@3.5.27': - resolution: {integrity: sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==} + '@vue/compiler-ssr@3.5.28': + resolution: {integrity: sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==} - '@vue/devtools-api@8.0.5': - resolution: {integrity: sha512-DgVcW8H/Nral7LgZEecYFFYXnAvGuN9C3L3DtWekAncFBedBczpNW8iHKExfaM559Zm8wQWrwtYZ9lXthEHtDw==} + '@vue/devtools-api@8.0.6': + resolution: {integrity: sha512-+lGBI+WTvJmnU2FZqHhEB8J1DXcvNlDeEalz77iYgOdY1jTj1ipSBaKj3sRhYcy+kqA8v/BSuvOz1XJucfQmUA==} '@vue/devtools-core@7.7.9': resolution: {integrity: sha512-48jrBSwG4GVQRvVeeXn9p9+dlx+ISgasM7SxZZKczseohB0cBz+ITKr4YbLWjmJdy45UHL7UMPlR4Y0CWTRcSQ==} @@ -1495,31 +1495,31 @@ packages: '@vue/devtools-kit@7.7.9': resolution: {integrity: sha512-PyQ6odHSgiDVd4hnTP+aDk2X4gl2HmLDfiyEnn3/oV+ckFDuswRs4IbBT7vacMuGdwY/XemxBoh302ctbsptuA==} - '@vue/devtools-kit@8.0.5': - resolution: {integrity: sha512-q2VV6x1U3KJMTQPUlRMyWEKVbcHuxhqJdSr6Jtjz5uAThAIrfJ6WVZdGZm5cuO63ZnSUz0RCsVwiUUb0mDV0Yg==} + '@vue/devtools-kit@8.0.6': + resolution: {integrity: sha512-9zXZPTJW72OteDXeSa5RVML3zWDCRcO5t77aJqSs228mdopYj5AiTpihozbsfFJ0IodfNs7pSgOGO3qfCuxDtw==} '@vue/devtools-shared@7.7.9': resolution: {integrity: sha512-iWAb0v2WYf0QWmxCGy0seZNDPdO3Sp5+u78ORnyeonS6MT4PC7VPrryX2BpMJrwlDeaZ6BD4vP4XKjK0SZqaeA==} - '@vue/devtools-shared@8.0.5': - resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==} + '@vue/devtools-shared@8.0.6': + resolution: {integrity: sha512-Pp1JylTqlgMJvxW6MGyfTF8vGvlBSCAvMFaDCYa82Mgw7TT5eE5kkHgDvmOGHWeJE4zIDfCpCxHapsK2LtIAJg==} - '@vue/reactivity@3.5.27': - resolution: {integrity: sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==} + '@vue/reactivity@3.5.28': + resolution: {integrity: sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==} - '@vue/runtime-core@3.5.27': - resolution: {integrity: sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==} + '@vue/runtime-core@3.5.28': + resolution: {integrity: sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==} - '@vue/runtime-dom@3.5.27': - resolution: {integrity: sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==} + '@vue/runtime-dom@3.5.28': + resolution: {integrity: sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==} - '@vue/server-renderer@3.5.27': - resolution: {integrity: sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==} + '@vue/server-renderer@3.5.28': + resolution: {integrity: sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==} peerDependencies: - vue: 3.5.27 + vue: 3.5.28 - '@vue/shared@3.5.27': - resolution: {integrity: sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==} + '@vue/shared@3.5.28': + resolution: {integrity: sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==} '@vueuse/core@13.9.0': resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} @@ -1596,8 +1596,8 @@ packages: astro-icon@1.1.5: resolution: {integrity: sha512-CJYS5nWOw9jz4RpGWmzNQY7D0y2ZZacH7atL2K9DeJXJVaz7/5WrxeyIxO8KASk1jCM96Q4LjRx/F3R+InjJrw==} - astro@6.0.0-beta.6: - resolution: {integrity: sha512-kh2v4n15z0es1fZMtadbfNDApdJivSKgMqCPHixro5lHTpte++4fH0z5AC/zqhkcUGztreRFuZMnvP0ZN1/uhQ==} + astro@6.0.0-beta.9: + resolution: {integrity: sha512-hWQNloN6eFJiFYxnrWn4Vi7X/5v6IabgKOa4dqpW1lHAKdlzZHvxHXOUhm4JZUMG4v9kKdvSWamm7XBmTcCltw==} engines: {node: ^20.19.1 || >=22.12.0, npm: '>=9.6.5', pnpm: '>=7.1.0'} hasBin: true @@ -1693,8 +1693,8 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} - caniuse-lite@1.0.30001767: - resolution: {integrity: sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==} + caniuse-lite@1.0.30001769: + resolution: {integrity: sha512-BCfFL1sHijQlBGWBMuJyhZUhzo7wer5sVj9hqekB/7xn0Ypy+pER/edCYQm4exbXj4WiySGp40P8UuTh6w1srg==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -1794,8 +1794,8 @@ packages: confbox@0.1.8: resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} - confbox@0.2.2: - resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -1855,8 +1855,8 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - daisyui@5.5.17: - resolution: {integrity: sha512-Y8QWps/990Epp0Gn+7ReeALSXgwrd3W36waokJvHgqUdYx6t2sj0e1krW3+YqviBa57XTJqHJNTt8HMvcODL2Q==} + daisyui@5.5.18: + resolution: {integrity: sha512-VVzjpOitMGB6DWIBeRSapbjdOevFqyzpk9u5Um6a4tyId3JFrU5pbtF0vgjXDth76mJZbueN/j9Ok03SPrh/og==} data-uri-to-buffer@4.0.1: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} @@ -1953,12 +1953,12 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dotenv@17.2.3: - resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + dotenv@17.2.4: + resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==} engines: {node: '>=12'} - drizzle-kit@0.31.8: - resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} + drizzle-kit@0.31.9: + resolution: {integrity: sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==} hasBin: true drizzle-orm@0.45.1: @@ -2064,8 +2064,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.283: - resolution: {integrity: sha512-3vifjt1HgrGW/h76UEeny+adYApveS9dH2h3p57JYzBSXJIKUJAvtmIytDKjcSCt9xHfrNCFJ7gts6vkhuq++w==} + electron-to-chromium@1.5.286: + resolution: {integrity: sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==} emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} @@ -2089,8 +2089,8 @@ packages: end-of-stream@1.4.5: resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - enhanced-resolve@5.18.4: - resolution: {integrity: sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==} + enhanced-resolve@5.19.0: + resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==} engines: {node: '>=10.13.0'} entities@4.5.0: @@ -2138,8 +2138,8 @@ packages: engines: {node: '>=18'} hasBin: true - esbuild@0.27.2: - resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + esbuild@0.27.3: + resolution: {integrity: sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg==} engines: {node: '>=18'} hasBin: true @@ -2288,8 +2288,8 @@ packages: resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} engines: {node: '>=18'} - get-tsconfig@4.13.1: - resolution: {integrity: sha512-EoY1N2xCn44xU6750Sx7OjOIT59FkmstNc3X6y5xpz7D5cBtZRe/3pSlTkDJgqsOk3WwZPkWfonhhUJfttQo3w==} + get-tsconfig@4.13.6: + resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} @@ -2621,8 +2621,8 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - magicast@0.5.1: - resolution: {integrity: sha512-xrHS24IxaLrvuo613F719wvOIv9xPHFWQHuvGUBmPnCA/3MQxKI3b+r7n1jAoDHmsbC5bRhTZYR77invLAxVnw==} + magicast@0.5.2: + resolution: {integrity: sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==} markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} @@ -2908,8 +2908,8 @@ packages: resolution: {integrity: sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==} engines: {node: '>=8'} - p-limit@7.2.0: - resolution: {integrity: sha512-ATHLtwoTNDloHRFFxFJdHnG6n2WUeFjaR8XQMFdKIv0xkXjrER8/iG9iu265jOM95zXHAfv9oTkqhrfbIzosrQ==} + p-limit@7.3.0: + resolution: {integrity: sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==} engines: {node: '>=20'} p-queue@9.1.0: @@ -3168,8 +3168,8 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.7.3: - resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} engines: {node: '>=10'} hasBin: true @@ -3407,8 +3407,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.20.0: - resolution: {integrity: sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ==} + undici@7.21.0: + resolution: {integrity: sha512-Hn2tCQpoDt1wv23a68Ctc8Cr/BHpUSfaPYrkajTXOS9IKpxVRx/X5m1K2YkbK2ipgZgxXSgsUinl3x+2YdSSfg==} engines: {node: '>=20.18.1'} unicode-properties@1.4.1: @@ -3714,8 +3714,8 @@ packages: chart.js: ^4.1.1 vue: ^3.0.0-0 || ^2.7.0 - vue@3.5.27: - resolution: {integrity: sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==} + vue@3.5.28: + resolution: {integrity: sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==} peerDependencies: typescript: '*' peerDependenciesMeta: @@ -3831,8 +3831,8 @@ packages: resolution: {integrity: sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==} engines: {node: '>=12.20'} - yocto-spinner@1.0.0: - resolution: {integrity: sha512-VPX8P/+Z2Fnpx8PC/JELbxp3QRrBxjAekio6yulGtA5gKt9YyRc5ycCb+NHgZCbZ0kx9KxwZp7gC6UlrCcCdSQ==} + yocto-spinner@1.1.0: + resolution: {integrity: sha512-/BY0AUXnS7IKO354uLLA2eRcWiqDifEbd6unXCsOxkFDAkhgUL3PH9X2bFoaU0YchnDXsF+iKleeTLJGckbXfA==} engines: {node: '>=18.19'} yoctocolors@2.1.2: @@ -3872,13 +3872,13 @@ snapshots: '@astrojs/compiler@0.0.0-render-script-20251003120459': {} - '@astrojs/compiler@2.13.0': {} + '@astrojs/compiler@2.13.1': {} - '@astrojs/internal-helpers@0.7.5': {} + '@astrojs/internal-helpers@0.8.0-beta.0': {} '@astrojs/language-server@2.16.3(prettier@3.8.1)(typescript@5.9.3)': dependencies: - '@astrojs/compiler': 2.13.0 + '@astrojs/compiler': 2.13.1 '@astrojs/yaml2ts': 0.2.2 '@jridgewell/sourcemap-codec': 1.5.5 '@volar/kit': 2.4.28(typescript@5.9.3) @@ -3901,9 +3901,9 @@ snapshots: transitivePeerDependencies: - typescript - '@astrojs/markdown-remark@7.0.0-beta.3': + '@astrojs/markdown-remark@7.0.0-beta.5': dependencies: - '@astrojs/internal-helpers': 0.7.5 + '@astrojs/internal-helpers': 0.8.0-beta.0 '@astrojs/prism': 4.0.0-beta.2 github-slugger: 2.0.0 hast-util-from-html: 2.0.3 @@ -3926,10 +3926,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/node@10.0.0-beta.0(astro@6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2))': + '@astrojs/node@10.0.0-beta.2(astro@6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2))': dependencies: - '@astrojs/internal-helpers': 0.7.5 - astro: 6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) + '@astrojs/internal-helpers': 0.8.0-beta.0 + astro: 6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) send: 1.2.1 server-destroy: 1.0.1 transitivePeerDependencies: @@ -3951,15 +3951,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@astrojs/vue@6.0.0-beta.0(@types/node@25.2.0)(astro@6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(vue@3.5.27(typescript@5.9.3))(yaml@2.8.2)': + '@astrojs/vue@6.0.0-beta.0(@types/node@25.2.2)(astro@6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(vue@3.5.28(typescript@5.9.3))(yaml@2.8.2)': dependencies: - '@vitejs/plugin-vue': 5.2.4(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) - '@vitejs/plugin-vue-jsx': 4.2.0(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) - '@vue/compiler-sfc': 3.5.27 - astro: 6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vite-plugin-vue-devtools: 7.7.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) - vue: 3.5.27(typescript@5.9.3) + '@vitejs/plugin-vue': 5.2.4(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) + '@vitejs/plugin-vue-jsx': 4.2.0(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) + '@vue/compiler-sfc': 3.5.28 + astro: 6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite-plugin-vue-devtools: 7.7.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) + vue: 3.5.28(typescript@5.9.3) transitivePeerDependencies: - '@nuxt/kit' - '@types/node' @@ -3991,7 +3991,7 @@ snapshots: '@babel/core@7.29.0': dependencies: '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-compilation-targets': 7.28.6 '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helpers': 7.28.6 @@ -4008,7 +4008,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@babel/generator@7.29.0': + '@babel/generator@7.29.1': dependencies: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 @@ -4159,7 +4159,7 @@ snapshots: '@babel/traverse@7.29.0': dependencies: '@babel/code-frame': 7.29.0 - '@babel/generator': 7.29.0 + '@babel/generator': 7.29.1 '@babel/helper-globals': 7.28.0 '@babel/parser': 7.29.0 '@babel/template': 7.28.6 @@ -4184,7 +4184,7 @@ snapshots: '@catppuccin/palette@1.7.1': {} - '@ceereals/vue-pdf@0.2.1(vue@3.5.27(typescript@5.9.3))': + '@ceereals/vue-pdf@0.2.1(vue@3.5.28(typescript@5.9.3))': dependencies: '@react-pdf/fns': 3.1.2 '@react-pdf/font': 4.0.4 @@ -4192,11 +4192,11 @@ snapshots: '@react-pdf/pdfkit': 4.1.0 '@react-pdf/primitives': 4.1.1 '@react-pdf/render': 4.3.2 - '@vue/devtools-api': 8.0.5 - '@vueuse/core': 13.9.0(vue@3.5.27(typescript@5.9.3)) + '@vue/devtools-api': 8.0.6 + '@vueuse/core': 13.9.0(vue@3.5.28(typescript@5.9.3)) defu: 6.1.4 patch-package: 8.0.1 - vue: 3.5.27(typescript@5.9.3) + vue: 3.5.28(typescript@5.9.3) optionalDependencies: '@rollup/rollup-linux-x64-gnu': 4.57.1 @@ -4238,12 +4238,12 @@ snapshots: '@esbuild-kit/esm-loader@2.6.5': dependencies: '@esbuild-kit/core-utils': 3.3.2 - get-tsconfig: 4.13.1 + get-tsconfig: 4.13.6 '@esbuild/aix-ppc64@0.25.12': optional: true - '@esbuild/aix-ppc64@0.27.2': + '@esbuild/aix-ppc64@0.27.3': optional: true '@esbuild/android-arm64@0.18.20': @@ -4252,7 +4252,7 @@ snapshots: '@esbuild/android-arm64@0.25.12': optional: true - '@esbuild/android-arm64@0.27.2': + '@esbuild/android-arm64@0.27.3': optional: true '@esbuild/android-arm@0.18.20': @@ -4261,7 +4261,7 @@ snapshots: '@esbuild/android-arm@0.25.12': optional: true - '@esbuild/android-arm@0.27.2': + '@esbuild/android-arm@0.27.3': optional: true '@esbuild/android-x64@0.18.20': @@ -4270,7 +4270,7 @@ snapshots: '@esbuild/android-x64@0.25.12': optional: true - '@esbuild/android-x64@0.27.2': + '@esbuild/android-x64@0.27.3': optional: true '@esbuild/darwin-arm64@0.18.20': @@ -4279,7 +4279,7 @@ snapshots: '@esbuild/darwin-arm64@0.25.12': optional: true - '@esbuild/darwin-arm64@0.27.2': + '@esbuild/darwin-arm64@0.27.3': optional: true '@esbuild/darwin-x64@0.18.20': @@ -4288,7 +4288,7 @@ snapshots: '@esbuild/darwin-x64@0.25.12': optional: true - '@esbuild/darwin-x64@0.27.2': + '@esbuild/darwin-x64@0.27.3': optional: true '@esbuild/freebsd-arm64@0.18.20': @@ -4297,7 +4297,7 @@ snapshots: '@esbuild/freebsd-arm64@0.25.12': optional: true - '@esbuild/freebsd-arm64@0.27.2': + '@esbuild/freebsd-arm64@0.27.3': optional: true '@esbuild/freebsd-x64@0.18.20': @@ -4306,7 +4306,7 @@ snapshots: '@esbuild/freebsd-x64@0.25.12': optional: true - '@esbuild/freebsd-x64@0.27.2': + '@esbuild/freebsd-x64@0.27.3': optional: true '@esbuild/linux-arm64@0.18.20': @@ -4315,7 +4315,7 @@ snapshots: '@esbuild/linux-arm64@0.25.12': optional: true - '@esbuild/linux-arm64@0.27.2': + '@esbuild/linux-arm64@0.27.3': optional: true '@esbuild/linux-arm@0.18.20': @@ -4324,7 +4324,7 @@ snapshots: '@esbuild/linux-arm@0.25.12': optional: true - '@esbuild/linux-arm@0.27.2': + '@esbuild/linux-arm@0.27.3': optional: true '@esbuild/linux-ia32@0.18.20': @@ -4333,7 +4333,7 @@ snapshots: '@esbuild/linux-ia32@0.25.12': optional: true - '@esbuild/linux-ia32@0.27.2': + '@esbuild/linux-ia32@0.27.3': optional: true '@esbuild/linux-loong64@0.18.20': @@ -4342,7 +4342,7 @@ snapshots: '@esbuild/linux-loong64@0.25.12': optional: true - '@esbuild/linux-loong64@0.27.2': + '@esbuild/linux-loong64@0.27.3': optional: true '@esbuild/linux-mips64el@0.18.20': @@ -4351,7 +4351,7 @@ snapshots: '@esbuild/linux-mips64el@0.25.12': optional: true - '@esbuild/linux-mips64el@0.27.2': + '@esbuild/linux-mips64el@0.27.3': optional: true '@esbuild/linux-ppc64@0.18.20': @@ -4360,7 +4360,7 @@ snapshots: '@esbuild/linux-ppc64@0.25.12': optional: true - '@esbuild/linux-ppc64@0.27.2': + '@esbuild/linux-ppc64@0.27.3': optional: true '@esbuild/linux-riscv64@0.18.20': @@ -4369,7 +4369,7 @@ snapshots: '@esbuild/linux-riscv64@0.25.12': optional: true - '@esbuild/linux-riscv64@0.27.2': + '@esbuild/linux-riscv64@0.27.3': optional: true '@esbuild/linux-s390x@0.18.20': @@ -4378,7 +4378,7 @@ snapshots: '@esbuild/linux-s390x@0.25.12': optional: true - '@esbuild/linux-s390x@0.27.2': + '@esbuild/linux-s390x@0.27.3': optional: true '@esbuild/linux-x64@0.18.20': @@ -4387,13 +4387,13 @@ snapshots: '@esbuild/linux-x64@0.25.12': optional: true - '@esbuild/linux-x64@0.27.2': + '@esbuild/linux-x64@0.27.3': optional: true '@esbuild/netbsd-arm64@0.25.12': optional: true - '@esbuild/netbsd-arm64@0.27.2': + '@esbuild/netbsd-arm64@0.27.3': optional: true '@esbuild/netbsd-x64@0.18.20': @@ -4402,13 +4402,13 @@ snapshots: '@esbuild/netbsd-x64@0.25.12': optional: true - '@esbuild/netbsd-x64@0.27.2': + '@esbuild/netbsd-x64@0.27.3': optional: true '@esbuild/openbsd-arm64@0.25.12': optional: true - '@esbuild/openbsd-arm64@0.27.2': + '@esbuild/openbsd-arm64@0.27.3': optional: true '@esbuild/openbsd-x64@0.18.20': @@ -4417,13 +4417,13 @@ snapshots: '@esbuild/openbsd-x64@0.25.12': optional: true - '@esbuild/openbsd-x64@0.27.2': + '@esbuild/openbsd-x64@0.27.3': optional: true '@esbuild/openharmony-arm64@0.25.12': optional: true - '@esbuild/openharmony-arm64@0.27.2': + '@esbuild/openharmony-arm64@0.27.3': optional: true '@esbuild/sunos-x64@0.18.20': @@ -4432,7 +4432,7 @@ snapshots: '@esbuild/sunos-x64@0.25.12': optional: true - '@esbuild/sunos-x64@0.27.2': + '@esbuild/sunos-x64@0.27.3': optional: true '@esbuild/win32-arm64@0.18.20': @@ -4441,7 +4441,7 @@ snapshots: '@esbuild/win32-arm64@0.25.12': optional: true - '@esbuild/win32-arm64@0.27.2': + '@esbuild/win32-arm64@0.27.3': optional: true '@esbuild/win32-ia32@0.18.20': @@ -4450,7 +4450,7 @@ snapshots: '@esbuild/win32-ia32@0.25.12': optional: true - '@esbuild/win32-ia32@0.27.2': + '@esbuild/win32-ia32@0.27.3': optional: true '@esbuild/win32-x64@0.18.20': @@ -4459,7 +4459,7 @@ snapshots: '@esbuild/win32-x64@0.25.12': optional: true - '@esbuild/win32-x64@0.27.2': + '@esbuild/win32-x64@0.27.3': optional: true '@hexagon/base64@1.1.28': {} @@ -4497,10 +4497,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@iconify/vue@5.0.0(vue@3.5.27(typescript@5.9.3))': + '@iconify/vue@5.0.0(vue@3.5.28(typescript@5.9.3))': dependencies: '@iconify/types': 2.0.0 - vue: 3.5.27(typescript@5.9.3) + vue: 3.5.28(typescript@5.9.3) '@img/colour@1.0.0': optional: true @@ -4868,7 +4868,7 @@ snapshots: '@react-pdf/primitives': 4.1.1 '@react-pdf/stylesheet': 6.1.2 - '@rolldown/pluginutils@1.0.0-rc.2': {} + '@rolldown/pluginutils@1.0.0-rc.3': {} '@rollup/pluginutils@5.3.0(rollup@4.57.1)': dependencies: @@ -5010,7 +5010,7 @@ snapshots: '@tailwindcss/node@4.1.18': dependencies: '@jridgewell/remapping': 2.3.5 - enhanced-resolve: 5.18.4 + enhanced-resolve: 5.19.0 jiti: 2.6.1 lightningcss: 1.30.2 magic-string: 0.30.21 @@ -5068,18 +5068,18 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.1.18 '@tailwindcss/oxide-win32-x64-msvc': 4.1.18 - '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': + '@tailwindcss/vite@4.1.18(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))': dependencies: '@tailwindcss/node': 4.1.18 '@tailwindcss/oxide': 4.1.18 tailwindcss: 4.1.18 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) '@trysound/sax@0.2.0': {} '@types/better-sqlite3@7.6.13': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.2 optional: true '@types/debug@4.1.12': @@ -5102,7 +5102,7 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/node@25.2.0': + '@types/node@25.2.2': dependencies: undici-types: 7.16.0 @@ -5112,30 +5112,30 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.2 '@types/yauzl@2.10.3': dependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.2 optional: true '@ungap/structured-clone@1.3.0': {} - '@vitejs/plugin-vue-jsx@4.2.0(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))': + '@vitejs/plugin-vue-jsx@4.2.0(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))': dependencies: '@babel/core': 7.29.0 '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) - '@rolldown/pluginutils': 1.0.0-rc.2 + '@rolldown/pluginutils': 1.0.0-rc.3 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vue: 3.5.27(typescript@5.9.3) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vue: 3.5.28(typescript@5.9.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@5.2.4(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))': + '@vitejs/plugin-vue@5.2.4(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))': dependencies: - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vue: 3.5.27(typescript@5.9.3) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vue: 3.5.28(typescript@5.9.3) '@volar/kit@2.4.28(typescript@5.9.3)': dependencies: @@ -5199,7 +5199,7 @@ snapshots: '@babel/types': 7.29.0 '@vue/babel-helper-vue-transform-on': 1.5.0 '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.29.0) - '@vue/shared': 3.5.27 + '@vue/shared': 3.5.28 optionalDependencies: '@babel/core': 7.29.0 transitivePeerDependencies: @@ -5212,53 +5212,53 @@ snapshots: '@babel/helper-module-imports': 7.28.6 '@babel/helper-plugin-utils': 7.28.6 '@babel/parser': 7.29.0 - '@vue/compiler-sfc': 3.5.27 + '@vue/compiler-sfc': 3.5.28 transitivePeerDependencies: - supports-color - '@vue/compiler-core@3.5.27': + '@vue/compiler-core@3.5.28': dependencies: '@babel/parser': 7.29.0 - '@vue/shared': 3.5.27 + '@vue/shared': 3.5.28 entities: 7.0.1 estree-walker: 2.0.2 source-map-js: 1.2.1 - '@vue/compiler-dom@3.5.27': + '@vue/compiler-dom@3.5.28': dependencies: - '@vue/compiler-core': 3.5.27 - '@vue/shared': 3.5.27 + '@vue/compiler-core': 3.5.28 + '@vue/shared': 3.5.28 - '@vue/compiler-sfc@3.5.27': + '@vue/compiler-sfc@3.5.28': dependencies: '@babel/parser': 7.29.0 - '@vue/compiler-core': 3.5.27 - '@vue/compiler-dom': 3.5.27 - '@vue/compiler-ssr': 3.5.27 - '@vue/shared': 3.5.27 + '@vue/compiler-core': 3.5.28 + '@vue/compiler-dom': 3.5.28 + '@vue/compiler-ssr': 3.5.28 + '@vue/shared': 3.5.28 estree-walker: 2.0.2 magic-string: 0.30.21 postcss: 8.5.6 source-map-js: 1.2.1 - '@vue/compiler-ssr@3.5.27': + '@vue/compiler-ssr@3.5.28': dependencies: - '@vue/compiler-dom': 3.5.27 - '@vue/shared': 3.5.27 + '@vue/compiler-dom': 3.5.28 + '@vue/shared': 3.5.28 - '@vue/devtools-api@8.0.5': + '@vue/devtools-api@8.0.6': dependencies: - '@vue/devtools-kit': 8.0.5 + '@vue/devtools-kit': 8.0.6 - '@vue/devtools-core@7.7.9(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3))': + '@vue/devtools-core@7.7.9(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3))': dependencies: '@vue/devtools-kit': 7.7.9 '@vue/devtools-shared': 7.7.9 mitt: 3.0.1 nanoid: 5.1.6 pathe: 2.0.3 - vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) - vue: 3.5.27(typescript@5.9.3) + vite-hot-client: 2.1.0(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + vue: 3.5.28(typescript@5.9.3) transitivePeerDependencies: - vite @@ -5272,9 +5272,9 @@ snapshots: speakingurl: 14.0.1 superjson: 2.2.6 - '@vue/devtools-kit@8.0.5': + '@vue/devtools-kit@8.0.6': dependencies: - '@vue/devtools-shared': 8.0.5 + '@vue/devtools-shared': 8.0.6 birpc: 2.9.0 hookable: 5.5.3 mitt: 3.0.1 @@ -5286,46 +5286,46 @@ snapshots: dependencies: rfdc: 1.4.1 - '@vue/devtools-shared@8.0.5': + '@vue/devtools-shared@8.0.6': dependencies: rfdc: 1.4.1 - '@vue/reactivity@3.5.27': + '@vue/reactivity@3.5.28': dependencies: - '@vue/shared': 3.5.27 + '@vue/shared': 3.5.28 - '@vue/runtime-core@3.5.27': + '@vue/runtime-core@3.5.28': dependencies: - '@vue/reactivity': 3.5.27 - '@vue/shared': 3.5.27 + '@vue/reactivity': 3.5.28 + '@vue/shared': 3.5.28 - '@vue/runtime-dom@3.5.27': + '@vue/runtime-dom@3.5.28': dependencies: - '@vue/reactivity': 3.5.27 - '@vue/runtime-core': 3.5.27 - '@vue/shared': 3.5.27 + '@vue/reactivity': 3.5.28 + '@vue/runtime-core': 3.5.28 + '@vue/shared': 3.5.28 csstype: 3.2.3 - '@vue/server-renderer@3.5.27(vue@3.5.27(typescript@5.9.3))': + '@vue/server-renderer@3.5.28(vue@3.5.28(typescript@5.9.3))': dependencies: - '@vue/compiler-ssr': 3.5.27 - '@vue/shared': 3.5.27 - vue: 3.5.27(typescript@5.9.3) + '@vue/compiler-ssr': 3.5.28 + '@vue/shared': 3.5.28 + vue: 3.5.28(typescript@5.9.3) - '@vue/shared@3.5.27': {} + '@vue/shared@3.5.28': {} - '@vueuse/core@13.9.0(vue@3.5.27(typescript@5.9.3))': + '@vueuse/core@13.9.0(vue@3.5.28(typescript@5.9.3))': dependencies: '@types/web-bluetooth': 0.0.21 '@vueuse/metadata': 13.9.0 - '@vueuse/shared': 13.9.0(vue@3.5.27(typescript@5.9.3)) - vue: 3.5.27(typescript@5.9.3) + '@vueuse/shared': 13.9.0(vue@3.5.28(typescript@5.9.3)) + vue: 3.5.28(typescript@5.9.3) '@vueuse/metadata@13.9.0': {} - '@vueuse/shared@13.9.0(vue@3.5.27(typescript@5.9.3))': + '@vueuse/shared@13.9.0(vue@3.5.28(typescript@5.9.3))': dependencies: - vue: 3.5.27(typescript@5.9.3) + vue: 3.5.28(typescript@5.9.3) '@yarnpkg/lockfile@1.1.0': {} @@ -5383,11 +5383,11 @@ snapshots: transitivePeerDependencies: - supports-color - astro@6.0.0-beta.6(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2): + astro@6.0.0-beta.9(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.57.1)(typescript@5.9.3)(yaml@2.8.2): dependencies: '@astrojs/compiler': 0.0.0-render-script-20251003120459 - '@astrojs/internal-helpers': 0.7.5 - '@astrojs/markdown-remark': 7.0.0-beta.3 + '@astrojs/internal-helpers': 0.8.0-beta.0 + '@astrojs/markdown-remark': 7.0.0-beta.5 '@astrojs/telemetry': 3.3.0 '@capsizecss/unpack': 4.0.0 '@oslojs/encoding': 1.1.0 @@ -5415,17 +5415,17 @@ snapshots: http-cache-semantics: 4.2.0 js-yaml: 4.1.1 magic-string: 0.30.21 - magicast: 0.5.1 + magicast: 0.5.2 mrmime: 2.0.1 neotraverse: 0.6.18 - p-limit: 7.2.0 + p-limit: 7.3.0 p-queue: 9.1.0 package-manager-detector: 1.6.0 piccolore: 0.1.3 picomatch: 4.0.3 prompts: 2.4.2 rehype: 13.0.2 - semver: 7.7.3 + semver: 7.7.4 shiki: 3.22.0 smol-toml: 1.6.0 svgo: 4.0.0 @@ -5437,11 +5437,11 @@ snapshots: unist-util-visit: 5.1.0 unstorage: 1.17.4 vfile: 6.0.3 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vitefu: 1.1.1(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vitefu: 1.1.1(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) xxhash-wasm: 1.1.0 yargs-parser: 22.0.0 - yocto-spinner: 1.0.0 + yocto-spinner: 1.1.0 zod: 4.3.6 optionalDependencies: sharp: 0.34.5 @@ -5546,8 +5546,8 @@ snapshots: browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.9.19 - caniuse-lite: 1.0.30001767 - electron-to-chromium: 1.5.283 + caniuse-lite: 1.0.30001769 + electron-to-chromium: 1.5.286 node-releases: 2.0.27 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -5584,7 +5584,7 @@ snapshots: camelcase@8.0.0: {} - caniuse-lite@1.0.30001767: {} + caniuse-lite@1.0.30001769: {} ccount@2.0.1: {} @@ -5625,7 +5625,7 @@ snapshots: parse5: 7.3.0 parse5-htmlparser2-tree-adapter: 7.1.0 parse5-parser-stream: 7.1.2 - undici: 7.20.0 + undici: 7.21.0 whatwg-mimetype: 4.0.0 chokidar@4.0.3: @@ -5678,7 +5678,7 @@ snapshots: confbox@0.1.8: {} - confbox@0.2.2: {} + confbox@0.2.4: {} convert-source-map@2.0.0: {} @@ -5741,7 +5741,7 @@ snapshots: csstype@3.2.3: {} - daisyui@5.5.17: {} + daisyui@5.5.18: {} data-uri-to-buffer@4.0.1: {} @@ -5822,9 +5822,9 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dotenv@17.2.3: {} + dotenv@17.2.4: {} - drizzle-kit@0.31.8: + drizzle-kit@0.31.9: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 @@ -5849,7 +5849,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.283: {} + electron-to-chromium@1.5.286: {} emmet@2.4.11: dependencies: @@ -5873,7 +5873,7 @@ snapshots: dependencies: once: 1.4.0 - enhanced-resolve@5.18.4: + enhanced-resolve@5.19.0: dependencies: graceful-fs: 4.2.11 tapable: 2.3.0 @@ -5957,34 +5957,34 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - esbuild@0.27.2: + esbuild@0.27.3: optionalDependencies: - '@esbuild/aix-ppc64': 0.27.2 - '@esbuild/android-arm': 0.27.2 - '@esbuild/android-arm64': 0.27.2 - '@esbuild/android-x64': 0.27.2 - '@esbuild/darwin-arm64': 0.27.2 - '@esbuild/darwin-x64': 0.27.2 - '@esbuild/freebsd-arm64': 0.27.2 - '@esbuild/freebsd-x64': 0.27.2 - '@esbuild/linux-arm': 0.27.2 - '@esbuild/linux-arm64': 0.27.2 - '@esbuild/linux-ia32': 0.27.2 - '@esbuild/linux-loong64': 0.27.2 - '@esbuild/linux-mips64el': 0.27.2 - '@esbuild/linux-ppc64': 0.27.2 - '@esbuild/linux-riscv64': 0.27.2 - '@esbuild/linux-s390x': 0.27.2 - '@esbuild/linux-x64': 0.27.2 - '@esbuild/netbsd-arm64': 0.27.2 - '@esbuild/netbsd-x64': 0.27.2 - '@esbuild/openbsd-arm64': 0.27.2 - '@esbuild/openbsd-x64': 0.27.2 - '@esbuild/openharmony-arm64': 0.27.2 - '@esbuild/sunos-x64': 0.27.2 - '@esbuild/win32-arm64': 0.27.2 - '@esbuild/win32-ia32': 0.27.2 - '@esbuild/win32-x64': 0.27.2 + '@esbuild/aix-ppc64': 0.27.3 + '@esbuild/android-arm': 0.27.3 + '@esbuild/android-arm64': 0.27.3 + '@esbuild/android-x64': 0.27.3 + '@esbuild/darwin-arm64': 0.27.3 + '@esbuild/darwin-x64': 0.27.3 + '@esbuild/freebsd-arm64': 0.27.3 + '@esbuild/freebsd-x64': 0.27.3 + '@esbuild/linux-arm': 0.27.3 + '@esbuild/linux-arm64': 0.27.3 + '@esbuild/linux-ia32': 0.27.3 + '@esbuild/linux-loong64': 0.27.3 + '@esbuild/linux-mips64el': 0.27.3 + '@esbuild/linux-ppc64': 0.27.3 + '@esbuild/linux-riscv64': 0.27.3 + '@esbuild/linux-s390x': 0.27.3 + '@esbuild/linux-x64': 0.27.3 + '@esbuild/netbsd-arm64': 0.27.3 + '@esbuild/netbsd-x64': 0.27.3 + '@esbuild/openbsd-arm64': 0.27.3 + '@esbuild/openbsd-x64': 0.27.3 + '@esbuild/openharmony-arm64': 0.27.3 + '@esbuild/sunos-x64': 0.27.3 + '@esbuild/win32-arm64': 0.27.3 + '@esbuild/win32-ia32': 0.27.3 + '@esbuild/win32-x64': 0.27.3 escalade@3.2.0: {} @@ -6143,7 +6143,7 @@ snapshots: '@sec-ant/readable-stream': 0.4.1 is-stream: 4.0.1 - get-tsconfig@4.13.1: + get-tsconfig@4.13.6: dependencies: resolve-pkg-maps: 1.0.0 @@ -6491,7 +6491,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - magicast@0.5.1: + magicast@0.5.2: dependencies: '@babel/parser': 7.29.0 '@babel/types': 7.29.0 @@ -6875,7 +6875,7 @@ snapshots: node-abi@3.87.0: dependencies: - semver: 7.7.3 + semver: 7.7.4 optional: true node-domexception@1.0.0: {} @@ -6949,7 +6949,7 @@ snapshots: is-docker: 2.2.1 is-wsl: 2.2.0 - p-limit@7.2.0: + p-limit@7.3.0: dependencies: yocto-queue: 1.2.2 @@ -7004,7 +7004,7 @@ snapshots: klaw-sync: 6.0.0 minimist: 1.2.8 open: 7.4.2 - semver: 7.7.3 + semver: 7.7.4 slash: 2.0.0 tmp: 0.2.5 yaml: 2.8.2 @@ -7039,7 +7039,7 @@ snapshots: pkg-types@2.3.0: dependencies: - confbox: 0.2.2 + confbox: 0.2.4 exsolve: 1.0.8 pathe: 2.0.3 @@ -7282,7 +7282,7 @@ snapshots: semver@6.3.1: {} - semver@7.7.3: {} + semver@7.7.4: {} send@1.2.1: dependencies: @@ -7317,7 +7317,7 @@ snapshots: dependencies: '@img/colour': 1.0.0 detect-libc: 2.1.2 - semver: 7.7.3 + semver: 7.7.4 optionalDependencies: '@img/sharp-darwin-arm64': 0.34.5 '@img/sharp-darwin-x64': 0.34.5 @@ -7546,7 +7546,7 @@ snapshots: typescript-auto-import-cache@0.3.6: dependencies: - semver: 7.7.3 + semver: 7.7.4 typescript@5.9.3: {} @@ -7558,7 +7558,7 @@ snapshots: undici-types@7.16.0: {} - undici@7.20.0: {} + undici@7.21.0: {} unicode-properties@1.4.1: dependencies: @@ -7672,11 +7672,11 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 - vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + vite-hot-client@2.1.0(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): dependencies: - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vite-plugin-inspect@0.8.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + vite-plugin-inspect@0.8.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): dependencies: '@antfu/utils': 0.7.10 '@rollup/pluginutils': 5.3.0(rollup@4.57.1) @@ -7687,28 +7687,28 @@ snapshots: perfect-debounce: 1.0.0 picocolors: 1.1.1 sirv: 3.0.2 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) transitivePeerDependencies: - rollup - supports-color - vite-plugin-vue-devtools@7.7.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)): + vite-plugin-vue-devtools@7.7.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)): dependencies: - '@vue/devtools-core': 7.7.9(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.27(typescript@5.9.3)) + '@vue/devtools-core': 7.7.9(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2))(vue@3.5.28(typescript@5.9.3)) '@vue/devtools-kit': 7.7.9 '@vue/devtools-shared': 7.7.9 execa: 9.6.1 sirv: 3.0.2 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) - vite-plugin-inspect: 0.8.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) - vite-plugin-vue-inspector: 5.3.2(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite-plugin-inspect: 0.8.9(rollup@4.57.1)(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) + vite-plugin-vue-inspector: 5.3.2(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)) transitivePeerDependencies: - '@nuxt/kit' - rollup - supports-color - vue - vite-plugin-vue-inspector@5.3.2(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + vite-plugin-vue-inspector@5.3.2(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): dependencies: '@babel/core': 7.29.0 '@babel/plugin-proposal-decorators': 7.29.0(@babel/core@7.29.0) @@ -7716,31 +7716,31 @@ snapshots: '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) '@babel/plugin-transform-typescript': 7.28.6(@babel/core@7.29.0) '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0) - '@vue/compiler-dom': 3.5.27 + '@vue/compiler-dom': 3.5.28 kolorist: 1.8.0 magic-string: 0.30.21 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) transitivePeerDependencies: - supports-color - vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2): + vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2): dependencies: - esbuild: 0.27.2 + esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 rollup: 4.57.1 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.2.0 + '@types/node': 25.2.2 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 yaml: 2.8.2 - vitefu@1.1.1(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): + vitefu@1.1.1(vite@7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2)): optionalDependencies: - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) + vite: 7.3.1(@types/node@25.2.2)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.2) volar-service-css@0.0.68(@volar/language-service@2.4.28): dependencies: @@ -7783,7 +7783,7 @@ snapshots: volar-service-typescript@0.0.68(@volar/language-service@2.4.28): dependencies: path-browserify: 1.0.1 - semver: 7.7.3 + semver: 7.7.4 typescript-auto-import-cache: 0.3.6 vscode-languageserver-textdocument: 1.0.12 vscode-nls: 5.2.0 @@ -7839,18 +7839,18 @@ snapshots: vscode-uri@3.1.0: {} - vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.27(typescript@5.9.3)): + vue-chartjs@5.3.3(chart.js@4.5.1)(vue@3.5.28(typescript@5.9.3)): dependencies: chart.js: 4.5.1 - vue: 3.5.27(typescript@5.9.3) + vue: 3.5.28(typescript@5.9.3) - vue@3.5.27(typescript@5.9.3): + vue@3.5.28(typescript@5.9.3): dependencies: - '@vue/compiler-dom': 3.5.27 - '@vue/compiler-sfc': 3.5.27 - '@vue/runtime-dom': 3.5.27 - '@vue/server-renderer': 3.5.27(vue@3.5.27(typescript@5.9.3)) - '@vue/shared': 3.5.27 + '@vue/compiler-dom': 3.5.28 + '@vue/compiler-sfc': 3.5.28 + '@vue/runtime-dom': 3.5.28 + '@vue/server-renderer': 3.5.28(vue@3.5.28(typescript@5.9.3)) + '@vue/shared': 3.5.28 optionalDependencies: typescript: 5.9.3 @@ -7949,7 +7949,7 @@ snapshots: yocto-queue@1.2.2: {} - yocto-spinner@1.0.0: + yocto-spinner@1.1.0: dependencies: yoctocolors: 2.1.2 diff --git a/src/components/StatCard.astro b/src/components/StatCard.astro new file mode 100644 index 0000000..8ad66b1 --- /dev/null +++ b/src/components/StatCard.astro @@ -0,0 +1,25 @@ +--- +import { Icon } from 'astro-icon/components'; + +interface Props { + title: string; + value: string; + description?: string; + icon?: string; + color?: string; + valueClass?: string; +} + +const { title, value, description, icon, color = 'text-primary', valueClass } = Astro.props; +--- + +
+ {icon && ( +
+ +
+ )} +
{title}
+
{value}
+ {description &&
{description}
} +
diff --git a/src/lib/formatTime.ts b/src/lib/formatTime.ts index e9a86af..54e0626 100644 --- a/src/lib/formatTime.ts +++ b/src/lib/formatTime.ts @@ -25,3 +25,16 @@ export function formatTimeRange(start: Date, end: Date | null): string { const ms = end.getTime() - start.getTime(); return formatDuration(ms); } + +/** + * Formats a cent-based amount as a currency string. + * @param amount - Amount in cents (e.g. 1500 = $15.00) + * @param currency - ISO 4217 currency code (default: 'USD') + * @returns Formatted currency string like "$15.00" + */ +export function formatCurrency(amount: number, currency: string = "USD"): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: currency, + }).format(amount / 100); +} diff --git a/src/lib/getCurrentTeam.ts b/src/lib/getCurrentTeam.ts new file mode 100644 index 0000000..6819a4b --- /dev/null +++ b/src/lib/getCurrentTeam.ts @@ -0,0 +1,24 @@ +import { db } from '../db'; +import { members } from '../db/schema'; +import { eq } from 'drizzle-orm'; + +type User = { id: string; [key: string]: any }; + +/** + * Get the current team membership for a user based on the currentTeamId cookie. + * Returns the membership row, or null if the user has no memberships. + */ +export async function getCurrentTeam(user: User, currentTeamId?: string | null) { + const userMemberships = await db.select() + .from(members) + .where(eq(members.userId, user.id)) + .all(); + + if (userMemberships.length === 0) return null; + + const membership = currentTeamId + ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] + : userMemberships[0]; + + return membership; +} diff --git a/src/lib/validation.ts b/src/lib/validation.ts index 1bba30e..255643a 100644 --- a/src/lib/validation.ts +++ b/src/lib/validation.ts @@ -2,6 +2,30 @@ import { db } from "../db"; import { clients, tags as tagsTable } from "../db/schema"; import { eq, and } from "drizzle-orm"; +export const MAX_LENGTHS = { + name: 255, + email: 320, + password: 128, + phone: 50, + address: 255, // street, city, state, zip, country + currency: 10, + invoiceNumber: 50, + invoiceNotes: 5000, + itemDescription: 2000, + description: 2000, // time entry description +} as const; + +export function exceedsLength( + field: string, + value: string | null | undefined, + maxLength: number, +): string | null { + if (value && value.length > maxLength) { + return `${field} must be ${maxLength} characters or fewer`; + } + return null; +} + export async function validateTimeEntryResources({ organizationId, clientId, @@ -60,3 +84,9 @@ export function validateTimeRange( return { valid: true, startDate, endDate }; } + +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + +export function isValidEmail(email: string): boolean { + return EMAIL_REGEX.test(email) && email.length <= 320; +} diff --git a/src/pages/api/auth/passkey/login/finish.ts b/src/pages/api/auth/passkey/login/finish.ts index dda86ae..7bfd670 100644 --- a/src/pages/api/auth/passkey/login/finish.ts +++ b/src/pages/api/auth/passkey/login/finish.ts @@ -65,7 +65,8 @@ export const POST: APIRoute = async ({ request, cookies }) => { }, }); } catch (error) { - return new Response(JSON.stringify({ error: (error as Error).message }), { + console.error("Passkey authentication verification failed:", error); + return new Response(JSON.stringify({ error: "Verification failed" }), { status: 400, }); } diff --git a/src/pages/api/auth/passkey/login/start.ts b/src/pages/api/auth/passkey/login/start.ts index 81697cc..409775f 100644 --- a/src/pages/api/auth/passkey/login/start.ts +++ b/src/pages/api/auth/passkey/login/start.ts @@ -2,8 +2,13 @@ import type { APIRoute } from "astro"; import { generateAuthenticationOptions } from "@simplewebauthn/server"; import { db } from "../../../../../db"; import { passkeyChallenges } from "../../../../../db/schema"; +import { lte } from "drizzle-orm"; export const GET: APIRoute = async ({ request }) => { + await db + .delete(passkeyChallenges) + .where(lte(passkeyChallenges.expiresAt, new Date())); + const options = await generateAuthenticationOptions({ rpID: new URL(request.url).hostname, userVerification: "preferred", diff --git a/src/pages/api/auth/passkey/register/finish.ts b/src/pages/api/auth/passkey/register/finish.ts index c933342..0c81661 100644 --- a/src/pages/api/auth/passkey/register/finish.ts +++ b/src/pages/api/auth/passkey/register/finish.ts @@ -48,7 +48,8 @@ export const POST: APIRoute = async ({ request, locals }) => { expectedRPID: new URL(request.url).hostname, }); } catch (error) { - return new Response(JSON.stringify({ error: (error as Error).message }), { + console.error("Passkey registration verification failed:", error); + return new Response(JSON.stringify({ error: "Verification failed" }), { status: 400, }); } diff --git a/src/pages/api/auth/passkey/register/start.ts b/src/pages/api/auth/passkey/register/start.ts index a3ec56a..4425080 100644 --- a/src/pages/api/auth/passkey/register/start.ts +++ b/src/pages/api/auth/passkey/register/start.ts @@ -2,7 +2,7 @@ import type { APIRoute } from "astro"; import { generateRegistrationOptions } from "@simplewebauthn/server"; import { db } from "../../../../../db"; import { passkeys, passkeyChallenges } from "../../../../../db/schema"; -import { eq } from "drizzle-orm"; +import { eq, lte } from "drizzle-orm"; export const GET: APIRoute = async ({ request, locals }) => { const user = locals.user; @@ -13,6 +13,10 @@ export const GET: APIRoute = async ({ request, locals }) => { }); } + await db + .delete(passkeyChallenges) + .where(lte(passkeyChallenges.expiresAt, new Date())); + const userPasskeys = await db.query.passkeys.findMany({ where: eq(passkeys.userId, user.id), }); diff --git a/src/pages/api/auth/signup.ts b/src/pages/api/auth/signup.ts index a03c708..fdb0aa1 100644 --- a/src/pages/api/auth/signup.ts +++ b/src/pages/api/auth/signup.ts @@ -7,6 +7,7 @@ import { siteSettings, } from "../../../db/schema"; import { hashPassword, createSession } from "../../../lib/auth"; +import { isValidEmail, MAX_LENGTHS } from "../../../lib/validation"; import { eq, count, sql } from "drizzle-orm"; import { nanoid } from "nanoid"; @@ -37,6 +38,18 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => { return redirect("/signup?error=missing_fields"); } + if (!isValidEmail(email)) { + return redirect("/signup?error=invalid_email"); + } + + if (name.length > MAX_LENGTHS.name) { + return redirect("/signup?error=name_too_long"); + } + + if (password.length > MAX_LENGTHS.password) { + return redirect("/signup?error=password_too_long"); + } + if (password.length < 8) { return redirect("/signup?error=password_too_short"); } @@ -47,7 +60,7 @@ export const POST: APIRoute = async ({ request, cookies, redirect }) => { .where(eq(users.email, email)) .get(); if (existingUser) { - return redirect("/signup?error=user_exists"); + return redirect("/login?registered=true"); } const passwordHash = await hashPassword(password); diff --git a/src/pages/api/clients/[id]/delete.ts b/src/pages/api/clients/[id]/delete.ts index 855f69b..5cb27fb 100644 --- a/src/pages/api/clients/[id]/delete.ts +++ b/src/pages/api/clients/[id]/delete.ts @@ -52,6 +52,17 @@ export const POST: APIRoute = async ({ params, locals, redirect }) => { return new Response("Not authorized", { status: 403 }); } + const isAdminOrOwner = membership.role === "owner" || membership.role === "admin"; + if (!isAdminOrOwner) { + if (locals.scopes) { + return new Response( + JSON.stringify({ error: "Only owners and admins can delete clients" }), + { status: 403, headers: { "Content-Type": "application/json" } }, + ); + } + return new Response("Only owners and admins can delete clients", { status: 403 }); + } + await db.delete(timeEntries).where(eq(timeEntries.clientId, id)).run(); await db.delete(clients).where(eq(clients.id, id)).run(); diff --git a/src/pages/api/clients/[id]/update.ts b/src/pages/api/clients/[id]/update.ts index 2b385a5..c56b2fc 100644 --- a/src/pages/api/clients/[id]/update.ts +++ b/src/pages/api/clients/[id]/update.ts @@ -2,6 +2,7 @@ import type { APIRoute } from "astro"; import { db } from "../../../../db"; import { clients, members } from "../../../../db/schema"; import { eq, and } from "drizzle-orm"; +import { MAX_LENGTHS, exceedsLength } from "../../../../lib/validation"; export const POST: APIRoute = async ({ request, params, locals, redirect }) => { const user = locals.user; @@ -49,6 +50,25 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => { return new Response("Client name is required", { status: 400 }); } + const lengthError = + exceedsLength("Name", name, MAX_LENGTHS.name) || + exceedsLength("Email", email, MAX_LENGTHS.email) || + exceedsLength("Phone", phone, MAX_LENGTHS.phone) || + exceedsLength("Street", street, MAX_LENGTHS.address) || + exceedsLength("City", city, MAX_LENGTHS.address) || + exceedsLength("State", state, MAX_LENGTHS.address) || + exceedsLength("ZIP", zip, MAX_LENGTHS.address) || + exceedsLength("Country", country, MAX_LENGTHS.address); + if (lengthError) { + if (locals.scopes) { + return new Response(JSON.stringify({ error: lengthError }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response(lengthError, { status: 400 }); + } + try { const client = await db .select() @@ -87,6 +107,17 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => { return new Response("Not authorized", { status: 403 }); } + const isAdminOrOwner = membership.role === "owner" || membership.role === "admin"; + if (!isAdminOrOwner) { + if (locals.scopes) { + return new Response( + JSON.stringify({ error: "Only owners and admins can update clients" }), + { status: 403, headers: { "Content-Type": "application/json" } }, + ); + } + return new Response("Only owners and admins can update clients", { status: 403 }); + } + await db .update(clients) .set({ diff --git a/src/pages/api/clients/create.ts b/src/pages/api/clients/create.ts index 331b4b3..c9b5988 100644 --- a/src/pages/api/clients/create.ts +++ b/src/pages/api/clients/create.ts @@ -3,6 +3,7 @@ import { db } from "../../../db"; import { clients, members } from "../../../db/schema"; import { eq } from "drizzle-orm"; import { nanoid } from "nanoid"; +import { MAX_LENGTHS, exceedsLength } from "../../../lib/validation"; export const POST: APIRoute = async ({ request, locals, redirect }) => { const user = locals.user; @@ -45,6 +46,25 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { return new Response("Name is required", { status: 400 }); } + const lengthError = + exceedsLength("Name", name, MAX_LENGTHS.name) || + exceedsLength("Email", email, MAX_LENGTHS.email) || + exceedsLength("Phone", phone, MAX_LENGTHS.phone) || + exceedsLength("Street", street, MAX_LENGTHS.address) || + exceedsLength("City", city, MAX_LENGTHS.address) || + exceedsLength("State", state, MAX_LENGTHS.address) || + exceedsLength("ZIP", zip, MAX_LENGTHS.address) || + exceedsLength("Country", country, MAX_LENGTHS.address); + if (lengthError) { + if (locals.scopes) { + return new Response(JSON.stringify({ error: lengthError }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + return new Response(lengthError, { status: 400 }); + } + const userOrg = await db .select() .from(members) @@ -55,6 +75,17 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { return new Response("No organization found", { status: 400 }); } + const isAdminOrOwner = userOrg.role === "owner" || userOrg.role === "admin"; + if (!isAdminOrOwner) { + if (locals.scopes) { + return new Response( + JSON.stringify({ error: "Only owners and admins can create clients" }), + { status: 403, headers: { "Content-Type": "application/json" } }, + ); + } + return new Response("Only owners and admins can create clients", { status: 403 }); + } + const id = nanoid(); await db.insert(clients).values({ diff --git a/src/pages/api/invoices/[id]/convert.ts b/src/pages/api/invoices/[id]/convert.ts index cd3524d..ab239f8 100644 --- a/src/pages/api/invoices/[id]/convert.ts +++ b/src/pages/api/invoices/[id]/convert.ts @@ -45,6 +45,11 @@ export const POST: APIRoute = async ({ redirect, locals, params }) => { return new Response("Unauthorized", { status: 401 }); } + const isAdminOrOwner = membership.role === "owner" || membership.role === "admin"; + if (!isAdminOrOwner) { + return new Response("Only owners and admins can convert quotes", { status: 403 }); + } + try { const lastInvoice = await db .select() diff --git a/src/pages/api/invoices/[id]/generate.ts b/src/pages/api/invoices/[id]/generate.ts index 73fc0bd..0cd8fbf 100644 --- a/src/pages/api/invoices/[id]/generate.ts +++ b/src/pages/api/invoices/[id]/generate.ts @@ -107,7 +107,7 @@ export const GET: APIRoute = async ({ params, locals }) => { return new Response(buffer, { headers: { "Content-Type": "application/pdf", - "Content-Disposition": `attachment; filename="${invoice.number}.pdf"`, + "Content-Disposition": `attachment; filename="${invoice.number.replace(/[^a-zA-Z0-9_\-\.]/g, "_")}.pdf"`, }, }); } catch (error) { diff --git a/src/pages/api/invoices/[id]/import-time.ts b/src/pages/api/invoices/[id]/import-time.ts index 61de21d..3e2e149 100644 --- a/src/pages/api/invoices/[id]/import-time.ts +++ b/src/pages/api/invoices/[id]/import-time.ts @@ -222,51 +222,52 @@ export const POST: APIRoute = async ({ request, params, locals, redirect }) => { return redirect(`/dashboard/invoices/${id}?error=no-entries`); } - // Transaction-like operations try { - await db.insert(invoiceItems).values(newItems); + await db.transaction(async (tx) => { + await tx.insert(invoiceItems).values(newItems); - if (entryIdsToUpdate.length > 0) { - await db - .update(timeEntries) - .set({ invoiceId: invoice.id }) - .where(inArray(timeEntries.id, entryIdsToUpdate)); - } - - const allItems = await db - .select() - .from(invoiceItems) - .where(eq(invoiceItems.invoiceId, invoice.id)); - - const subtotal = allItems.reduce((sum, item) => sum + item.amount, 0); - - let discountAmount = 0; - if (invoice.discountType === "percentage") { - discountAmount = Math.round( - subtotal * ((invoice.discountValue || 0) / 100), - ); - } else { - discountAmount = Math.round((invoice.discountValue || 0) * 100); - if (invoice.discountValue && invoice.discountValue > 0) { - discountAmount = Math.round((invoice.discountValue || 0) * 100); + if (entryIdsToUpdate.length > 0) { + await tx + .update(timeEntries) + .set({ invoiceId: invoice.id }) + .where(inArray(timeEntries.id, entryIdsToUpdate)); } - } - const taxableAmount = Math.max(0, subtotal - discountAmount); - const taxAmount = Math.round( - taxableAmount * ((invoice.taxRate || 0) / 100), - ); - const total = subtotal - discountAmount + taxAmount; + const allItems = await tx + .select() + .from(invoiceItems) + .where(eq(invoiceItems.invoiceId, invoice.id)); - await db - .update(invoices) - .set({ - subtotal, - discountAmount, - taxAmount, - total, - }) - .where(eq(invoices.id, invoice.id)); + const subtotal = allItems.reduce((sum, item) => sum + item.amount, 0); + + let discountAmount = 0; + if (invoice.discountType === "percentage") { + discountAmount = Math.round( + subtotal * ((invoice.discountValue || 0) / 100), + ); + } else { + discountAmount = Math.round((invoice.discountValue || 0) * 100); + if (invoice.discountValue && invoice.discountValue > 0) { + discountAmount = Math.round((invoice.discountValue || 0) * 100); + } + } + + const taxableAmount = Math.max(0, subtotal - discountAmount); + const taxAmount = Math.round( + taxableAmount * ((invoice.taxRate || 0) / 100), + ); + const total = subtotal - discountAmount + taxAmount; + + await tx + .update(invoices) + .set({ + subtotal, + discountAmount, + taxAmount, + total, + }) + .where(eq(invoices.id, invoice.id)); + }); return redirect(`/dashboard/invoices/${id}?success=imported`); } catch (error) { diff --git a/src/pages/api/invoices/[id]/items/add.ts b/src/pages/api/invoices/[id]/items/add.ts index c205c8a..241f4fc 100644 --- a/src/pages/api/invoices/[id]/items/add.ts +++ b/src/pages/api/invoices/[id]/items/add.ts @@ -3,6 +3,7 @@ import { db } from "../../../../../db"; import { invoiceItems, invoices, members } from "../../../../../db/schema"; import { eq, and } from "drizzle-orm"; import { recalculateInvoiceTotals } from "../../../../../utils/invoice"; +import { MAX_LENGTHS, exceedsLength } from "../../../../../lib/validation"; export const POST: APIRoute = async ({ request, @@ -61,6 +62,11 @@ export const POST: APIRoute = async ({ return new Response("Missing required fields", { status: 400 }); } + const lengthError = exceedsLength("Description", description, MAX_LENGTHS.itemDescription); + if (lengthError) { + return new Response(lengthError, { status: 400 }); + } + const quantity = parseFloat(quantityStr); const unitPriceMajor = parseFloat(unitPriceStr); diff --git a/src/pages/api/invoices/[id]/status.ts b/src/pages/api/invoices/[id]/status.ts index f3b94ee..ea02e93 100644 --- a/src/pages/api/invoices/[id]/status.ts +++ b/src/pages/api/invoices/[id]/status.ts @@ -60,6 +60,13 @@ export const POST: APIRoute = async ({ return new Response("Unauthorized", { status: 401 }); } + // Destructive status changes require owner/admin + const destructiveStatuses = ["void"]; + const isAdminOrOwner = membership.role === "owner" || membership.role === "admin"; + if (destructiveStatuses.includes(status) && !isAdminOrOwner) { + return new Response("Only owners and admins can void invoices", { status: 403 }); + } + try { await db .update(invoices) diff --git a/src/pages/api/invoices/[id]/update.ts b/src/pages/api/invoices/[id]/update.ts index 5a8de22..8075c4d 100644 --- a/src/pages/api/invoices/[id]/update.ts +++ b/src/pages/api/invoices/[id]/update.ts @@ -3,6 +3,7 @@ import { db } from "../../../../db"; import { invoices, members } from "../../../../db/schema"; import { eq, and } from "drizzle-orm"; import { recalculateInvoiceTotals } from "../../../../utils/invoice"; +import { MAX_LENGTHS, exceedsLength } from "../../../../lib/validation"; export const POST: APIRoute = async ({ request, redirect, locals, params }) => { const user = locals.user; @@ -56,6 +57,14 @@ export const POST: APIRoute = async ({ request, redirect, locals, params }) => { return new Response("Missing required fields", { status: 400 }); } + const lengthError = + exceedsLength("Invoice number", number, MAX_LENGTHS.invoiceNumber) || + exceedsLength("Currency", currency, MAX_LENGTHS.currency) || + exceedsLength("Notes", notes, MAX_LENGTHS.invoiceNotes); + if (lengthError) { + return new Response(lengthError, { status: 400 }); + } + try { const issueDate = new Date(issueDateStr); const dueDate = new Date(dueDateStr); diff --git a/src/pages/api/invoices/delete.ts b/src/pages/api/invoices/delete.ts index 4aacdc2..c921d2c 100644 --- a/src/pages/api/invoices/delete.ts +++ b/src/pages/api/invoices/delete.ts @@ -43,6 +43,11 @@ export const POST: APIRoute = async ({ request, redirect, locals }) => { return new Response("Unauthorized", { status: 401 }); } + const isAdminOrOwner = membership.role === "owner" || membership.role === "admin"; + if (!isAdminOrOwner) { + return new Response("Only owners and admins can delete invoices", { status: 403 }); + } + try { // Delete invoice items first (manual cascade) await db.delete(invoiceItems).where(eq(invoiceItems.invoiceId, invoiceId)); diff --git a/src/pages/api/organizations/update-name.ts b/src/pages/api/organizations/update-name.ts index 139dba0..83b6ce9 100644 --- a/src/pages/api/organizations/update-name.ts +++ b/src/pages/api/organizations/update-name.ts @@ -4,6 +4,7 @@ import path from "path"; import { db } from "../../../db"; import { organizations, members } from "../../../db/schema"; import { eq, and } from "drizzle-orm"; +import { MAX_LENGTHS, exceedsLength } from "../../../lib/validation"; export const POST: APIRoute = async ({ request, locals, redirect }) => { const user = locals.user; @@ -29,6 +30,18 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { }); } + const lengthError = + exceedsLength("Name", name, MAX_LENGTHS.name) || + exceedsLength("Street", street, MAX_LENGTHS.address) || + exceedsLength("City", city, MAX_LENGTHS.address) || + exceedsLength("State", state, MAX_LENGTHS.address) || + exceedsLength("ZIP", zip, MAX_LENGTHS.address) || + exceedsLength("Country", country, MAX_LENGTHS.address) || + exceedsLength("Currency", defaultCurrency, MAX_LENGTHS.currency); + if (lengthError) { + return new Response(lengthError, { status: 400 }); + } + try { // Verify user is admin/owner of this organization const membership = await db @@ -67,7 +80,9 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { ); } - const ext = logo.name.split(".").pop() || "png"; + const rawExt = (logo.name.split(".").pop() || "png").toLowerCase().replace(/[^a-z]/g, ""); + const allowedExtensions = ["png", "jpg", "jpeg"]; + const ext = allowedExtensions.includes(rawExt) ? rawExt : "png"; const filename = `${organizationId}-${Date.now()}.${ext}`; const dataDir = process.env.DATA_DIR ? process.env.DATA_DIR diff --git a/src/pages/api/reports/export.ts b/src/pages/api/reports/export.ts index 416b587..93474e8 100644 --- a/src/pages/api/reports/export.ts +++ b/src/pages/api/reports/export.ts @@ -128,6 +128,13 @@ export const GET: APIRoute = async ({ request, locals, cookies }) => { "Tag", "Description", ]; + const sanitizeCell = (value: string): string => { + if (/^[=+\-@\t\r]/.test(value)) { + return `\t${value}`; + } + return value; + }; + const rows = entries.map((e) => { const start = e.entry.startTime; const end = e.entry.endTime; @@ -144,10 +151,10 @@ export const GET: APIRoute = async ({ request, locals, cookies }) => { start.toLocaleTimeString(), end ? end.toLocaleTimeString() : "", end ? duration.toFixed(2) : "Running", - `"${(e.user.name || "").replace(/"/g, '""')}"`, - `"${(e.client.name || "").replace(/"/g, '""')}"`, - `"${tagsStr.replace(/"/g, '""')}"`, - `"${(e.entry.description || "").replace(/"/g, '""')}"`, + `"${sanitizeCell((e.user.name || "").replace(/"/g, '""'))}"`, + `"${sanitizeCell((e.client.name || "").replace(/"/g, '""'))}"`, + `"${sanitizeCell(tagsStr.replace(/"/g, '""'))}"`, + `"${sanitizeCell((e.entry.description || "").replace(/"/g, '""'))}"`, ].join(","); }); diff --git a/src/pages/api/team/invite.ts b/src/pages/api/team/invite.ts index a9ca2cf..db0a616 100644 --- a/src/pages/api/team/invite.ts +++ b/src/pages/api/team/invite.ts @@ -2,6 +2,7 @@ import type { APIRoute } from 'astro'; import { db } from '../../../db'; import { users, members } from '../../../db/schema'; import { eq, and } from 'drizzle-orm'; +import { isValidEmail } from '../../../lib/validation'; export const POST: APIRoute = async ({ request, locals, redirect }) => { const user = locals.user; @@ -26,6 +27,10 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { return new Response('Email is required', { status: 400 }); } + if (!isValidEmail(email)) { + return new Response('Invalid email format', { status: 400 }); + } + if (!['member', 'admin'].includes(role)) { return new Response('Invalid role', { status: 400 }); } diff --git a/src/pages/api/time-entries/manual.ts b/src/pages/api/time-entries/manual.ts index 52e15a6..06fd988 100644 --- a/src/pages/api/time-entries/manual.ts +++ b/src/pages/api/time-entries/manual.ts @@ -6,6 +6,7 @@ import { nanoid } from "nanoid"; import { validateTimeEntryResources, validateTimeRange, + MAX_LENGTHS, } from "../../../lib/validation"; export const POST: APIRoute = async ({ request, locals }) => { @@ -27,6 +28,13 @@ export const POST: APIRoute = async ({ request, locals }) => { }); } + if (description && description.length > MAX_LENGTHS.description) { + return new Response( + JSON.stringify({ error: `Description must be ${MAX_LENGTHS.description} characters or fewer` }), + { status: 400, headers: { "Content-Type": "application/json" } }, + ); + } + if (!startTime) { return new Response(JSON.stringify({ error: "Start time is required" }), { status: 400, diff --git a/src/pages/api/time-entries/start.ts b/src/pages/api/time-entries/start.ts index 40e9f6d..6ad1d3d 100644 --- a/src/pages/api/time-entries/start.ts +++ b/src/pages/api/time-entries/start.ts @@ -3,7 +3,7 @@ import { db } from "../../../db"; import { timeEntries, members } from "../../../db/schema"; import { eq, and, isNull } from "drizzle-orm"; import { nanoid } from "nanoid"; -import { validateTimeEntryResources } from "../../../lib/validation"; +import { validateTimeEntryResources, MAX_LENGTHS } from "../../../lib/validation"; export const POST: APIRoute = async ({ request, locals }) => { if (!locals.user) return new Response("Unauthorized", { status: 401 }); @@ -17,6 +17,10 @@ export const POST: APIRoute = async ({ request, locals }) => { return new Response("Client is required", { status: 400 }); } + if (description && description.length > MAX_LENGTHS.description) { + return new Response(`Description must be ${MAX_LENGTHS.description} characters or fewer`, { status: 400 }); + } + const runningEntry = await db .select() .from(timeEntries) diff --git a/src/pages/api/user/change-password.ts b/src/pages/api/user/change-password.ts index a142b37..f1531ef 100644 --- a/src/pages/api/user/change-password.ts +++ b/src/pages/api/user/change-password.ts @@ -1,10 +1,11 @@ import type { APIRoute } from "astro"; import { db } from "../../../db"; -import { users } from "../../../db/schema"; +import { users, sessions } from "../../../db/schema"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; +import { MAX_LENGTHS } from "../../../lib/validation"; -export const POST: APIRoute = async ({ request, locals, redirect }) => { +export const POST: APIRoute = async ({ request, locals, redirect, cookies }) => { const user = locals.user; const contentType = request.headers.get("content-type"); const isJson = contentType?.includes("application/json"); @@ -53,6 +54,13 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { return new Response(msg, { status: 400 }); } + if (currentPassword.length > MAX_LENGTHS.password || newPassword.length > MAX_LENGTHS.password) { + const msg = `Password must be ${MAX_LENGTHS.password} characters or fewer`; + if (isJson) + return new Response(JSON.stringify({ error: msg }), { status: 400 }); + return new Response(msg, { status: 400 }); + } + try { // Get current user from database const dbUser = await db @@ -90,6 +98,32 @@ export const POST: APIRoute = async ({ request, locals, redirect }) => { .where(eq(users.id, user.id)) .run(); + // Invalidate all sessions, then re-create one for the current user + const currentSessionId = cookies.get("session_id")?.value; + if (currentSessionId) { + await db + .delete(sessions) + .where( + eq(sessions.userId, user.id), + ) + .run(); + + const { createSession } = await import("../../../lib/auth"); + const { sessionId, expiresAt } = await createSession(user.id); + cookies.set("session_id", sessionId, { + path: "/", + httpOnly: true, + secure: import.meta.env.PROD, + sameSite: "lax", + expires: expiresAt, + }); + } else { + await db + .delete(sessions) + .where(eq(sessions.userId, user.id)) + .run(); + } + if (isJson) { return new Response(JSON.stringify({ success: true }), { status: 200 }); } diff --git a/src/pages/dashboard/clients.astro b/src/pages/dashboard/clients.astro index 2f8645e..49736ea 100644 --- a/src/pages/dashboard/clients.astro +++ b/src/pages/dashboard/clients.astro @@ -1,26 +1,15 @@ --- import DashboardLayout from '../../layouts/DashboardLayout.astro'; import { db } from '../../db'; -import { clients, members } from '../../db/schema'; -import { eq, and } from 'drizzle-orm'; +import { clients } from '../../db/schema'; +import { eq } from 'drizzle-orm'; +import { getCurrentTeam } from '../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const organizationId = userMembership.organizationId; diff --git a/src/pages/dashboard/clients/[id]/edit.astro b/src/pages/dashboard/clients/[id]/edit.astro index 3eb39a7..e4281e9 100644 --- a/src/pages/dashboard/clients/[id]/edit.astro +++ b/src/pages/dashboard/clients/[id]/edit.astro @@ -2,8 +2,9 @@ import DashboardLayout from '../../../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; import { db } from '../../../../db'; -import { clients, members } from '../../../../db/schema'; +import { clients } from '../../../../db/schema'; import { eq, and } from 'drizzle-orm'; +import { getCurrentTeam } from '../../../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); @@ -11,20 +12,8 @@ if (!user) return Astro.redirect('/login'); const { id } = Astro.params; if (!id) return Astro.redirect('/dashboard/clients'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const client = await db.select() .from(clients) diff --git a/src/pages/dashboard/clients/[id]/index.astro b/src/pages/dashboard/clients/[id]/index.astro index 579c97e..4cedfdd 100644 --- a/src/pages/dashboard/clients/[id]/index.astro +++ b/src/pages/dashboard/clients/[id]/index.astro @@ -2,9 +2,11 @@ import DashboardLayout from '../../../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; import { db } from '../../../../db'; -import { clients, timeEntries, members, tags, users } from '../../../../db/schema'; +import { clients, timeEntries, tags, users } from '../../../../db/schema'; import { eq, and, desc, sql } from 'drizzle-orm'; import { formatTimeRange } from '../../../../lib/formatTime'; +import { getCurrentTeam } from '../../../../lib/getCurrentTeam'; +import StatCard from '../../../../components/StatCard.astro'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); @@ -12,20 +14,8 @@ if (!user) return Astro.redirect('/login'); const { id } = Astro.params; if (!id) return Astro.redirect('/dashboard/clients'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const client = await db.select() .from(clients) @@ -132,23 +122,20 @@ const totalEntriesCount = totalEntriesResult?.count || 0;
-
-
- -
-
Total Time Tracked
-
{totalHours}h {totalMinutes}m
-
Across all projects
-
- -
-
- -
-
Total Entries
-
{totalEntriesCount}
-
Recorded entries
-
+ +
diff --git a/src/pages/dashboard/index.astro b/src/pages/dashboard/index.astro index 1bce981..0b504de 100644 --- a/src/pages/dashboard/index.astro +++ b/src/pages/dashboard/index.astro @@ -1,6 +1,7 @@ --- import DashboardLayout from '../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; +import StatCard from '../../components/StatCard.astro'; import { db } from '../../db'; import { organizations, members, timeEntries, clients, tags } from '../../db/schema'; import { eq, desc, and, isNull, gte, sql } from 'drizzle-orm'; @@ -134,41 +135,38 @@ const hasMembership = userOrgs.length > 0; <>
-
-
- -
-
This Week
-
{formatDuration(stats.totalTimeThisWeek)}
-
Total tracked time
-
- -
-
- -
-
This Month
-
{formatDuration(stats.totalTimeThisMonth)}
-
Total tracked time
-
- -
-
- -
-
Active Timers
-
{stats.activeTimers}
-
Currently running
-
- -
-
- -
-
Clients
-
{stats.totalClients}
-
Total active
-
+ + + +
diff --git a/src/pages/dashboard/invoices/[id].astro b/src/pages/dashboard/invoices/[id].astro index e678317..15244a6 100644 --- a/src/pages/dashboard/invoices/[id].astro +++ b/src/pages/dashboard/invoices/[id].astro @@ -4,6 +4,7 @@ import { Icon } from 'astro-icon/components'; import { db } from '../../../db'; import { invoices, invoiceItems, clients, members, organizations } from '../../../db/schema'; import { eq, and } from 'drizzle-orm'; +import { formatCurrency } from '../../../lib/formatTime'; const { id } = Astro.params; const user = Astro.locals.user; @@ -49,13 +50,6 @@ const items = await db.select() .where(eq(invoiceItems.invoiceId, invoice.id)) .all(); -const formatCurrency = (amount: number) => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: invoice.currency, - }).format(amount / 100); -}; - const isDraft = invoice.status === 'draft'; --- @@ -235,8 +229,8 @@ const isDraft = invoice.status === 'draft'; {item.description} {item.quantity} - {formatCurrency(item.unitPrice)} - {formatCurrency(item.amount)} + {formatCurrency(item.unitPrice, invoice.currency)} + {formatCurrency(item.amount, invoice.currency)} {isDraft && (
@@ -299,7 +293,7 @@ const isDraft = invoice.status === 'draft';
Subtotal - {formatCurrency(invoice.subtotal)} + {formatCurrency(invoice.subtotal, invoice.currency)}
{(invoice.discountAmount && invoice.discountAmount > 0) && (
@@ -307,7 +301,7 @@ const isDraft = invoice.status === 'draft'; Discount {invoice.discountType === 'percentage' && ` (${invoice.discountValue}%)`} - -{formatCurrency(invoice.discountAmount)} + -{formatCurrency(invoice.discountAmount, invoice.currency)}
)} {((invoice.taxRate ?? 0) > 0 || isDraft) && ( @@ -320,13 +314,13 @@ const isDraft = invoice.status === 'draft'; )} - {formatCurrency(invoice.taxAmount)} + {formatCurrency(invoice.taxAmount, invoice.currency)}
)}
Total - {formatCurrency(invoice.total)} + {formatCurrency(invoice.total, invoice.currency)}
diff --git a/src/pages/dashboard/invoices/index.astro b/src/pages/dashboard/invoices/index.astro index e8da46c..0989e0e 100644 --- a/src/pages/dashboard/invoices/index.astro +++ b/src/pages/dashboard/invoices/index.astro @@ -1,27 +1,18 @@ --- import DashboardLayout from '../../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; +import StatCard from '../../../components/StatCard.astro'; import { db } from '../../../db'; -import { invoices, clients, members } from '../../../db/schema'; +import { invoices, clients } from '../../../db/schema'; import { eq, desc, and, gte, lte, sql } from 'drizzle-orm'; +import { getCurrentTeam } from '../../../lib/getCurrentTeam'; +import { formatCurrency } from '../../../lib/formatTime'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const currentTeamIdResolved = userMembership.organizationId; @@ -96,13 +87,6 @@ const yearInvoices = allInvoicesRaw.filter(i => { return issueDate >= yearStart && issueDate <= yearEnd; }); -const formatCurrency = (amount: number, currency: string) => { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currency, - }).format(amount / 100); -}; - const getStatusColor = (status: string) => { switch (status) { case 'paid': return 'badge-success'; @@ -130,40 +114,35 @@ const getStatusColor = (status: string) => {
-
-
- -
-
Total Invoices
-
{yearInvoices.filter(i => i.invoice.type === 'invoice').length}
-
{selectedYear === 'current' ? `${currentYear} (YTD)` : selectedYear}
-
+ i.invoice.type === 'invoice').length)} + description={selectedYear === 'current' ? `${currentYear} (YTD)` : selectedYear} + icon="heroicons:document-text" + color="text-primary" + />
-
-
- -
-
Open Quotes
-
{yearInvoices.filter(i => i.invoice.type === 'quote' && i.invoice.status === 'sent').length}
-
Waiting for approval
-
+ i.invoice.type === 'quote' && i.invoice.status === 'sent').length)} + description="Waiting for approval" + icon="heroicons:clipboard-document-list" + color="text-secondary" + />
-
-
- -
-
Total Revenue
-
- {formatCurrency(yearInvoices - .filter(i => i.invoice.type === 'invoice' && i.invoice.status === 'paid') - .reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')} -
-
Paid invoices ({selectedYear === 'current' ? `${currentYear} YTD` : selectedYear})
-
+ i.invoice.type === 'invoice' && i.invoice.status === 'paid') + .reduce((acc, curr) => acc + curr.invoice.total, 0), 'USD')} + description={`Paid invoices (${selectedYear === 'current' ? `${currentYear} YTD` : selectedYear})`} + icon="heroicons:currency-dollar" + color="text-success" + />
diff --git a/src/pages/dashboard/invoices/new.astro b/src/pages/dashboard/invoices/new.astro index 0f7a179..55d991f 100644 --- a/src/pages/dashboard/invoices/new.astro +++ b/src/pages/dashboard/invoices/new.astro @@ -2,26 +2,15 @@ import DashboardLayout from '../../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; import { db } from '../../../db'; -import { clients, members, invoices, organizations } from '../../../db/schema'; +import { clients, invoices, organizations } from '../../../db/schema'; import { eq, desc, and } from 'drizzle-orm'; +import { getCurrentTeam } from '../../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const currentTeamIdResolved = userMembership.organizationId; diff --git a/src/pages/dashboard/reports.astro b/src/pages/dashboard/reports.astro index 6b1b745..ad01641 100644 --- a/src/pages/dashboard/reports.astro +++ b/src/pages/dashboard/reports.astro @@ -1,31 +1,21 @@ --- import DashboardLayout from '../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; +import StatCard from '../../components/StatCard.astro'; import TagChart from '../../components/TagChart.vue'; import ClientChart from '../../components/ClientChart.vue'; import MemberChart from '../../components/MemberChart.vue'; import { db } from '../../db'; import { timeEntries, members, users, clients, tags, invoices } from '../../db/schema'; import { eq, and, gte, lte, sql, desc } from 'drizzle-orm'; -import { formatDuration, formatTimeRange } from '../../lib/formatTime'; +import { formatDuration, formatTimeRange, formatCurrency } from '../../lib/formatTime'; +import { getCurrentTeam } from '../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const teamMembers = await db.select({ id: users.id, @@ -247,13 +237,6 @@ const revenueByClient = allClients.map(client => { }; }).filter(s => s.revenue > 0).sort((a, b) => b.revenue - a.revenue); -function formatCurrency(amount: number, currency: string = 'USD') { - return new Intl.NumberFormat('en-US', { - style: 'currency', - currency: currency, - }).format(amount / 100); -} - function getTimeRangeLabel(range: string) { switch (range) { case 'today': return 'Today'; @@ -383,46 +366,44 @@ function getTimeRangeLabel(range: string) {
-
-
- -
-
Total Time
-
{formatDuration(totalTime)}
-
{getTimeRangeLabel(timeRange)}
-
+
-
-
- -
-
Total Entries
-
{entries.length}
-
{getTimeRangeLabel(timeRange)}
-
+
-
-
- -
-
Revenue
-
{formatCurrency(revenueStats.total)}
-
{invoiceStats.paid} paid invoices
-
+
-
-
- -
-
Active Members
-
{statsByMember.filter(s => s.entryCount > 0).length}
-
of {teamMembers.length} total
+ s.entryCount > 0).length)} + description={`of ${teamMembers.length} total`} + icon="heroicons:user-group" + color="text-accent" + /> +
diff --git a/src/pages/dashboard/settings.astro b/src/pages/dashboard/settings.astro index ed47cac..689c26e 100644 --- a/src/pages/dashboard/settings.astro +++ b/src/pages/dashboard/settings.astro @@ -50,10 +50,10 @@ const userPasskeys = await db.select() )} - + - + ({ diff --git a/src/pages/dashboard/team.astro b/src/pages/dashboard/team.astro index 504bee3..f268227 100644 --- a/src/pages/dashboard/team.astro +++ b/src/pages/dashboard/team.astro @@ -5,24 +5,13 @@ import { Icon } from 'astro-icon/components'; import { db } from '../../db'; import { members, users } from '../../db/schema'; import { eq } from 'drizzle-orm'; +import { getCurrentTeam } from '../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const teamMembers = await db.select({ member: members, diff --git a/src/pages/dashboard/team/settings.astro b/src/pages/dashboard/team/settings.astro index 1e7d533..f6d756a 100644 --- a/src/pages/dashboard/team/settings.astro +++ b/src/pages/dashboard/team/settings.astro @@ -2,26 +2,15 @@ import DashboardLayout from '../../../layouts/DashboardLayout.astro'; import { Icon } from 'astro-icon/components'; import { db } from '../../../db'; -import { members, organizations, tags } from '../../../db/schema'; +import { organizations, tags } from '../../../db/schema'; import { eq } from 'drizzle-orm'; +import { getCurrentTeam } from '../../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const isAdmin = userMembership.role === 'owner' || userMembership.role === 'admin'; if (!isAdmin) return Astro.redirect('/dashboard/team'); diff --git a/src/pages/dashboard/tracker.astro b/src/pages/dashboard/tracker.astro index a8218bb..6fecf3f 100644 --- a/src/pages/dashboard/tracker.astro +++ b/src/pages/dashboard/tracker.astro @@ -4,27 +4,16 @@ import { Icon } from 'astro-icon/components'; import Timer from '../../components/Timer.vue'; import ManualEntry from '../../components/ManualEntry.vue'; import { db } from '../../db'; -import { timeEntries, clients, members, tags, users } from '../../db/schema'; +import { timeEntries, clients, tags, users } from '../../db/schema'; import { eq, desc, asc, and, sql, or, like } from 'drizzle-orm'; import { formatTimeRange } from '../../lib/formatTime'; +import { getCurrentTeam } from '../../lib/getCurrentTeam'; const user = Astro.locals.user; if (!user) return Astro.redirect('/login'); -// Get current team from cookie -const currentTeamId = Astro.cookies.get('currentTeamId')?.value; - -const userMemberships = await db.select() - .from(members) - .where(eq(members.userId, user.id)) - .all(); - -if (userMemberships.length === 0) return Astro.redirect('/dashboard'); - -// Use current team or fallback to first membership -const userMembership = currentTeamId - ? userMemberships.find(m => m.organizationId === currentTeamId) || userMemberships[0] - : userMemberships[0]; +const userMembership = await getCurrentTeam(user, Astro.cookies.get('currentTeamId')?.value); +if (!userMembership) return Astro.redirect('/dashboard'); const organizationId = userMembership.organizationId; @@ -153,12 +142,12 @@ const paginationPages = getPaginationPages(page, totalPages);

Time Tracker

-
- -
+
+ +
{allClients.length === 0 ? (
- + You need to create a client before tracking time. Add Client
@@ -177,11 +166,11 @@ const paginationPages = getPaginationPages(page, totalPages); )}
- -
+ +
{allClients.length === 0 ? (
- + You need to create a client before adding time entries. Add Client
diff --git a/src/pages/uploads/[...path].ts b/src/pages/uploads/[...path].ts index 721405a..72e3ef2 100644 --- a/src/pages/uploads/[...path].ts +++ b/src/pages/uploads/[...path].ts @@ -52,10 +52,8 @@ export const GET: APIRoute = async ({ params }) => { case ".gif": contentType = "image/gif"; break; - case ".svg": - contentType = "image/svg+xml"; - break; - // WebP is intentionally omitted as it is not supported in PDF generation + // SVG excluded to prevent stored XSS + // WebP omitted — not supported in PDF generation } return new Response(fileContent, {