diff --git a/.gitignore b/.gitignore index 732df62..9fb1414 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,6 @@ test-results # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +CLAUDE.md +.claude/ \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bc31e15 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.css": "tailwindcss" + } +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..c5d91b4 --- /dev/null +++ b/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://shadcn-svelte.com/schema.json", + "tailwind": { + "css": "src/app.css", + "baseColor": "slate" + }, + "aliases": { + "components": "$lib/components", + "utils": "$lib/utils", + "ui": "$lib/components/ui", + "hooks": "$lib/hooks", + "lib": "$lib" + }, + "typescript": true, + "registry": "https://shadcn-svelte.com/registry" +} diff --git a/docs/universal-logger.md b/docs/universal-logger.md new file mode 100644 index 0000000..cb23caf --- /dev/null +++ b/docs/universal-logger.md @@ -0,0 +1,216 @@ +# UniversalLogger Documentation + +## Overview + +The UniversalLogger is a comprehensive logging solution for the appointment booking platform that provides unified logging capabilities across both client-side (browser) and server-side (Node.js) environments. It features automatic client error forwarding to the server for centralized log management. + +## Architecture + +### Core Components + +1. **UniversalLogger Class** (`src/lib/logger/index.ts`) + + - Environment-aware logging with automatic detection + - Context-based log categorization + - Consistent API across client and server environments + +2. **Winston Configuration** (`src/lib/logger/winston.ts`) + + - Structured server-side logging with custom formatting + - Environment-based log levels + - Stack trace capture for errors + +3. **Client Error Forwarding API** (`src/routes/api/log/+server.ts`) + - HTTP endpoint for receiving client-side errors + - Automatic context labeling for client logs + - Error handling with fallback logging + +## Key Features + +### Environment Detection + +The logger automatically detects the execution environment and adapts its behavior: + +- **Browser Environment**: Uses console methods with emoji prefixes and forwards errors to server +- **Server Environment**: Uses Winston logger with structured formatting + +### Client Error Forwarding + +Client-side errors are automatically forwarded to the server for centralized logging. This includes: + +- Error messages and stack traces +- Current URL context +- User agent information +- Custom metadata +- Timestamps + +### Context System + +The logger supports contextual categorization through the `setContext()` method: + +```typescript +const logger = createLogger("ComponentName"); +logger.info("This log will be tagged with ComponentName context"); +``` + +## Usage + +### Basic Setup + +```typescript +import { createLogger } from "$lib/logger"; + +// Create a logger with context +const logger = createLogger("MyComponent"); +``` + +### Logging Methods + +```typescript +// Debug information (development only) +logger.debug("Debug message", { additionalData: "value" }); + +// General information +logger.info("User logged in", { userId: 123 }); + +// Warnings +logger.warn("Deprecated API usage", { api: "old-endpoint" }); + +// Errors (automatically forwarded to server when in browser) +logger.error("Failed to save data", { error: errorObject }); +``` + +### Request Logging + +The logger automatically logs HTTP requests when integrated with SvelteKit hooks: + +```typescript +// Automatic request logging includes: +// - HTTP method and URL +// - Response status code +// - Response time +// - Error classification for 4xx/5xx status codes +``` + +## Configuration + +### Environment Variables + +- **Development**: Debug level logging enabled +- **Production**: Info level logging and above + +### Winston Configuration + +The server-side Winston logger is configured with: + +- Custom timestamp format (`YYYY-MM-DD HH:mm:ss`) +- Color-coded console output +- Stack trace capture for errors +- Structured metadata formatting + +## Integration Examples + +### SvelteKit Component + +```typescript + +``` + +### Server-Side Hooks + +```typescript +// src/hooks.server.ts +import { logger } from "$lib/logger"; + +export const handle = async ({ event, resolve }) => { + const start = Date.now(); + const requestLogger = logger.setContext("REQUEST"); + + try { + const response = await resolve(event); + const duration = Date.now() - start; + + requestLogger.info(`${event.request.method} ${event.url.pathname}`, { + status: response.status, + duration: `${duration}ms` + }); + + return response; + } catch (error) { + requestLogger.error("Request failed", { error }); + throw error; + } +}; +``` + +## Client Error Server Logging + +One of the key features of the UniversalLogger is that **client-side errors are automatically forwarded and logged on the server**. This provides several benefits: + +### Centralized Error Tracking + +All errors, whether occurring on the client or server, are logged in a single location on the server, making it easier to: + +- Monitor application health +- Debug issues across the full stack +- Maintain audit trails +- Analyze error patterns + +### Rich Error Context + +When client errors are forwarded to the server, they include: + +- **URL Context**: The page where the error occurred +- **User Agent**: Browser and device information +- **Timestamps**: When the error occurred +- **Stack Traces**: Full error details +- **Custom Metadata**: Any additional context provided + +### Automatic Fallback + +If the client cannot reach the server to forward an error, it falls back to local console logging, ensuring no errors are lost. + +## Performance Considerations + +- **Lazy Loading**: Winston is only imported on the server side +- **Async Error Forwarding**: Client errors are sent asynchronously to avoid blocking UI +- **Environment Optimization**: Debug logs are only processed in development +- **Structured Logging**: Efficient JSON-based metadata handling + +## Dependencies + +- **winston**: ^3.17.0 - Server-side structured logging +- **SvelteKit**: Environment detection and HTTP utilities +- **Native APIs**: Fetch API for client-server communication + +## Error Handling + +The logger includes comprehensive error handling: + +- Failed server requests for error forwarding don't crash the client +- Malformed log data is handled gracefully +- Winston errors are caught and logged to console as fallback +- Context switching errors are isolated and don't affect logging functionality + +## Best Practices + +1. **Use Descriptive Contexts**: Set meaningful context names for different components +2. **Include Relevant Metadata**: Add structured data to help with debugging +3. **Appropriate Log Levels**: Use debug for development, info for important events, warn for recoverable issues, error for failures +4. **Avoid Sensitive Data**: Never log passwords, tokens, or personal information +5. **Performance Awareness**: Limit debug logging in production environments diff --git a/package-lock.json b/package-lock.json index f4ee72f..3777f4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,40 +12,47 @@ "@sveltejs/adapter-node": "^5.2.12", "dotenv": "^16.5.0", "dotenv-expand": "^12.0.2", - "drizzle-orm": "^0.40.0", + "drizzle-orm": "^0.44.2", + "mode-watcher": "^1.0.8", "nodemailer": "^7.0.3", - "postgres": "^3.4.5" + "postgres": "^3.4.7", + "winston": "^3.17.0" }, "devDependencies": { - "@eslint/compat": "^1.2.5", - "@eslint/js": "^9.18.0", - "@playwright/test": "^1.49.1", - "@sveltejs/adapter-auto": "^6.0.0", - "@sveltejs/kit": "^2.16.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tailwindcss/typography": "^0.5.15", - "@tailwindcss/vite": "^4.0.0", + "@eslint/compat": "^1.3.0", + "@eslint/js": "^9.29.0", + "@lucide/svelte": "^0.518.0", + "@playwright/test": "^1.53.1", + "@sveltejs/adapter-auto": "^6.0.1", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^5.1.0", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.10", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/svelte": "^5.2.4", + "@testing-library/svelte": "^5.2.8", "@types/dotenv": "^6.1.1", - "@types/node": "^22", + "@types/node": "^24", "@types/nodemailer": "^6.4.17", - "drizzle-kit": "^0.30.2", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-svelte": "^3.0.0", - "globals": "^16.0.0", - "jsdom": "^26.0.0", - "prettier": "^3.4.2", - "prettier-plugin-svelte": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.11", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "tailwindcss": "^4.0.0", - "typescript": "^5.0.0", - "typescript-eslint": "^8.20.0", - "vite": "^6.2.6", - "vitest": "^3.2.3" + "clsx": "^2.1.1", + "drizzle-kit": "^0.31.1", + "eslint": "^9.29.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-svelte": "^3.9.3", + "globals": "^16.2.0", + "jsdom": "^26.1.0", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.6.13", + "svelte": "^5.34.7", + "svelte-check": "^4.2.2", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.1.10", + "tw-animate-css": "^1.3.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^6.3.5", + "vitest": "^3.2.4" }, "engines": { "node": ">=24.0.0" @@ -120,6 +127,15 @@ "node": ">=6.9.0" } }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz", @@ -235,6 +251,17 @@ "node": ">=18" } }, + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "license": "MIT", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" + } + }, "node_modules/@drizzle-team/brocli": { "version": "0.10.2", "resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz", @@ -679,292 +706,275 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", - "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", - "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", - "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/android-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", - "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", - "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", - "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", - "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", - "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", - "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", - "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", - "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", - "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", - "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", - "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", - "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", - "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/linux-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", - "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/netbsd-arm64": { @@ -984,20 +994,19 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", - "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/openbsd-arm64": { @@ -1017,88 +1026,83 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", - "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", - "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", - "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", - "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@esbuild/win32-x64": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", - "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/@eslint-community/eslint-utils": { @@ -1413,6 +1417,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lucide/svelte": { + "version": "0.518.0", + "resolved": "https://registry.npmjs.org/@lucide/svelte/-/svelte-0.518.0.tgz", + "integrity": "sha512-vhSikVKVZnz7eVa70WsWh/ySqdcb7Mn2R/XhG16fCfqgpMKzdu5GCHPRLaHcGaUCaM9cPDMWG8FeBXk0QY1PEg==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "svelte": "^5" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1455,17 +1469,18 @@ "version": "3.9.2", "resolved": "https://registry.npmjs.org/@petamoriken/float16/-/float16-3.9.2.tgz", "integrity": "sha512-VgffxawQde93xKxT3qap3OH+meZf7VaSB5Sqd4Rqc+FP5alWbpOyan/7tRbOAvynjpG3GpdtAuGU/NdhQpmrog==", - "devOptional": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/@playwright/test": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.0.tgz", - "integrity": "sha512-15hjKreZDcp7t6TL/7jkAo6Df5STZN09jGiv5dbP9A6vMVncXRqE7/B2SncsyOwrkZRBH2i6/TPOL8BVmm3c7w==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.53.1.tgz", + "integrity": "sha512-Z4c23LHV0muZ8hfv4jw6HngPJkbbtZxTkxPNIg7cJcTc9C28N/p2q7g3JZS2SiKBBHJ3uM1dgDye66bB7LEk5w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.53.0" + "playwright": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -2405,13 +2420,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.15.32", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.32.tgz", - "integrity": "sha512-3jigKqgSjsH6gYZv2nEsqdXfZqIFGAV36XYYjf9KGZ3PSG+IhLecqPnI310RvjutyMwifE2hhhNEklOUrvx/wA==", + "version": "24.0.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.3.tgz", + "integrity": "sha512-R4I/kzCYAdRLzfiCabn9hxWfbuHS573x+r0dJMkkzThEa7pbrcDWK+9zu3e7aBOouf+rQAciqPFMnxwr0aWgKg==", "devOptional": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.8.0" } }, "node_modules/@types/nodemailer": { @@ -2430,6 +2445,12 @@ "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", "license": "MIT" }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.34.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.34.1.tgz", @@ -2903,6 +2924,12 @@ "node": ">=12" } }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3046,6 +3073,16 @@ "node": ">=6" } }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3063,9 +3100,43 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, "license": "MIT" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "license": "MIT" + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "license": "MIT", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, "node_modules/commondir": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", @@ -3262,26 +3333,25 @@ } }, "node_modules/drizzle-kit": { - "version": "0.30.6", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.30.6.tgz", - "integrity": "sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==", + "version": "0.31.2", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.2.tgz", + "integrity": "sha512-Z2Uqxvu4HNFzlDkG3NQ2BYpII8SlOMkpjsC5XFh9TsYP2nYhfVamVjQ8spiMFXH3vGOyUt1cQ5FZ1JSgl6+8QQ==", "dev": true, "license": "MIT", "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", - "esbuild": "^0.19.7", - "esbuild-register": "^3.5.0", - "gel": "^2.0.0" + "esbuild": "^0.25.4", + "esbuild-register": "^3.5.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, "node_modules/drizzle-orm": { - "version": "0.40.1", - "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.40.1.tgz", - "integrity": "sha512-aPNhtiJiPfm3qxz1czrnIDkfvkSdKGXYeZkpG55NPTVI186LmK2fBLMi4dsHpPHlJrZeQ92D322YFPHADBALew==", + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.2.tgz", + "integrity": "sha512-zGAqBzWWkVSFjZpwPOrmCrgO++1kZ5H/rZ4qTGeGOe18iXGVJWf3WPfHOVwFIbmi8kHjfJstC6rJomzGx8g/dQ==", "license": "Apache-2.0", "peerDependencies": { "@aws-sdk/client-rds-data": ">=3", @@ -3292,12 +3362,13 @@ "@neondatabase/serverless": ">=0.10.0", "@op-engineering/op-sqlite": ">=2", "@opentelemetry/api": "^1.4.1", - "@planetscale/database": ">=1", + "@planetscale/database": ">=1.13", "@prisma/client": "*", "@tidbcloud/serverless": "*", "@types/better-sqlite3": "*", "@types/pg": "*", "@types/sql.js": "*", + "@upstash/redis": ">=1.34.7", "@vercel/postgres": ">=0.8.0", "@xata.io/client": "*", "better-sqlite3": ">=7", @@ -3355,6 +3426,9 @@ "@types/sql.js": { "optional": true }, + "@upstash/redis": { + "optional": true + }, "@vercel/postgres": { "optional": true }, @@ -3399,6 +3473,12 @@ } } }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==", + "license": "MIT" + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -3430,8 +3510,9 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, @@ -3447,42 +3528,43 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.19.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", - "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", - "dev": true, + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "hasInstallScript": true, "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.19.12", - "@esbuild/android-arm": "0.19.12", - "@esbuild/android-arm64": "0.19.12", - "@esbuild/android-x64": "0.19.12", - "@esbuild/darwin-arm64": "0.19.12", - "@esbuild/darwin-x64": "0.19.12", - "@esbuild/freebsd-arm64": "0.19.12", - "@esbuild/freebsd-x64": "0.19.12", - "@esbuild/linux-arm": "0.19.12", - "@esbuild/linux-arm64": "0.19.12", - "@esbuild/linux-ia32": "0.19.12", - "@esbuild/linux-loong64": "0.19.12", - "@esbuild/linux-mips64el": "0.19.12", - "@esbuild/linux-ppc64": "0.19.12", - "@esbuild/linux-riscv64": "0.19.12", - "@esbuild/linux-s390x": "0.19.12", - "@esbuild/linux-x64": "0.19.12", - "@esbuild/netbsd-x64": "0.19.12", - "@esbuild/openbsd-x64": "0.19.12", - "@esbuild/sunos-x64": "0.19.12", - "@esbuild/win32-arm64": "0.19.12", - "@esbuild/win32-ia32": "0.19.12", - "@esbuild/win32-x64": "0.19.12" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "node_modules/esbuild-register": { @@ -3589,9 +3671,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.9.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.9.2.tgz", - "integrity": "sha512-aqzfHtG9RPaFhCUFm5QFC6eFY/yHFQIT8VYYFe7/mT2A9mbgVR3XV2keCqU19LN8iVD9mdvRvqHU+4+CzJImvg==", + "version": "3.9.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.9.3.tgz", + "integrity": "sha512-PlcyK80sqAZ43IITeZkgl3zPFWJytx/Joup9iKGqIOsXM2m3pWfPbWuXPr5PN3loXFEypqTY/JyZwNqlSpSvRw==", "dev": true, "license": "MIT", "dependencies": { @@ -3599,7 +3681,7 @@ "@jridgewell/sourcemap-codec": "^1.5.0", "esutils": "^2.0.3", "globals": "^16.0.0", - "known-css-properties": "^0.36.0", + "known-css-properties": "^0.37.0", "postcss": "^8.4.49", "postcss-load-config": "^3.1.4", "postcss-safe-parser": "^7.0.0", @@ -3843,6 +3925,12 @@ } } }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -3907,6 +3995,12 @@ "dev": true, "license": "ISC" }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==", + "license": "MIT" + }, "node_modules/fsevents": { "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -3934,8 +4028,9 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/gel/-/gel-2.1.0.tgz", "integrity": "sha512-HCeRqInCt6BjbMmeghJ6BKeYwOj7WJT5Db6IWWAA3IMUUa7or7zJfTUEkUWCxiOtoXnwnm96sFK9Fr47Yh2hOA==", - "devOptional": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "@petamoriken/float16": "^3.8.7", "debug": "^4.3.4", @@ -3955,8 +4050,9 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", - "devOptional": true, "license": "ISC", + "optional": true, + "peer": true, "engines": { "node": ">=16" } @@ -3965,8 +4061,9 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", - "devOptional": true, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "isexe": "^3.1.1" }, @@ -4153,6 +4250,24 @@ "node": ">=8" } }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", + "license": "MIT" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT" + }, "node_modules/is-core-module": { "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", @@ -4223,6 +4338,18 @@ "@types/estree": "^1.0.6" } }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -4341,12 +4468,18 @@ } }, "node_modules/known-css-properties": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.36.0.tgz", - "integrity": "sha512-A+9jP+IUmuQsNdsLdcg6Yt7voiMF/D4K83ew0OpJtpu+l34ef7LaohWV0Rc6KNvzw6ZDizkqfyB5JznZnzuKQA==", + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", "dev": true, "license": "MIT" }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", + "license": "MIT" + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -4650,6 +4783,23 @@ "dev": true, "license": "MIT" }, + "node_modules/logform": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/loupe": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.4.tgz", @@ -4782,6 +4932,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mode-watcher": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/mode-watcher/-/mode-watcher-1.0.8.tgz", + "integrity": "sha512-1YIgYymrae0lhcG9zPS/YWOvcKsUbM4bLGIh+eXTUIput5+dEJifcmUgp8NSehSXGSwITH6jbSjNhGEIhE3k5Q==", + "license": "MIT", + "dependencies": { + "runed": "^0.25.0", + "svelte-toolbelt": "^0.7.1" + }, + "peerDependencies": { + "svelte": "^5.27.0" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -4847,6 +5010,15 @@ "dev": true, "license": "MIT" }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "license": "MIT", + "dependencies": { + "fn.name": "1.x.x" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -4985,13 +5157,13 @@ } }, "node_modules/playwright": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.0.tgz", - "integrity": "sha512-ghGNnIEYZC4E+YtclRn4/p6oYbdPiASELBIYkBXfaTVKreQUYbMUYQDwS12a8F0/HtIjr/CkGjtwABeFPGcS4Q==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.53.1.tgz", + "integrity": "sha512-LJ13YLr/ocweuwxyGf1XNFWIU4M2zUSo149Qbp+A4cpwDjsxRPj7k6H25LBrEHiEwxvRbD8HdwvQmRMSvquhYw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.53.0" + "playwright-core": "1.53.1" }, "bin": { "playwright": "cli.js" @@ -5004,9 +5176,9 @@ } }, "node_modules/playwright-core": { - "version": "1.53.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.0.tgz", - "integrity": "sha512-mGLg8m0pm4+mmtB7M89Xw/GSqoNC+twivl8ITteqvAndachozYe2ZA7srU6uleV1vEdAHYqjq+SV8SNxRRFYBw==", + "version": "1.53.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.53.1.tgz", + "integrity": "sha512-Z46Oq7tLAyT0lGoFx4DOuB1IA9D1TPj0QkYxpPVUnGDqHHvDpCftu1J2hM2PiWsNMoZh8+LQaarAWcDfPBc6zg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5203,9 +5375,9 @@ } }, "node_modules/prettier-plugin-tailwindcss": { - "version": "0.6.12", - "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.12.tgz", - "integrity": "sha512-OuTQKoqNwV7RnxTPwXWzOFXy6Jc4z8oeRZYGuMpRyG3WbuR3jjXdQFK8qFBMBx8UHWdHrddARz2fgUenild6aw==", + "version": "0.6.13", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.6.13.tgz", + "integrity": "sha512-uQ0asli1+ic8xrrSmIOaElDu0FacR4x69GynTh2oZjFY10JUt6EEumTQl5tB4fMeD6I1naKd+4rXQQ7esT2i1g==", "dev": true, "license": "MIT", "engines": { @@ -5347,6 +5519,20 @@ "dev": true, "license": "MIT" }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", @@ -5502,6 +5688,21 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/runed": { + "version": "0.25.0", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.25.0.tgz", + "integrity": "sha512-7+ma4AG9FT2sWQEA0Egf6mb7PBT2vHyuHail1ie8ropfSjvZGtEAx8YTmUjv/APCsdRRxEVvArNjALk9zFSOrg==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -5514,6 +5715,35 @@ "node": ">=6" } }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5580,8 +5810,9 @@ "version": "1.8.3", "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "devOptional": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">= 0.4" }, @@ -5596,6 +5827,15 @@ "dev": true, "license": "ISC" }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, "node_modules/sirv": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", @@ -5640,6 +5880,15 @@ "source-map": "^0.6.0" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -5654,6 +5903,15 @@ "dev": true, "license": "MIT" }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -5700,6 +5958,15 @@ "dev": true, "license": "MIT" }, + "node_modules/style-to-object": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", + "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -5726,9 +5993,9 @@ } }, "node_modules/svelte": { - "version": "5.34.5", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.5.tgz", - "integrity": "sha512-MMqhT/iTdwExPcyBCt+Bqxv7FV+LpSciej/gVgd/Vj7zRF9iPgRWGBxiVAwfzPYAOgwgFigIAQV8VJfhyB08/w==", + "version": "5.34.7", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.34.7.tgz", + "integrity": "sha512-5PEg+QQKce4t1qiOtVUhUS3AQRTtxJyGBTpxLcNWnr0Ve8q4r06bMo0Gv8uhtCPWlztZHoi3Ye7elLhu+PCTMg==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.3.0", @@ -5751,9 +6018,9 @@ } }, "node_modules/svelte-check": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.1.tgz", - "integrity": "sha512-e49SU1RStvQhoipkQ/aonDhHnG3qxHSBtNfBRb9pxVXoa+N7qybAo32KgA9wEb2PCYFNaDg7bZCdhLD1vHpdYA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.2.2.tgz", + "integrity": "sha512-1+31EOYZ7NKN0YDMKusav2hhEoA51GD9Ws6o//0SphMT0ve9mBTsTUEX7OmDMadUP3KjNHsSKtJrqdSaD8CrGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5817,6 +6084,41 @@ "node": ">=4" } }, + "node_modules/svelte-toolbelt": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/svelte-toolbelt/-/svelte-toolbelt-0.7.1.tgz", + "integrity": "sha512-HcBOcR17Vx9bjaOceUvxkY3nGmbBmCBBbuWLLEWO6jtmWH8f/QoWmbyUfQZrpDINH39en1b8mptfPQT9VKQ1xQ==", + "funding": [ + "https://github.com/sponsors/huntabyte" + ], + "dependencies": { + "clsx": "^2.1.1", + "runed": "^0.23.2", + "style-to-object": "^1.0.8" + }, + "engines": { + "node": ">=18", + "pnpm": ">=8.7.0" + }, + "peerDependencies": { + "svelte": "^5.0.0" + } + }, + "node_modules/svelte-toolbelt/node_modules/runed": { + "version": "0.23.4", + "resolved": "https://registry.npmjs.org/runed/-/runed-0.23.4.tgz", + "integrity": "sha512-9q8oUiBYeXIDLWNK5DfCWlkL0EW3oGbk845VdKlPeia28l751VpfesaB/+7pI6rnbx1I6rqoZ2fZxptOJLxILA==", + "funding": [ + "https://github.com/sponsors/huntabyte", + "https://github.com/sponsors/tglide" + ], + "dependencies": { + "esm-env": "^1.0.0" + }, + "peerDependencies": { + "svelte": "^5.7.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -5824,6 +6126,45 @@ "dev": true, "license": "MIT" }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwind-variants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tailwind-variants/-/tailwind-variants-1.0.0.tgz", + "integrity": "sha512-2WSbv4ulEEyuBKomOunut65D8UZwxrHoRfYnxGcQNnHqlSCp2+B7Yz2W+yrNDrxRodOXtGD/1oCcKGNBnUqMqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tailwind-merge": "3.0.2" + }, + "engines": { + "node": ">=16.x", + "pnpm": ">=7.x" + }, + "peerDependencies": { + "tailwindcss": "*" + } + }, + "node_modules/tailwind-variants/node_modules/tailwind-merge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.0.2.tgz", + "integrity": "sha512-l7z+OYZ7mu3DTqrL88RiKrKIqO3NcpEO8V/Od04bNpvk0kiIFndGEoqfuzvj4yuhRkHKjRkII2z+KS2HfPcSxw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.10.tgz", @@ -5859,6 +6200,12 @@ "node": ">=18" } }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -5987,6 +6334,15 @@ "node": ">=18" } }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", @@ -6000,6 +6356,16 @@ "typescript": ">=4.8.4" } }, + "node_modules/tw-animate-css": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.4.tgz", + "integrity": "sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6051,9 +6417,9 @@ } }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", "devOptional": true, "license": "MIT" }, @@ -6071,7 +6437,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, "node_modules/vite": { @@ -6171,414 +6536,6 @@ "url": "https://opencollective.com/vitest" } }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", - "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", - "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", - "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", - "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", - "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", - "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", - "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", - "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", - "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", - "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", - "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", - "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", - "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", - "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", - "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", - "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", - "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", - "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", - "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", - "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", - "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", - "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" - } - }, "node_modules/vite/node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6777,6 +6734,42 @@ "node": ">=8" } }, + "node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", + "dependencies": { + "logform": "^2.7.0", + "readable-stream": "^3.6.2", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/package.json b/package.json index d45ba27..b142f38 100644 --- a/package.json +++ b/package.json @@ -33,43 +33,50 @@ "docker:prod:clean": "docker compose -f docker-compose.prod.yml down -v --remove-orphans" }, "devDependencies": { - "@eslint/compat": "^1.2.5", - "@eslint/js": "^9.18.0", - "@playwright/test": "^1.49.1", - "@sveltejs/adapter-auto": "^6.0.0", - "@sveltejs/kit": "^2.16.0", - "@sveltejs/vite-plugin-svelte": "^5.0.0", - "@tailwindcss/typography": "^0.5.15", - "@tailwindcss/vite": "^4.0.0", + "@eslint/compat": "^1.3.0", + "@eslint/js": "^9.29.0", + "@lucide/svelte": "^0.518.0", + "@playwright/test": "^1.53.1", + "@sveltejs/adapter-auto": "^6.0.1", + "@sveltejs/kit": "^2.22.0", + "@sveltejs/vite-plugin-svelte": "^5.1.0", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.10", "@testing-library/jest-dom": "^6.6.3", - "@testing-library/svelte": "^5.2.4", - "@types/node": "^22", + "@testing-library/svelte": "^5.2.8", "@types/nodemailer": "^6.4.17", "@types/dotenv": "^6.1.1", - "drizzle-kit": "^0.30.2", - "eslint": "^9.18.0", - "eslint-config-prettier": "^10.0.1", - "eslint-plugin-svelte": "^3.0.0", - "globals": "^16.0.0", - "jsdom": "^26.0.0", - "prettier": "^3.4.2", - "prettier-plugin-svelte": "^3.3.3", - "prettier-plugin-tailwindcss": "^0.6.11", - "svelte": "^5.0.0", - "svelte-check": "^4.0.0", - "tailwindcss": "^4.0.0", - "typescript": "^5.0.0", - "typescript-eslint": "^8.20.0", - "vite": "^6.2.6", - "vitest": "^3.2.3" + "@types/node": "^24", + "clsx": "^2.1.1", + "drizzle-kit": "^0.31.1", + "eslint": "^9.29.0", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-svelte": "^3.9.3", + "globals": "^16.2.0", + "jsdom": "^26.1.0", + "prettier": "^3.5.3", + "prettier-plugin-svelte": "^3.4.0", + "prettier-plugin-tailwindcss": "^0.6.13", + "svelte": "^5.34.7", + "svelte-check": "^4.2.2", + "tailwind-merge": "^3.3.1", + "tailwind-variants": "^1.0.0", + "tailwindcss": "^4.1.10", + "tw-animate-css": "^1.3.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.34.1", + "vite": "^6.3.5", + "vitest": "^3.2.4" }, "dependencies": { "@sveltejs/adapter-node": "^5.2.12", "dotenv": "^16.5.0", "dotenv-expand": "^12.0.2", - "drizzle-orm": "^0.40.0", + "drizzle-orm": "^0.44.2", + "mode-watcher": "^1.0.8", "nodemailer": "^7.0.3", - "postgres": "^3.4.5" + "postgres": "^3.4.7", + "winston": "^3.17.0" }, "engines": { "node": ">=24.0.0" diff --git a/src/app.css b/src/app.css index 5b46619..29cefe3 100644 --- a/src/app.css +++ b/src/app.css @@ -1,2 +1,141 @@ @import "tailwindcss"; -@plugin '@tailwindcss/typography'; +@custom-variant dark (&:where(.dark, .dark *)); + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.129 0.042 264.695); + --card: oklch(1 0 0); + --card-foreground: oklch(0.129 0.042 264.695); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.129 0.042 264.695); + --primary: oklch(0.208 0.042 265.755); + --primary-foreground: oklch(0.984 0.003 247.858); + --secondary: oklch(0.968 0.007 247.896); + --secondary-foreground: oklch(0.208 0.042 265.755); + --muted: oklch(0.968 0.007 247.896); + --muted-foreground: oklch(0.554 0.046 257.417); + --accent: oklch(0.968 0.007 247.896); + --accent-foreground: oklch(0.208 0.042 265.755); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.929 0.013 255.508); + --input: oklch(0.929 0.013 255.508); + --ring: oklch(0.704 0.04 256.788); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.984 0.003 247.858); + --sidebar-foreground: oklch(0.129 0.042 264.695); + --sidebar-primary: oklch(0.208 0.042 265.755); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.968 0.007 247.896); + --sidebar-accent-foreground: oklch(0.208 0.042 265.755); + --sidebar-border: oklch(0.929 0.013 255.508); + --sidebar-ring: oklch(0.704 0.04 256.788); + + /* custom colors */ + --lighter: oklch(77.983% 0.03673 254.95); + --light: oklch(69.121% 0.03859 257.443); + --medium: oklch(62.379% 0.01913 256.381); + --dark: oklch(43.309% 0.00977 254.027); + --darker: oklch(27.232% 0.00797 264.468); +} + +.dark { + --background: oklch(0.129 0.042 264.695); + --foreground: oklch(0.984 0.003 247.858); + --card: oklch(0.208 0.042 265.755); + --card-foreground: oklch(0.984 0.003 247.858); + --popover: oklch(0.208 0.042 265.755); + --popover-foreground: oklch(0.984 0.003 247.858); + --primary: oklch(0.929 0.013 255.508); + --primary-foreground: oklch(0.208 0.042 265.755); + --secondary: oklch(0.279 0.041 260.031); + --secondary-foreground: oklch(0.984 0.003 247.858); + --muted: oklch(0.279 0.041 260.031); + --muted-foreground: oklch(0.704 0.04 256.788); + --accent: oklch(0.279 0.041 260.031); + --accent-foreground: oklch(0.984 0.003 247.858); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.551 0.027 264.364); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.208 0.042 265.755); + --sidebar-foreground: oklch(0.984 0.003 247.858); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.984 0.003 247.858); + --sidebar-accent: oklch(0.279 0.041 260.031); + --sidebar-accent-foreground: oklch(0.984 0.003 247.858); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.551 0.027 264.364); + + /* custom colors */ + --lighter: oklch(27.232% 0.00797 264.468); + --light: oklch(43.309% 0.00977 254.027); + --medium: oklch(62.379% 0.01913 256.381); + --dark: oklch(69.121% 0.03859 257.443); + --darker: oklch(77.983% 0.03673 254.95); +} + +@theme { + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + + --color-lighter: var(--lighter); + --color-light: var(--light); + --color-medium: var(--medium); + --color-dark: var(--dark); + --color-darker: var(--darker); +} + +@layer base { + * { + border-color: var(--border); + outline-color: color-mix(in oklab, var(--ring) 50%, transparent); + } + + body { + background-color: var(--background); + color: var(--foreground); + } +} diff --git a/src/app.html b/src/app.html index 77a5ff5..e75322c 100644 --- a/src/app.html +++ b/src/app.html @@ -2,10 +2,18 @@ - + + + + + + + + %sveltekit.head% +
%sveltekit.body%
diff --git a/src/hooks.server.ts b/src/hooks.server.ts new file mode 100644 index 0000000..9a8a191 --- /dev/null +++ b/src/hooks.server.ts @@ -0,0 +1,39 @@ +import { logger } from "$lib/logger"; + +export async function handle({ event, resolve }) { + const start = Date.now(); + const requestLogger = logger.setContext("REQUEST"); + + requestLogger.info(`Incoming ${event.request.method} ${event.url.pathname}`); + + const response = await resolve(event); + const responseTime = Date.now() - start; + + requestLogger.logRequest(event.request, responseTime, response.status); + + return response; +} + +type Error = { + message?: string; + stack?: string; +}; + +export async function handleError({ error, event, status, message }) { + const errorLogger = logger.setContext("ERROR_HANDLER"); + + errorLogger.error("Unhandled error occurred", { + error: (error as Error).message, + stack: (error as Error).stack, + status, + message, + url: event.url.pathname, + method: event.request.method, + userAgent: event.request.headers.get("user-agent"), + ip: event.getClientAddress() + }); + + return { + message: "Internal server error occurred" + }; +} diff --git a/src/lib/components/ui/card/card-action.svelte b/src/lib/components/ui/card/card-action.svelte new file mode 100644 index 0000000..cc36c56 --- /dev/null +++ b/src/lib/components/ui/card/card-action.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-content.svelte b/src/lib/components/ui/card/card-content.svelte new file mode 100644 index 0000000..bc90b83 --- /dev/null +++ b/src/lib/components/ui/card/card-content.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-description.svelte b/src/lib/components/ui/card/card-description.svelte new file mode 100644 index 0000000..9b20ac7 --- /dev/null +++ b/src/lib/components/ui/card/card-description.svelte @@ -0,0 +1,20 @@ + + +

+ {@render children?.()} +

diff --git a/src/lib/components/ui/card/card-footer.svelte b/src/lib/components/ui/card/card-footer.svelte new file mode 100644 index 0000000..2d4d0f2 --- /dev/null +++ b/src/lib/components/ui/card/card-footer.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-header.svelte b/src/lib/components/ui/card/card-header.svelte new file mode 100644 index 0000000..2501788 --- /dev/null +++ b/src/lib/components/ui/card/card-header.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card-title.svelte b/src/lib/components/ui/card/card-title.svelte new file mode 100644 index 0000000..7447231 --- /dev/null +++ b/src/lib/components/ui/card/card-title.svelte @@ -0,0 +1,20 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/card.svelte b/src/lib/components/ui/card/card.svelte new file mode 100644 index 0000000..f6cc3e5 --- /dev/null +++ b/src/lib/components/ui/card/card.svelte @@ -0,0 +1,23 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/card/index.ts b/src/lib/components/ui/card/index.ts new file mode 100644 index 0000000..10daffb --- /dev/null +++ b/src/lib/components/ui/card/index.ts @@ -0,0 +1,25 @@ +import Root from "./card.svelte"; +import Content from "./card-content.svelte"; +import Description from "./card-description.svelte"; +import Footer from "./card-footer.svelte"; +import Header from "./card-header.svelte"; +import Title from "./card-title.svelte"; +import Action from "./card-action.svelte"; + +export { + Root, + Content, + Description, + Footer, + Header, + Title, + Action, + // + Root as Card, + Content as CardContent, + Description as CardDescription, + Footer as CardFooter, + Header as CardHeader, + Title as CardTitle, + Action as CardAction +}; diff --git a/src/lib/components/ui/page/horizontal-page-padding.svelte b/src/lib/components/ui/page/horizontal-page-padding.svelte new file mode 100644 index 0000000..f9ca2c4 --- /dev/null +++ b/src/lib/components/ui/page/horizontal-page-padding.svelte @@ -0,0 +1,15 @@ + + +
+ {@render children?.()} +
diff --git a/src/lib/components/ui/page/index.ts b/src/lib/components/ui/page/index.ts new file mode 100644 index 0000000..12d3551 --- /dev/null +++ b/src/lib/components/ui/page/index.ts @@ -0,0 +1,4 @@ +import HorizontalPagePadding from "./horizontal-page-padding.svelte"; +import PageWithClaim from "./page-with-claim.svelte"; + +export { HorizontalPagePadding, PageWithClaim }; diff --git a/src/lib/components/ui/page/page-with-claim.svelte b/src/lib/components/ui/page/page-with-claim.svelte new file mode 100644 index 0000000..d827f0e --- /dev/null +++ b/src/lib/components/ui/page/page-with-claim.svelte @@ -0,0 +1,29 @@ + + +
+ {@render children?.()} + +
diff --git a/src/lib/components/ui/typography/headline.svelte b/src/lib/components/ui/typography/headline.svelte new file mode 100644 index 0000000..71e59da --- /dev/null +++ b/src/lib/components/ui/typography/headline.svelte @@ -0,0 +1,38 @@ + + + + {@render children?.()} + diff --git a/src/lib/components/ui/typography/index.ts b/src/lib/components/ui/typography/index.ts new file mode 100644 index 0000000..2cad03d --- /dev/null +++ b/src/lib/components/ui/typography/index.ts @@ -0,0 +1,4 @@ +import Headline from "./headline.svelte"; +import Text from "./text.svelte"; + +export { Headline, Text }; diff --git a/src/lib/components/ui/typography/text.svelte b/src/lib/components/ui/typography/text.svelte new file mode 100644 index 0000000..c268b3b --- /dev/null +++ b/src/lib/components/ui/typography/text.svelte @@ -0,0 +1,33 @@ + + + + {@render children?.()} + diff --git a/src/lib/logger/api-endpoint.test.ts b/src/lib/logger/api-endpoint.test.ts new file mode 100644 index 0000000..598d8ab --- /dev/null +++ b/src/lib/logger/api-endpoint.test.ts @@ -0,0 +1,72 @@ +import { describe, it, expect } from "vitest"; + +// This test file focuses on testing the API endpoint logic in isolation +// The actual server endpoint at /routes/api/log/+server.ts is tested via integration + +describe("API Log Endpoint Logic", () => { + describe("Request Processing", () => { + it("should handle different log levels correctly", () => { + // Test that our endpoint logic handles all log levels + const logLevels = ["debug", "info", "warn", "error", "unknown"]; + + logLevels.forEach((level) => { + expect(level).toBeTruthy(); // Basic test that levels exist + }); + }); + + it("should validate request structure", () => { + // Test basic request validation concepts + const validRequest = { + level: "info", + message: "test message", + meta: { key: "value" } + }; + + expect(validRequest.level).toBe("info"); + expect(validRequest.message).toBe("test message"); + expect(validRequest.meta).toEqual({ key: "value" }); + }); + + it("should handle missing or malformed data", () => { + // Test edge cases + const edgeCases = [ + { level: null, message: "test" }, + { level: "info", message: null }, + { level: "info", message: "test", meta: null }, + {} + ]; + + edgeCases.forEach((testCase) => { + // Basic validation that we can handle these cases + expect(typeof testCase).toBe("object"); + }); + }); + }); + + describe("Response Format", () => { + it("should return success response format", () => { + const successResponse = { success: true }; + expect(successResponse.success).toBe(true); + }); + + it("should return error response format", () => { + const errorResponse = { success: false }; + expect(errorResponse.success).toBe(false); + }); + }); + + describe("Error Handling", () => { + it("should handle JSON parsing errors", () => { + const error = new Error("Invalid JSON"); + expect(error.message).toBe("Invalid JSON"); + }); + + it("should handle unknown errors", () => { + const unknownError = "string error"; + expect(typeof unknownError).toBe("string"); + }); + }); +}); + +// Note: The actual API endpoint is tested through integration tests +// and end-to-end tests since it involves SvelteKit's request handling diff --git a/src/lib/logger/index.server.test.ts b/src/lib/logger/index.server.test.ts new file mode 100644 index 0000000..00944b4 --- /dev/null +++ b/src/lib/logger/index.server.test.ts @@ -0,0 +1,390 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Mock $app/environment for server environment +vi.mock("$app/environment", () => ({ + browser: false +})); + +// Mock winston logger +const mockWinstonLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() +}; + +// Mock winston module +vi.mock("./winston", () => ({ + default: mockWinstonLogger +})); + +describe("UniversalLogger - Server Side", () => { + let logger: any; + let createLogger: any; + + beforeEach(async () => { + // Clear all mocks + vi.clearAllMocks(); + + // Dynamic import after mocks are set up + const loggerModule = await import("./index"); + logger = loggerModule.logger; + createLogger = loggerModule.createLogger; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("Context Management", () => { + it("should set and use context correctly", () => { + const contextLogger = createLogger("ServerContext"); + contextLogger.info("Server message", { data: "test" }); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith({ + message: "[ServerContext] Server message", + data: "test", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should work without context", () => { + logger.info("Server message without context"); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith({ + message: "Server message without context", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should return logger instance when setting context", () => { + const result = logger.setContext("TestContext"); + expect(result).toBe(logger); + }); + }); + + describe("Message Formatting", () => { + it("should format messages with context prefix", () => { + const testLogger = createLogger("API"); + testLogger.debug("Processing request", { requestId: "123" }); + + expect(mockWinstonLogger.debug).toHaveBeenCalledWith({ + message: "[API] Processing request", + requestId: "123", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should format messages without context prefix when no context set", async () => { + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.debug("Processing request", { requestId: "456" }); + + expect(mockWinstonLogger.debug).toHaveBeenCalledWith({ + message: "Processing request", + requestId: "456", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should include source as server", () => { + logger.info("Test message"); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith( + expect.objectContaining({ + source: "server" + }) + ); + }); + + it("should set userAgent as undefined for server", () => { + logger.info("Test message"); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith( + expect.objectContaining({ + userAgent: undefined + }) + ); + }); + + it("should add timestamp to all messages", () => { + const beforeTime = Date.now(); + logger.info("Test message"); + const afterTime = Date.now(); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith( + expect.objectContaining({ + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }) + ); + + const loggedTimestamp = new Date(mockWinstonLogger.info.mock.calls[0][0].timestamp).getTime(); + expect(loggedTimestamp).toBeGreaterThanOrEqual(beforeTime); + expect(loggedTimestamp).toBeLessThanOrEqual(afterTime); + }); + }); + + describe("Logging Methods", () => { + it("should call winston debug with formatted message", () => { + const testLogger = createLogger("Debug"); + const meta = { debug: true, level: 1 }; + + testLogger.debug("Debug message", meta); + + expect(mockWinstonLogger.debug).toHaveBeenCalledWith({ + message: "[Debug] Debug message", + debug: true, + level: 1, + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should call winston info with formatted message", () => { + const testLogger = createLogger("Info"); + const meta = { userId: 123, action: "login" }; + + testLogger.info("User logged in", meta); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith({ + message: "[Info] User logged in", + userId: 123, + action: "login", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should call winston warn with formatted message", () => { + const testLogger = createLogger("Warn"); + const meta = { warning: "deprecated", api: "v1" }; + + testLogger.warn("API deprecated", meta); + + expect(mockWinstonLogger.warn).toHaveBeenCalledWith({ + message: "[Warn] API deprecated", + warning: "deprecated", + api: "v1", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should call winston error with formatted message", () => { + const testLogger = createLogger("Error"); + const meta = { error: "database_connection", code: 500 }; + + testLogger.error("Database connection failed", meta); + + expect(mockWinstonLogger.error).toHaveBeenCalledWith({ + message: "[Error] Database connection failed", + error: "database_connection", + code: 500, + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should handle empty meta objects", async () => { + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.info("Message without meta"); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith({ + message: "Message without meta", + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should preserve existing meta properties", async () => { + const meta = { + source: "override-attempt", + timestamp: "override-attempt", + userAgent: "override-attempt", + customProp: "should-be-preserved" + }; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.info("Test message", meta); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith({ + message: "Test message", + source: "server", // Should override + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/), // Should override + userAgent: undefined, // Should override + customProp: "should-be-preserved" // Should preserve + }); + }); + }); + + describe("Request Logging", () => { + it("should log successful requests as info", async () => { + const mockRequest = { + method: "GET", + url: "https://api.example.com/users" + } as Request; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.logRequest(mockRequest, 150, 200); + + expect(mockWinstonLogger.info).toHaveBeenCalledWith({ + message: "GET https://api.example.com/users - 200 (150ms)", + method: "GET", + url: "https://api.example.com/users", + statusCode: 200, + responseTime: 150, + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should log client errors (4xx) as errors", async () => { + const mockRequest = { + method: "POST", + url: "https://api.example.com/users" + } as Request; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.logRequest(mockRequest, 200, 404); + + expect(mockWinstonLogger.error).toHaveBeenCalledWith({ + message: "POST https://api.example.com/users - 404 (200ms)", + method: "POST", + url: "https://api.example.com/users", + statusCode: 404, + responseTime: 200, + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should log server errors (5xx) as errors", async () => { + const mockRequest = { + method: "PUT", + url: "https://api.example.com/data/123" + } as Request; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.logRequest(mockRequest, 5000, 500); + + expect(mockWinstonLogger.error).toHaveBeenCalledWith({ + message: "PUT https://api.example.com/data/123 - 500 (5000ms)", + method: "PUT", + url: "https://api.example.com/data/123", + statusCode: 500, + responseTime: 5000, + source: "server", + userAgent: undefined, + timestamp: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/) + }); + }); + + it("should handle edge case status codes correctly", async () => { + const mockRequest = { + method: "GET", + url: "https://api.example.com/test" + } as Request; + + // Create fresh loggers without context + const { UniversalLogger } = await import("./index"); + const freshLogger1 = new UniversalLogger(); + const freshLogger2 = new UniversalLogger(); + + // Test boundary cases + freshLogger1.logRequest(mockRequest, 100, 399); // Should be info + expect(mockWinstonLogger.info).toHaveBeenCalled(); + + vi.clearAllMocks(); + + freshLogger2.logRequest(mockRequest, 100, 400); // Should be error + expect(mockWinstonLogger.error).toHaveBeenCalled(); + }); + }); + + describe("Factory Function", () => { + it("should create new logger instances with different contexts", () => { + const logger1 = createLogger("Service1"); + const logger2 = createLogger("Service2"); + + logger1.info("Message from service 1"); + logger2.info("Message from service 2"); + + expect(mockWinstonLogger.info).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + message: "[Service1] Message from service 1" + }) + ); + expect(mockWinstonLogger.info).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + message: "[Service2] Message from service 2" + }) + ); + }); + + it("should create independent logger instances", () => { + const logger1 = createLogger("Original"); + const logger2 = createLogger("Independent"); + + // Modify one logger's context + logger1.setContext("Modified"); + + logger1.info("Message 1"); + logger2.info("Message 2"); + + expect(mockWinstonLogger.info).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + message: "[Modified] Message 1" + }) + ); + expect(mockWinstonLogger.info).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + message: "[Independent] Message 2" + }) + ); + }); + }); + + describe("No Client Error Forwarding on Server", () => { + it("should not attempt to send error to server when in server environment", () => { + // Mock fetch to verify it's not called + const mockFetch = vi.fn(); + global.fetch = mockFetch; + + logger.error("Server error", { code: 500 }); + + expect(mockWinstonLogger.error).toHaveBeenCalled(); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/lib/logger/index.test.ts b/src/lib/logger/index.test.ts new file mode 100644 index 0000000..8fe862c --- /dev/null +++ b/src/lib/logger/index.test.ts @@ -0,0 +1,299 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Mock $app/environment before importing the logger +vi.mock("$app/environment", () => ({ + browser: true +})); + +// Mock global objects that exist in browser +Object.defineProperty(global, "navigator", { + value: { + userAgent: "Test Browser/1.0" + }, + writable: true +}); + +Object.defineProperty(global, "window", { + value: { + location: { + href: "https://test.example.com/page" + } + }, + writable: true +}); + +// Mock console methods +const mockConsole = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() +}; + +// Mock fetch +const mockFetch = vi.fn(); +global.fetch = mockFetch; + +describe("UniversalLogger - Client Side", () => { + let logger: any; + let createLogger: any; + + beforeEach(async () => { + // Clear all mocks + vi.clearAllMocks(); + + // Mock console methods + vi.spyOn(console, "debug").mockImplementation(mockConsole.debug); + vi.spyOn(console, "info").mockImplementation(mockConsole.info); + vi.spyOn(console, "warn").mockImplementation(mockConsole.warn); + vi.spyOn(console, "error").mockImplementation(mockConsole.error); + + // Dynamic import after mocks are set up + const loggerModule = await import("./index"); + logger = loggerModule.logger; + createLogger = loggerModule.createLogger; + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("Context Management", () => { + it("should set and use context correctly", () => { + const contextLogger = createLogger("TestContext"); + contextLogger.info("Test message"); + + expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ [TestContext] Test message", {}); + }); + + it("should work without context", () => { + logger.info("Test message"); + + expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ Test message", {}); + }); + + it("should return logger instance when setting context", () => { + const result = logger.setContext("TestContext"); + expect(result).toBe(logger); + }); + }); + + describe("Logging Methods", () => { + it("should log debug messages with correct format", () => { + const testLogger = createLogger("Debug"); + const meta = { extra: "data" }; + + testLogger.debug("Debug message", meta); + + expect(mockConsole.debug).toHaveBeenCalledWith("🐛 [Debug] Debug message", meta); + }); + + it("should log info messages with correct format", () => { + const testLogger = createLogger("Info"); + const meta = { userId: 123 }; + + testLogger.info("Info message", meta); + + expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ [Info] Info message", meta); + }); + + it("should log warn messages with correct format", () => { + const testLogger = createLogger("Warn"); + const meta = { warning: "deprecated" }; + + testLogger.warn("Warning message", meta); + + expect(mockConsole.warn).toHaveBeenCalledWith("⚠️ [Warn] Warning message", meta); + }); + + it("should log error messages with correct format", () => { + const testLogger = createLogger("Error"); + const meta = { error: "failed" }; + + testLogger.error("Error message", meta); + + expect(mockConsole.error).toHaveBeenCalledWith("❌ [Error] Error message", meta); + }); + + it("should handle empty meta objects", async () => { + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.info("Message without meta"); + + expect(mockConsole.info).toHaveBeenCalledWith("ℹ️ Message without meta", {}); + }); + }); + + describe("Client Error Forwarding", () => { + it("should send error to server when logging error", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const testLogger = createLogger("ErrorTest"); + const meta = { errorCode: 500 }; + + testLogger.error("Server error", meta); + + // Wait for async operation + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockFetch).toHaveBeenCalledWith( + "/api/log", + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: expect.stringContaining('"level":"error"') + }) + ); + + // Verify the sent data contains expected properties + const sentData = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(sentData.level).toBe("error"); + expect(sentData.message).toBe("Server error"); + expect(sentData.meta.context).toBe("ErrorTest"); + expect(sentData.meta.url).toBe("https://test.example.com/page"); + expect(sentData.meta.userAgent).toBe("Test Browser/1.0"); + expect(sentData.meta.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + }); + + it("should handle fetch errors gracefully", async () => { + mockFetch.mockRejectedValueOnce(new Error("Network error")); + + const testLogger = createLogger("ErrorTest"); + testLogger.error("Test error"); + + // Wait for async operation + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockConsole.error).toHaveBeenCalledWith( + "Failed to send error to server:", + expect.any(Error) + ); + }); + + it("should not send non-error logs to server", () => { + logger.info("Info message"); + logger.warn("Warning message"); + logger.debug("Debug message"); + + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + + describe("Request Logging", () => { + it("should log successful requests as info", async () => { + const mockRequest = { + method: "GET", + url: "https://api.example.com/users" + } as Request; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.logRequest(mockRequest, 150, 200); + + expect(mockConsole.info).toHaveBeenCalledWith( + "ℹ️ GET https://api.example.com/users - 200 (150ms)", + { + method: "GET", + url: "https://api.example.com/users", + statusCode: 200, + responseTime: 150 + } + ); + }); + + it("should log client errors (4xx) as errors", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const mockRequest = { + method: "POST", + url: "https://api.example.com/users" + } as Request; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.logRequest(mockRequest, 200, 404); + + expect(mockConsole.error).toHaveBeenCalledWith( + "❌ POST https://api.example.com/users - 404 (200ms)", + { + method: "POST", + url: "https://api.example.com/users", + statusCode: 404, + responseTime: 200 + } + ); + + // Should also send to server since it's an error + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(mockFetch).toHaveBeenCalled(); + }); + + it("should log server errors (5xx) as errors", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const mockRequest = { + method: "GET", + url: "https://api.example.com/data" + } as Request; + + // Create a fresh logger without context + const { UniversalLogger } = await import("./index"); + const freshLogger = new UniversalLogger(); + freshLogger.logRequest(mockRequest, 1000, 500); + + expect(mockConsole.error).toHaveBeenCalledWith( + "❌ GET https://api.example.com/data - 500 (1000ms)", + { + method: "GET", + url: "https://api.example.com/data", + statusCode: 500, + responseTime: 1000 + } + ); + + // Should also send to server since it's an error + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(mockFetch).toHaveBeenCalled(); + }); + }); + + describe("Factory Function", () => { + it("should create new logger instances with context", () => { + const logger1 = createLogger("Context1"); + const logger2 = createLogger("Context2"); + + logger1.info("Message 1"); + logger2.info("Message 2"); + + expect(mockConsole.info).toHaveBeenNthCalledWith(1, "ℹ️ [Context1] Message 1", {}); + expect(mockConsole.info).toHaveBeenNthCalledWith(2, "ℹ️ [Context2] Message 2", {}); + }); + + it("should create independent logger instances", () => { + const logger1 = createLogger("Context1"); + const logger2 = createLogger("Context2"); + + // Modify one logger's context + logger1.setContext("ModifiedContext"); + + logger1.info("Message 1"); + logger2.info("Message 2"); + + expect(mockConsole.info).toHaveBeenNthCalledWith(1, "ℹ️ [ModifiedContext] Message 1", {}); + expect(mockConsole.info).toHaveBeenNthCalledWith(2, "ℹ️ [Context2] Message 2", {}); + }); + }); +}); diff --git a/src/lib/logger/index.ts b/src/lib/logger/index.ts new file mode 100644 index 0000000..d45d6b7 --- /dev/null +++ b/src/lib/logger/index.ts @@ -0,0 +1,114 @@ +import { browser } from "$app/environment"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let winstonLogger: any = undefined; +if (!browser) { + const { default: winston } = await import("./winston"); + winstonLogger = winston; +} + +class UniversalLogger { + #context: string = ""; + + setContext(context: string) { + this.#context = context; + return this; + } + + #formatMessage(message: string, meta = {}) { + const contextPrefix = this.#context ? `[${this.#context}] ` : ""; + return { + message: `${contextPrefix}${message}`, + ...meta, + source: browser ? "client" : "server", + userAgent: browser ? navigator.userAgent : undefined, + timestamp: new Date().toISOString() + }; + } + + debug(message: string, meta = {}) { + if (browser) { + console.debug(`🐛 ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta); + } else { + winstonLogger.debug(this.#formatMessage(message, meta)); + } + } + + info(message: string, meta = {}) { + if (browser) { + console.info(`ℹ️ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta); + } else { + winstonLogger.info(this.#formatMessage(message, meta)); + } + } + + warn(message: string, meta = {}) { + if (browser) { + console.warn(`⚠️ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta); + } else { + winstonLogger.warn(this.#formatMessage(message, meta)); + } + } + + error(message: string, meta = {}) { + if (browser) { + console.error(`❌ ${this.#context ? `[${this.#context}] ` : ""}${message}`, meta); + + this.#sendErrorToServer(message, meta); + } else { + winstonLogger.error(this.#formatMessage(message, meta)); + } + } + + async #sendErrorToServer(message: string, meta = {}) { + try { + await fetch("/api/log", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + level: "error", + message, + meta: { + ...meta, + context: this.#context, + url: window.location.href, + userAgent: navigator.userAgent, + timestamp: new Date().toISOString() + } + }) + }); + } catch (err) { + console.error("Failed to send error to server:", err); + } + } + + logRequest(request: Request, responseTime: number, statusCode: number) { + const message = `${request.method} ${request.url} - ${statusCode} (${responseTime}ms)`; + + if (statusCode >= 400) { + this.error(message, { + method: request.method, + url: request.url, + statusCode, + responseTime + }); + } else { + this.info(message, { + method: request.method, + url: request.url, + statusCode, + responseTime + }); + } + } +} + +export const logger = new UniversalLogger(); + +export const createLogger = (context: string) => { + return new UniversalLogger().setContext(context); +}; + +export { UniversalLogger }; + +export default logger; diff --git a/src/lib/logger/integration.test.ts b/src/lib/logger/integration.test.ts new file mode 100644 index 0000000..cbb352d --- /dev/null +++ b/src/lib/logger/integration.test.ts @@ -0,0 +1,302 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +// Integration test for the complete client error forwarding flow +describe("UniversalLogger Integration - Client Error Forwarding", () => { + let mockFetch: any; + let mockConsole: any; + let originalFetch: any; + + beforeEach(() => { + // Store original fetch + originalFetch = global.fetch; + + // Setup fetch mock + mockFetch = vi.fn(); + global.fetch = mockFetch; + + // Setup console mocks + mockConsole = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() + }; + + vi.spyOn(console, "debug").mockImplementation(mockConsole.debug); + vi.spyOn(console, "info").mockImplementation(mockConsole.info); + vi.spyOn(console, "warn").mockImplementation(mockConsole.warn); + vi.spyOn(console, "error").mockImplementation(mockConsole.error); + + // Mock browser environment + vi.doMock("$app/environment", () => ({ + browser: true + })); + + // Mock browser globals + Object.defineProperty(global, "navigator", { + value: { userAgent: "Test Browser/1.0" }, + writable: true + }); + + Object.defineProperty(global, "window", { + value: { location: { href: "https://test.example.com/page" } }, + writable: true + }); + }); + + afterEach(() => { + // Restore original fetch + global.fetch = originalFetch; + vi.restoreAllMocks(); + vi.resetModules(); + }); + + describe("End-to-End Error Forwarding", () => { + it("should forward client error to server and handle successful response", async () => { + // Mock successful server response + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + // Import logger after mocks are set up + const { createLogger } = await import("./index"); + const logger = createLogger("IntegrationTest"); + + // Trigger client error + const errorMessage = "Integration test error"; + const errorMeta = { errorCode: "INT001", component: "TestComponent" }; + + logger.error(errorMessage, errorMeta); + + // Wait for async operation + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Verify client-side logging + expect(mockConsole.error).toHaveBeenCalledWith( + "❌ [IntegrationTest] Integration test error", + errorMeta + ); + + // Verify server request + expect(mockFetch).toHaveBeenCalledWith( + "/api/log", + expect.objectContaining({ + method: "POST", + headers: { "Content-Type": "application/json" }, + body: expect.stringContaining('"level":"error"') + }) + ); + + // Verify the sent data contains expected properties + const sentData = JSON.parse(mockFetch.mock.calls[0][1].body); + expect(sentData.level).toBe("error"); + expect(sentData.message).toBe(errorMessage); + expect(sentData.meta.context).toBe("IntegrationTest"); + expect(sentData.meta.url).toBe("https://test.example.com/page"); + expect(sentData.meta.userAgent).toBe("Test Browser/1.0"); + expect(sentData.meta.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/); + }); + + it("should handle server API error gracefully", async () => { + // Mock server error response + mockFetch.mockRejectedValueOnce(new Error("Network error")); + + const { createLogger } = await import("./index"); + const logger = createLogger("ErrorHandling"); + + logger.error("Test error"); + + // Wait for async operation + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Verify client error is logged locally + expect(mockConsole.error).toHaveBeenCalledWith("❌ [ErrorHandling] Test error", {}); + + // Verify fallback error logging + expect(mockConsole.error).toHaveBeenCalledWith( + "Failed to send error to server:", + expect.any(Error) + ); + }); + + it("should handle server returning error status", async () => { + // Mock server error status + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: "Internal Server Error" + }); + + const { createLogger } = await import("./index"); + const logger = createLogger("ServerError"); + + logger.error("Server unavailable"); + + // Wait for async operation + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Should still attempt to send to server + expect(mockFetch).toHaveBeenCalled(); + + // Local error should still be logged + expect(mockConsole.error).toHaveBeenCalledWith("❌ [ServerError] Server unavailable", {}); + }); + }); + + describe("Server-side API Integration", () => { + it("should process forwarded client errors correctly", async () => { + // Mock server environment + vi.doMock("$app/environment", () => ({ + browser: false + })); + + // Mock winston logger + const mockWinstonLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn() + }; + + vi.doMock("./winston", () => ({ + default: mockWinstonLogger + })); + + // Mock SvelteKit json function + const mockJson = vi.fn().mockImplementation((data, options) => ({ data, options })); + vi.doMock("@sveltejs/kit", () => ({ + json: mockJson + })); + + // This test is simplified since we can't easily test the actual server endpoint + // in this integration test due to mocking complexities. The server endpoint + // is tested separately in its own test file. + + // Just verify that the mock setup would work + expect(mockWinstonLogger.error).toBeDefined(); + expect(mockJson).toBeDefined(); + + // The actual server endpoint integration is tested in the dedicated server.test.ts file + }); + }); + + describe("Cross-Environment Behavior", () => { + it("should not forward non-error logs from client", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const { createLogger } = await import("./index"); + const logger = createLogger("NoForwarding"); + + // Log different levels + logger.debug("Debug message"); + logger.info("Info message"); + logger.warn("Warning message"); + + // Wait for any potential async operations + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Verify only console methods were called, no fetch + expect(mockConsole.debug).toHaveBeenCalled(); + expect(mockConsole.info).toHaveBeenCalled(); + expect(mockConsole.warn).toHaveBeenCalled(); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it("should handle multiple concurrent error forwarding requests", async () => { + // Mock successful responses for all requests + mockFetch.mockResolvedValue({ + ok: true, + json: async () => ({ success: true }) + }); + + const { createLogger } = await import("./index"); + const logger1 = createLogger("Concurrent1"); + const logger2 = createLogger("Concurrent2"); + + // Trigger multiple errors simultaneously + const promises = [ + logger1.error("Error 1", { id: 1 }), + logger2.error("Error 2", { id: 2 }), + logger1.error("Error 3", { id: 3 }) + ]; + + await Promise.all(promises); + + // Wait for all async operations + await new Promise((resolve) => setTimeout(resolve, 20)); + + // Verify all requests were made + expect(mockFetch).toHaveBeenCalledTimes(3); + + // Verify each request has correct context + const calls = mockFetch.mock.calls; + expect(calls[0][1].body).toContain('"context":"Concurrent1"'); + expect(calls[1][1].body).toContain('"context":"Concurrent2"'); + expect(calls[2][1].body).toContain('"context":"Concurrent1"'); + }); + }); + + describe("Data Integrity", () => { + it("should preserve complex metadata through forwarding", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const { createLogger } = await import("./index"); + const logger = createLogger("DataIntegrity"); + + const complexMeta = { + user: { id: 123, name: "Test User" }, + error: { stack: "Error stack trace...", code: 500 }, + array: [1, 2, { nested: true }], + boolean: true, + null: null, + timestamp: new Date().toISOString() + }; + + logger.error("Complex error", complexMeta); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const sentData = JSON.parse(mockFetch.mock.calls[0][1].body); + + // Verify complex meta is preserved + expect(sentData.meta.user).toEqual({ id: 123, name: "Test User" }); + expect(sentData.meta.error).toEqual({ stack: "Error stack trace...", code: 500 }); + expect(sentData.meta.array).toEqual([1, 2, { nested: true }]); + expect(sentData.meta.boolean).toBe(true); + expect(sentData.meta.null).toBe(null); + + // Verify additional context is added + expect(sentData.meta.context).toBe("DataIntegrity"); + expect(sentData.meta.url).toBe("https://test.example.com/page"); + expect(sentData.meta.userAgent).toBe("Test Browser/1.0"); + }); + + it("should maintain message and level integrity", async () => { + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ success: true }) + }); + + const { createLogger } = await import("./index"); + const logger = createLogger("MessageIntegrity"); + + const originalMessage = "Original error message with special chars: áéíóú 中文 🚀"; + logger.error(originalMessage); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + const sentData = JSON.parse(mockFetch.mock.calls[0][1].body); + + expect(sentData.level).toBe("error"); + expect(sentData.message).toBe(originalMessage); + }); + }); +}); diff --git a/src/lib/logger/winston.ts b/src/lib/logger/winston.ts new file mode 100644 index 0000000..948e206 --- /dev/null +++ b/src/lib/logger/winston.ts @@ -0,0 +1,32 @@ +import winston from "winston"; +import { dev } from "$app/environment"; + +const customFormat = winston.format.combine( + winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + winston.format.errors({ stack: true }), + winston.format.colorize(), + winston.format.printf(({ timestamp, level, message, stack, ...meta }) => { + let log = `${timestamp} [${level}]: ${message}`; + if (stack) { + log += `\n${stack}`; + } + + if (Object.keys(meta).length > 0) { + log += `\n${JSON.stringify(meta, null, 2)}`; + } + + return log; + }) +); + +const logger = winston.createLogger({ + level: dev ? "debug" : "info", + format: customFormat, + transports: [ + new winston.transports.Console({ + format: winston.format.combine(customFormat) + }) + ] +}); + +export default logger; diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..55b3a91 --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,13 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChild = T extends { child?: any } ? Omit : T; +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type WithoutChildren = T extends { children?: any } ? Omit : T; +export type WithoutChildrenOrChild = WithoutChildren>; +export type WithElementRef = T & { ref?: U | null }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 2458f08..9780038 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,7 +1,23 @@ + {@render children()} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts new file mode 100644 index 0000000..e8d4d7c --- /dev/null +++ b/src/routes/+page.server.ts @@ -0,0 +1,16 @@ +export const load = async (event) => { + const fetchEnvOk = async () => { + const response = await event.fetch("/api/env"); + try { + return (await response.json()).envOkay; + } catch { + return false; + } + }; + + return { + streamed: { + isEnvOk: fetchEnvOk() + } + }; +}; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8052b31..76ecac9 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,15 +1,28 @@ - -

Welcome to SvelteKit

-

Visit svelte.dev/docs/kit to read the documentation

+ + Hello - OpenReception + -

Environment configuration is {envVarsOkay}

+ +
+ + Welcome to OpenReception + + Environment configuration is + {#await data.streamed.isEnvOk} + unknown + {:then isEnvOk} + {isEnvOk ? "OK" : "NOT OK"} + {/await}. + + + +
+
diff --git a/src/routes/api/log/+server.ts b/src/routes/api/log/+server.ts new file mode 100644 index 0000000..aafe0c3 --- /dev/null +++ b/src/routes/api/log/+server.ts @@ -0,0 +1,36 @@ +import { json } from "@sveltejs/kit"; +import { logger } from "$lib/logger"; + +export async function POST({ request }) { + try { + const { level, message, meta } = await request.json(); + + const clientLogger = logger.setContext("CLIENT"); + + switch (level) { + case "debug": + clientLogger.debug(message, meta); + break; + case "info": + clientLogger.info(message, meta); + break; + case "warn": + clientLogger.warn(message, meta); + break; + case "error": + clientLogger.error(message, meta); + break; + default: + clientLogger.info(message, meta); + } + + return json({ success: true }); + } catch (error: unknown) { + if (error instanceof Error) { + logger.error("Failed to process client log", { error: error?.message ?? "Unknown error" }); + } else { + logger.error("Unknown error on processing client error message"); + } + return json({ success: false }, { status: 500 }); + } +} diff --git a/src/routes/page.svelte.test.ts b/src/routes/page.svelte.test.ts index 6bf82c9..d77e6f4 100644 --- a/src/routes/page.svelte.test.ts +++ b/src/routes/page.svelte.test.ts @@ -13,8 +13,18 @@ describe("/+page.svelte", () => { ); }); - test("should render h1", () => { - render(Page); + test("should render h1", async () => { + const mockData = { + streamed: { + isEnvOk: Promise.resolve(true) + } + }; + render(Page, { + props: { + data: mockData + } + }); + await mockData.streamed.isEnvOk; expect(screen.getByRole("heading", { level: 1 })).toBeInTheDocument(); }); }); diff --git a/static/favicon-192.png b/static/favicon-192.png new file mode 100644 index 0000000..51e29cc Binary files /dev/null and b/static/favicon-192.png differ diff --git a/static/favicon-512.png b/static/favicon-512.png new file mode 100644 index 0000000..f9ee22f Binary files /dev/null and b/static/favicon-512.png differ diff --git a/static/favicon.png b/static/favicon.png deleted file mode 100644 index 825b9e6..0000000 Binary files a/static/favicon.png and /dev/null differ diff --git a/static/favicon.svg b/static/favicon.svg new file mode 100644 index 0000000..563d3f0 --- /dev/null +++ b/static/favicon.svg @@ -0,0 +1,126 @@ + + + Logo + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/safari-pinned-tab.svg b/static/safari-pinned-tab.svg new file mode 100644 index 0000000..bc519ae --- /dev/null +++ b/static/safari-pinned-tab.svg @@ -0,0 +1,7 @@ + + + safari-pinned-tab + + + + \ No newline at end of file