diff --git a/.claude-flow/metrics/agent-metrics.json b/.claude-flow/metrics/agent-metrics.json deleted file mode 100644 index 9e26dfeeb..000000000 --- a/.claude-flow/metrics/agent-metrics.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/.claude-flow/metrics/performance.json b/.claude-flow/metrics/performance.json deleted file mode 100644 index e246e9149..000000000 --- a/.claude-flow/metrics/performance.json +++ /dev/null @@ -1,87 +0,0 @@ -{ - "startTime": 1764952181915, - "sessionId": "session-1764952181915", - "lastActivity": 1764952181915, - "sessionDuration": 0, - "totalTasks": 1, - "successfulTasks": 1, - "failedTasks": 0, - "totalAgents": 0, - "activeAgents": 0, - "neuralEvents": 0, - "memoryMode": { - "reasoningbankOperations": 0, - "basicOperations": 0, - "autoModeSelections": 0, - "modeOverrides": 0, - "currentMode": "auto" - }, - "operations": { - "store": { - "count": 0, - "totalDuration": 0, - "errors": 0 - }, - "retrieve": { - "count": 0, - "totalDuration": 0, - "errors": 0 - }, - "query": { - "count": 0, - "totalDuration": 0, - "errors": 0 - }, - "list": { - "count": 0, - "totalDuration": 0, - "errors": 0 - }, - "delete": { - "count": 0, - "totalDuration": 0, - "errors": 0 - }, - "search": { - "count": 0, - "totalDuration": 0, - "errors": 0 - }, - "init": { - "count": 0, - "totalDuration": 0, - "errors": 0 - } - }, - "performance": { - "avgOperationDuration": 0, - "minOperationDuration": null, - "maxOperationDuration": null, - "slowOperations": 0, - "fastOperations": 0, - "totalOperationTime": 0 - }, - "storage": { - "totalEntries": 0, - "reasoningbankEntries": 0, - "basicEntries": 0, - "databaseSize": 0, - "lastBackup": null, - "growthRate": 0 - }, - "errors": { - "total": 0, - "byType": {}, - "byOperation": {}, - "recent": [] - }, - "reasoningbank": { - "semanticSearches": 0, - "sqlFallbacks": 0, - "embeddingGenerated": 0, - "consolidations": 0, - "avgQueryTime": 0, - "cacheHits": 0, - "cacheMisses": 0 - } -} \ No newline at end of file diff --git a/.claude-flow/metrics/task-metrics.json b/.claude-flow/metrics/task-metrics.json deleted file mode 100644 index 67b62b609..000000000 --- a/.claude-flow/metrics/task-metrics.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "id": "cmd-swarm-1764952182017", - "type": "swarm", - "success": true, - "duration": 4.868416999999994, - "timestamp": 1764952182022, - "metadata": {} - } -] \ No newline at end of file diff --git a/.env.development b/.env.development index 18f715025..a0ff60283 100644 --- a/.env.development +++ b/.env.development @@ -36,6 +36,9 @@ S3_REGION=us-east-1 S3_ACCESS_KEY=minioadmin S3_SECRET_KEY=minioadmin +# Unified Storage (single bucket for all apps) +MANACORE_STORAGE_PUBLIC_URL=http://localhost:9000/manacore-storage + # ============================================ # MANA-CORE-AUTH SERVICE # ============================================ @@ -57,6 +60,11 @@ STRIPE_SECRET_KEY=sk_test_YOUR_KEY STRIPE_PUBLISHABLE_KEY=pk_test_YOUR_KEY STRIPE_WEBHOOK_SECRET=whsec_YOUR_SECRET +# Brevo Email Service (get key from https://app.brevo.com/settings/keys/api) +BREVO_API_KEY= +EMAIL_SENDER_ADDRESS=noreply@manacore.ai +EMAIL_SENDER_NAME=ManaCore + # ============================================ # CHAT PROJECT # ============================================ @@ -127,9 +135,7 @@ PICTURE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/picture # Replicate API Token for AI image generation PICTURE_REPLICATE_API_TOKEN=r8_QlvkstNhIc6NBX1ktpQ6ibvzOE2d2UQ1Emamd -# Storage Configuration (uses MinIO locally, Hetzner in production) -# Uses shared S3_* variables from above - no project-specific override needed for local dev -PICTURE_STORAGE_PUBLIC_URL=http://localhost:9000/picture-storage +# Storage: Uses unified manacore-storage bucket (see MANACORE_STORAGE_PUBLIC_URL above) # Credit System (staging only - freemium: 3 free images, then credits) PICTURE_APP_ID=picture-app @@ -148,8 +154,7 @@ NUTRIPHI_DATABASE_URL=postgresql://nutriphi:nutriphi_dev_password@localhost:5435 NUTRIPHI_APP_ID=nutriphi NUTRIPHI_GEMINI_API_KEY=your-gemini-api-key-here -# S3 Storage (uses MinIO locally via shared S3_* variables, Hetzner in production) -NUTRIPHI_S3_PUBLIC_URL=http://localhost:9000/nutriphi-storage +# Storage: Uses unified manacore-storage bucket # ============================================ # ZITARE PROJECT @@ -180,9 +185,7 @@ VOXEL_LAVA_API_URL=http://localhost:3010 CONTACTS_BACKEND_PORT=3015 CONTACTS_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/contacts -# S3 Storage for contact photos -CONTACTS_S3_BUCKET=contacts-photos -CONTACTS_S3_PUBLIC_URL=http://localhost:9000/contacts-photos +# Storage: Uses unified manacore-storage bucket # Google OAuth for contacts import # Get credentials from https://console.cloud.google.com/apis/credentials @@ -204,7 +207,6 @@ CALENDAR_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/calendar STORAGE_BACKEND_PORT=3016 STORAGE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/storage -STORAGE_S3_PUBLIC_URL=http://localhost:9000/storage-storage STORAGE_MAX_FILE_SIZE=104857600 STORAGE_MAX_FILES_PER_UPLOAD=10 @@ -265,7 +267,6 @@ FINANCE_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/finance INVENTORY_BACKEND_PORT=3020 INVENTORY_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/inventory -INVENTORY_S3_PUBLIC_URL=http://localhost:9000/inventory-storage # ============================================ # TECHBASE PROJECT diff --git a/.github/workflows/cd-staging.yml b/.github/workflows/cd-staging.yml index 4af222aed..aa35e9568 100644 --- a/.github/workflows/cd-staging.yml +++ b/.github/workflows/cd-staging.yml @@ -98,29 +98,41 @@ jobs: POSTGRES_PORT=5432 POSTGRES_DB=manacore POSTGRES_USER=postgres - POSTGRES_PASSWORD=${{ secrets.STAGING_POSTGRES_PASSWORD }} + POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }} # Redis - Configuration REDIS_HOST=redis REDIS_PORT=6379 - REDIS_PASSWORD=${{ secrets.STAGING_REDIS_PASSWORD }} + REDIS_PASSWORD=${{ secrets.REDIS_PASSWORD }} # Mana Core Auth - Configuration MANA_SERVICE_URL=http://mana-core-auth:3001 - JWT_SECRET=${{ secrets.STAGING_JWT_SECRET }} - JWT_PUBLIC_KEY=${{ secrets.STAGING_JWT_PUBLIC_KEY }} - JWT_PRIVATE_KEY=${{ secrets.STAGING_JWT_PRIVATE_KEY }} + JWT_SECRET=${{ secrets.JWT_SECRET }} + JWT_PUBLIC_KEY=${{ secrets.JWT_PUBLIC_KEY }} + JWT_PRIVATE_KEY=${{ secrets.JWT_PRIVATE_KEY }} + + # Brevo Email Service + BREVO_API_KEY=${{ secrets.BREVO_API_KEY }} + EMAIL_SENDER_ADDRESS=noreply@manacore.ai + EMAIL_SENDER_NAME=ManaCore # Supabase - SUPABASE_URL=${{ secrets.STAGING_SUPABASE_URL }} - SUPABASE_ANON_KEY=${{ secrets.STAGING_SUPABASE_ANON_KEY }} - SUPABASE_SERVICE_ROLE_KEY=${{ secrets.STAGING_SUPABASE_SERVICE_ROLE_KEY }} + SUPABASE_URL=${{ secrets.SUPABASE_URL }} + SUPABASE_ANON_KEY=${{ secrets.SUPABASE_ANON_KEY }} + SUPABASE_SERVICE_ROLE_KEY=${{ secrets.SUPABASE_SERVICE_ROLE_KEY }} # Azure OpenAI - AZURE_OPENAI_ENDPOINT=${{ secrets.STAGING_AZURE_OPENAI_ENDPOINT }} - AZURE_OPENAI_API_KEY=${{ secrets.STAGING_AZURE_OPENAI_API_KEY }} + AZURE_OPENAI_ENDPOINT=${{ secrets.AZURE_OPENAI_ENDPOINT }} + AZURE_OPENAI_API_KEY=${{ secrets.AZURE_OPENAI_API_KEY }} AZURE_OPENAI_API_VERSION=2024-12-01-preview + # Hetzner Object Storage (S3-compatible) + S3_ENDPOINT=${{ secrets.S3_ENDPOINT }} + S3_REGION=${{ secrets.S3_REGION }} + S3_ACCESS_KEY=${{ secrets.S3_ACCESS_KEY }} + S3_SECRET_KEY=${{ secrets.S3_SECRET_KEY }} + MANACORE_STORAGE_PUBLIC_URL=${{ secrets.MANACORE_STORAGE_PUBLIC_URL }} + # Environment NODE_ENV=staging EOF diff --git a/.husky/pre-commit b/.husky/pre-commit index ee2ec94c8..30c1cf220 100644 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,2 +1,5 @@ pnpm exec lint-staged pnpm run type-check + +# Run svelte-check on staged web apps (catches a11y, imports, Svelte 5 issues) +./scripts/svelte-check-staged.sh diff --git a/.husky/pre-push b/.husky/pre-push new file mode 100755 index 000000000..c3bd72b64 --- /dev/null +++ b/.husky/pre-push @@ -0,0 +1,3 @@ +# Run production build check before push +# This catches npm package incompatibilities and Docker issues before CI/CD +./scripts/build-changed-apps.sh diff --git a/CLAUDE.md b/CLAUDE.md index f925c51e2..f945c6bad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,678 +1,176 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +This file provides guidance to Claude Code when working with this repository. -## Monorepo Overview +## Purpose & Constraints -This is a pnpm workspace monorepo containing multiple product applications with shared packages. All projects use Supabase for database/auth and follow similar architectural patterns. +Multi-app SaaS monorepo with shared packages and centralized authentication. -**Package Manager:** pnpm 9.15.0 (use `pnpm` for all commands) -**Build System:** Turborepo -**Node Version:** 20+ +- **Package Manager:** pnpm 9.15+ (use `pnpm` for all commands) +- **Build System:** Turborepo +- **Node Version:** 20+ +- **NEVER create files unless necessary** - prefer editing existing files +- **NEVER create documentation** unless explicitly requested +- **Prefer editing over creating** - check if functionality exists first -## Detailed Guidelines - -For comprehensive guidelines on code patterns and conventions, see the `.claude/` directory: - -| Document | Purpose | -|----------|---------| -| [`.claude/GUIDELINES.md`](.claude/GUIDELINES.md) | Main reference overview | -| [`.claude/guidelines/code-style.md`](.claude/guidelines/code-style.md) | Formatting, naming, linting | -| [`.claude/guidelines/database.md`](.claude/guidelines/database.md) | Drizzle ORM, schema patterns | -| [`.claude/guidelines/testing.md`](.claude/guidelines/testing.md) | Jest/Vitest, mock factories | -| [`.claude/guidelines/nestjs-backend.md`](.claude/guidelines/nestjs-backend.md) | Controllers, services, DTOs | -| [`.claude/guidelines/error-handling.md`](.claude/guidelines/error-handling.md) | Go-style Result types, error codes | -| [`.claude/guidelines/sveltekit-web.md`](.claude/guidelines/sveltekit-web.md) | Svelte 5 runes, stores | -| [`.claude/guidelines/expo-mobile.md`](.claude/guidelines/expo-mobile.md) | React Native, NativeWind | -| [`.claude/guidelines/authentication.md`](.claude/guidelines/authentication.md) | Mana Core Auth integration | - -**Always consult these guidelines before making changes.** - -## Projects - -| Project | Description | Apps | -| ------------ | ---------------------------- | --------------------------------------------------------- | -| **manacore** | Multi-app ecosystem platform | Expo mobile, SvelteKit web | -| **manadeck** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web | -| **picture** | AI image generation | Expo mobile, SvelteKit web, Astro landing | -| **chat** | AI chat application | NestJS backend, Expo mobile, SvelteKit web, Astro landing | -| **zitare** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing | -| **contacts** | Contact management | NestJS backend, SvelteKit web | - -### Archived Projects (`apps-archived/`) - -These projects are temporarily archived and excluded from the workspace. To re-activate, move back to `apps/`. - -| Project | Description | -| ------------------ | -------------------------------- | -| **bauntown** | Community website for developers | -| **maerchenzauber** | AI story generation | -| **memoro** | Voice memo & AI analysis | -| **news** | News aggregation | -| **nutriphi** | Nutrition tracking | -| **reader** | Reading app | -| **uload** | URL shortener | -| **wisekeep** | AI wisdom extraction from video | -| **techbase** | Software comparison platform | -| **inventory** | Inventory management | -| **presi** | Presentation tool | -| **storage** | Cloud storage | - -## Development Commands - -For detailed local development setup, see **[docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)**. - -### Quick Start (Recommended) - -Use `dev:*:full` commands to start any app with automatic database setup: - -```bash -pnpm docker:up # Start PostgreSQL, Redis, MinIO -pnpm dev:chat:full # Start chat with auth + auto DB setup -pnpm dev:zitare:full # Start zitare with auth + auto DB setup -pnpm dev:contacts:full # Start contacts with auth + auto DB setup -pnpm dev:calendar:full # Start calendar with auth + auto DB setup -pnpm dev:clock:full # Start clock with auth + auto DB setup -pnpm dev:todo:full # Start todo with auth + auto DB setup -pnpm dev:picture:full # Start picture with auth + auto DB setup -``` - -These commands automatically: -1. Create the database if missing -2. Push the latest schema -3. Start auth, backend, and web with colored output - -### Database Setup - -```bash -pnpm setup:db # Setup ALL databases and schemas -pnpm setup:db:chat # Setup just chat -pnpm setup:db:auth # Setup just auth -``` - -### Individual App Commands - -```bash -# Start specific project (runs all apps in project) -pnpm run manacore:dev -pnpm run manadeck:dev -pnpm run picture:dev -pnpm run chat:dev -pnpm run zitare:dev -pnpm run contacts:dev - -# Start specific app within project -pnpm run dev:chat:mobile # Just mobile app -pnpm run dev:chat:backend # Just NestJS backend -pnpm run dev:chat:app # Web + backend together - -# Build & quality -pnpm run build -pnpm run type-check -pnpm run format -``` - -Each project has its own `CLAUDE.md` with detailed project-specific commands. - -## Architecture Patterns - -### Monorepo Structure +## Monorepo Structure ``` manacore-monorepo/ -├── apps/ # Active SaaS product applications -│ ├── chat/ -│ │ ├── apps/ -│ │ │ ├── backend/ # NestJS API -│ │ │ ├── mobile/ # Expo React Native app -│ │ │ ├── web/ # SvelteKit web app -│ │ │ └── landing/ # Astro marketing page -│ │ └── packages/ # Project-specific shared code -│ ├── manadeck/ -│ ├── picture/ +├── apps/ # Active SaaS applications +│ ├── chat/ # AI chat (backend, mobile, web, landing) +│ ├── picture/ # AI image generation +│ ├── zitare/ # Daily quotes +│ ├── contacts/ # Contact management │ └── ... -├── apps-archived/ # Archived apps (excluded from workspace) -│ ├── bauntown/ -│ ├── maerchenzauber/ -│ ├── memoro/ -│ ├── news/ -│ ├── nutriphi/ -│ ├── reader/ -│ ├── uload/ -│ └── wisekeep/ -├── games/ # Game projects -│ └── {game-name}/ # Individual games -├── services/ # Standalone microservices -│ └── mana-core-auth/ # Central authentication service -├── packages/ # Monorepo-wide shared packages -└── docker/ # Docker configuration files +├── services/ +│ └── mana-core-auth/ # Central auth service (port 3001) +├── packages/ # Shared packages (@manacore/*) +├── .claude/ # Code guidelines (detailed patterns) +└── docs/ # Technical documentation ``` -### Standard Project Structure (inside apps/) +## Quick Start -``` -apps/{project}/ -├── apps/ -│ ├── backend/ # NestJS API (when present) -│ ├── mobile/ # Expo React Native app -│ ├── web/ # SvelteKit web app -│ └── landing/ # Astro marketing page -├── packages/ # Project-specific shared code -└── package.json +```bash +# Start infrastructure (PostgreSQL, Redis, MinIO) +pnpm docker:up + +# Start any app with automatic DB setup +pnpm dev:chat:full # Chat with auth + backend + web +pnpm dev:zitare:full # Zitare with auth + backend + web +pnpm dev:picture:full # Picture with auth + backend + web +pnpm dev:contacts:full # Contacts with auth + backend + web ``` -### Turborepo Configuration +## Technology Stack -**CRITICAL: Avoid Recursive Turbo Calls** +| App Type | Stack | +|----------|-------| +| **Backend** | NestJS 10-11 + Drizzle ORM + PostgreSQL | +| **Web** | SvelteKit 2 + Svelte 5 (runes mode) | +| **Mobile** | Expo SDK 52-54 + React Native + NativeWind | +| **Landing** | Astro 5 + Tailwind CSS | +| **Auth** | mana-core-auth (EdDSA JWT, port 3001) | -Parent workspace packages (e.g., `apps/chat/package.json`, `apps/zitare/package.json`) must **NEVER** have scripts that call `turbo run ` for tasks that turbo orchestrates from the root. +## Critical Gotchas + +### 1. Turborepo Infinite Loops +**NEVER** put `turbo run ` in child package.json for tasks orchestrated from root. ```jsonc -// WRONG - Creates infinite recursion! +// ❌ WRONG - Creates infinite recursion // apps/chat/package.json { "scripts": { "type-check": "turbo run type-check", // DON'T DO THIS - "build": "turbo run build", // DON'T DO THIS - "lint": "turbo run lint" // DON'T DO THIS + "build": "turbo run build" // DON'T DO THIS } } -// CORRECT - Let root turbo handle orchestration -// apps/chat/package.json +// ✅ CORRECT - Let root turbo handle it { "scripts": { - "dev": "turbo run dev" // OK for dev (persistent task, scoped) - // No type-check, build, lint scripts - handled by root turbo + "dev": "turbo run dev" // OK (persistent task) + // No type-check, build, lint - handled by root } } ``` -**Why this matters:** When root turbo runs `type-check`, it finds packages with `type-check` scripts and runs them. If that script is `turbo run type-check`, it spawns another turbo process that does the same thing → infinite loop. This causes tasks to run for 10+ minutes with thousands of duplicate task entries. - -**The `dev` script exception:** Using `turbo run dev` in parent packages is acceptable because: -1. It's typically run directly on that package (scoped) -2. Dev tasks are persistent and turbo handles them differently - -**Current turbo.json settings:** -- `concurrency: "5"` - Parallel task limit (adjust based on machine) -- `type-check` has `dependsOn: ["^type-check"]` - Dependencies are checked first - -### Technology Stack by App Type - -**Mobile Apps (Expo):** - -- React Native 0.76-0.81 + Expo SDK 52-54 -- Expo Router (file-based routing) -- NativeWind (Tailwind for React Native) -- Zustand (state management) - -**Web Apps (SvelteKit):** - -- SvelteKit 2.x + Svelte 5 -- Tailwind CSS -- Supabase SSR auth - -**Landing Pages (Astro):** - -- Astro 5.x -- Tailwind CSS -- Static site generation - -**Backends (NestJS):** - -- NestJS 10-11 -- TypeScript -- Supabase integration - -### Authentication Architecture - -All projects use **mana-core-auth** as the central authentication service: - -``` -┌─────────────┐ ┌─────────────┐ ┌────────────────┐ -│ Client │────>│ Backend │────>│ mana-core-auth │ -│ (Web/Mobile)│ │ (NestJS) │ │ (port 3001) │ -└─────────────┘ └─────────────┘ └────────────────┘ - │ │ │ - │ Bearer token │ POST /validate │ - │ │ {token} │ - │ │<────────────────────│ - │ │ {valid, payload} │ - │<──────────────────│ │ - │ Response │ │ -``` - -#### Key Components - -| Component | Purpose | -| ------------------------------- | -------------------------------------------------- | -| `services/mana-core-auth` | Central auth service (Better Auth + EdDSA JWT) | -| `@manacore/shared-nestjs-auth` | Shared NestJS guards/decorators for JWT validation | -| `@mana-core/nestjs-integration` | Extended NestJS module with auth + credits | -| `@manacore/shared-auth` | Client-side auth for web/mobile apps | - -#### NestJS Backend Integration - -**Option 1: Simple auth only** - Use `@manacore/shared-nestjs-auth`: +### 2. Svelte 5 Runes ONLY +Always use Svelte 5 runes syntax, never old Svelte syntax. ```typescript -// In your controller -import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; - -@Controller('api') -@UseGuards(JwtAuthGuard) -export class MyController { - @Get('profile') - getProfile(@CurrentUser() user: CurrentUserData) { - return { userId: user.userId, email: user.email }; - } -} -``` - -**Option 2: Auth + Credits** - Use `@mana-core/nestjs-integration`: - -```typescript -// app.module.ts -import { ManaCoreModule } from '@mana-core/nestjs-integration'; - -@Module({ - imports: [ - ManaCoreModule.forRootAsync({ - imports: [ConfigModule], - useFactory: (config: ConfigService) => ({ - appId: config.get('APP_ID'), - serviceKey: config.get('MANA_CORE_SERVICE_KEY'), - debug: config.get('NODE_ENV') === 'development', - }), - inject: [ConfigService], - }), - ], -}) -export class AppModule {} - -// In controller -import { AuthGuard } from '@mana-core/nestjs-integration/guards'; -import { CurrentUser } from '@mana-core/nestjs-integration/decorators'; -import { CreditClientService } from '@mana-core/nestjs-integration'; - -@Controller('api') -@UseGuards(AuthGuard) -export class ApiController { - constructor(private creditClient: CreditClientService) {} - - @Post('generate') - async generate(@CurrentUser() user: any) { - await this.creditClient.consumeCredits(user.sub, 'generation', 10, 'AI generation'); - // ... do work - } -} -``` - -#### Required Environment Variables - -```env -# All backends need this -MANA_CORE_AUTH_URL=http://localhost:3001 - -# For development bypass (optional) -NODE_ENV=development -DEV_BYPASS_AUTH=true -DEV_USER_ID=your-test-user-id - -# For credit operations (optional) -MANA_CORE_SERVICE_KEY=your-service-key -APP_ID=your-app-id -``` - -#### JWT Token Structure (EdDSA) - -```json -{ - "sub": "user-id", - "email": "user@example.com", - "role": "user", - "sid": "session-id", - "exp": 1764606251, - "iss": "manacore", - "aud": "manacore" -} -``` - -#### Testing Auth Integration - -```bash -# 1. Start mana-core-auth -pnpm dev:auth - -# 2. Start a backend (e.g., Zitare) -pnpm dev:zitare:backend - -# 3. Get a token -TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \ - -H "Content-Type: application/json" \ - -d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken') - -# 4. Call protected endpoint -curl http://localhost:3007/api/favorites \ - -H "Authorization: Bearer $TOKEN" -``` - -#### Integrated Backends - -| Backend | Package | Port | -| -------- | ------------------------------- | ---- | -| Chat | `@mana-core/nestjs-integration` | 3002 | -| Picture | `@manacore/shared-nestjs-auth` | 3006 | -| Zitare | `@manacore/shared-nestjs-auth` | 3007 | -| Presi | Custom (same pattern) | 3008 | -| ManaDeck | `@mana-core/nestjs-integration` | 3009 | - -### Svelte 5 Runes Mode (Web Apps) - -All SvelteKit apps use Svelte 5 runes: - -```typescript -// CORRECT - Svelte 5 +// ✅ CORRECT - Svelte 5 runes let count = $state(0); let doubled = $derived(count * 2); -$effect(() => { - console.log(count); -}); +$effect(() => console.log(count)); -// WRONG - Old Svelte syntax +// ❌ WRONG - Old Svelte syntax let count = 0; $: doubled = count * 2; ``` -## Shared Packages (`packages/`) - -| Package | Purpose | -| ------------------------------- | ----------------------------------------------- | -| `@manacore/shared-nestjs-auth` | NestJS JWT validation guards via mana-core-auth | -| `@mana-core/nestjs-integration` | NestJS module with auth guards + credit client | -| `@manacore/shared-auth` | Client-side auth service for web/mobile apps | -| `@manacore/shared-storage` | S3-compatible storage (MinIO local, Hetzner prod) | -| `@manacore/shared-supabase` | Unified Supabase client | -| `@manacore/shared-types` | Common TypeScript types | -| `@manacore/shared-utils` | Utility functions | -| `@manacore/shared-ui` | React Native UI components | -| `@manacore/shared-theme` | Theme configuration | -| `@manacore/shared-i18n` | Internationalization | - -Import shared packages: +### 3. Authentication Integration +All backends need `MANA_CORE_AUTH_URL=http://localhost:3001` env var. ```typescript -import { createAuthService } from '@manacore/shared-auth'; -import { formatDate, truncate } from '@manacore/shared-utils'; +// Use @manacore/shared-nestjs-auth for JWT validation +import { JwtAuthGuard, CurrentUser } from '@manacore/shared-nestjs-auth'; + +@UseGuards(JwtAuthGuard) +@Get('profile') +getProfile(@CurrentUser() user: CurrentUserData) { + return { userId: user.userId }; +} ``` -## Database (Supabase) - -- All projects use Supabase for PostgreSQL database, auth, and storage -- Row Level Security (RLS) policies enforce access control via JWT claims -- Each project has its own Supabase project/schema -- Types typically generated via `supabase gen types` - -## Object Storage (MinIO / Hetzner) - -S3-compatible object storage for file uploads, generated images, etc. - -### Architecture - -| Environment | Service | Purpose | -|-------------|---------|---------| -| **Local** | MinIO (Docker) | S3-compatible local storage | -| **Production** | Hetzner Object Storage | Cost-effective S3-compatible cloud storage | - -### Local Development - -```bash -# Start infrastructure (includes MinIO) -pnpm docker:up - -# MinIO Web Console: http://localhost:9001 -# Username: minioadmin -# Password: minioadmin - -# S3 API endpoint: http://localhost:9000 -``` - -### Pre-configured Buckets - -| Bucket | Project | Purpose | -|--------|---------|---------| -| `picture-storage` | Picture | AI-generated images | -| `chat-storage` | Chat | User file uploads | -| `manadeck-storage` | ManaDeck | Card/deck assets | -| `nutriphi-storage` | NutriPhi | Meal photos | -| `presi-storage` | Presi | Presentation slides | -| `calendar-storage` | Calendar | Calendar attachments | -| `contacts-storage` | Contacts | Contact avatars/files | -| `storage-storage` | Storage | Cloud drive files | - -### Usage in Backend +### 4. Go-Style Error Handling +Use Result types, never throw exceptions in application code. ```typescript -import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage'; +// ✅ CORRECT +import { Result, ok, err } from '@manacore/shared-errors'; -const storage = createPictureStorage(); +async function getUser(id: string): Promise> { + if (!id) return err('INVALID_USER_ID', 'User ID required'); + return ok(user); +} -// Upload -const key = generateUserFileKey(userId, 'image.png'); -const result = await storage.upload(key, buffer, { - contentType: getContentType('image.png'), - public: true, -}); - -// Download -const data = await storage.download(key); - -// Presigned URLs -const uploadUrl = await storage.getUploadUrl(key, { expiresIn: 3600 }); +// ❌ WRONG +async function getUser(id: string): Promise { + if (!id) throw new Error('User ID required'); + return user; +} ``` -### Environment Variables +### 5. Environment Variables +Generated from `.env.development` via `pnpm setup:env` (auto-runs after install). -```env -# Local (in .env.development) -S3_ENDPOINT=http://localhost:9000 -S3_REGION=us-east-1 -S3_ACCESS_KEY=minioadmin -S3_SECRET_KEY=minioadmin +- **Mobile (Expo):** `EXPO_PUBLIC_*` prefix +- **Web (SvelteKit):** `PUBLIC_*` prefix +- **Backend (NestJS):** No prefix -# Production (Hetzner) -S3_ENDPOINT=https://fsn1.your-objectstorage.com -S3_REGION=fsn1 -S3_ACCESS_KEY=your-access-key -S3_SECRET_KEY=your-secret-key -``` - -## Landing Pages (Cloudflare Pages) - -All landing pages are deployed to Cloudflare Pages using Direct Upload via Wrangler CLI. - -### Landing Pages - -| Project | Package | Cloudflare Project | URL | -|---------|---------|-------------------|-----| -| Chat | `@chat/landing` | `chat-landing` | https://chat-landing.pages.dev | -| Picture | `@picture/landing` | `picture-landing` | https://picture-landing.pages.dev | -| ManaCore | `@manacore/landing` | `manacore-landing` | https://manacore-landing.pages.dev | -| ManaDeck | `@manadeck/landing` | `manadeck-landing` | https://manadeck-landing.pages.dev | -| Zitare | `@zitare/landing` | `zitare-landing` | https://zitare-landing.pages.dev | - -### Local Deployment +## Common Commands ```bash -# First time: Login to Cloudflare -pnpm cf:login - -# Create projects (one-time setup) -pnpm cf:projects:create - -# Deploy individual landing page -pnpm deploy:landing:chat -pnpm deploy:landing:picture -pnpm deploy:landing:manacore -pnpm deploy:landing:manadeck -pnpm deploy:landing:zitare - -# Deploy all landing pages -pnpm deploy:landing:all - -# List all projects -pnpm cf:projects:list +pnpm install # Install dependencies +pnpm dev:{app}:full # Start app with DB setup +pnpm type-check # Type check all packages +pnpm format # Format code +pnpm build # Build all packages +pnpm docker:up # Start local infrastructure +pnpm setup:env # Regenerate .env files ``` -### Adding New Landing Pages +## Documentation -1. Create the landing page in `apps/{project}/apps/landing/` -2. Add `wrangler.toml`: - ```toml - name = "{project}-landing" - compatibility_date = "2024-12-01" - pages_build_output_dir = "dist" - ``` -3. Add deploy script to root `package.json`: - ```json - "deploy:landing:{project}": "pnpm --filter @{project}/landing build && npx wrangler pages deploy apps/{project}/apps/landing/dist --project-name={project}-landing" - ``` -4. Create Cloudflare project: `npx wrangler pages project create {project}-landing --production-branch=main` +- **Code Patterns:** [.claude/GUIDELINES.md](.claude/GUIDELINES.md) - Detailed technical guidelines +- **Local Setup:** [docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md) - Complete dev environment setup +- **Database:** [docs/DATABASE_MIGRATIONS.md](docs/DATABASE_MIGRATIONS.md) - Migration best practices +- **Deployment:** [docs/DEPLOYMENT_ARCHITECTURE.md](docs/DEPLOYMENT_ARCHITECTURE.md) - Full deployment guide +- **All Docs:** [docs/README.md](docs/README.md) - Complete documentation index +- **Project-Specific:** Navigate to `apps/{project}/CLAUDE.md` for project details -### Custom Domains +## Detailed Guidelines -```bash -# Add custom domain to a project -npx wrangler pages project add-domain chat-landing chat.manacore.app -``` +For comprehensive code patterns and conventions: -## Server Access +| Guideline | Purpose | +|-----------|---------| +| [code-style.md](.claude/guidelines/code-style.md) | Formatting, naming, linting | +| [database.md](.claude/guidelines/database.md) | Drizzle ORM, schema patterns | +| [error-handling.md](.claude/guidelines/error-handling.md) | Result types, error codes | +| [authentication.md](.claude/guidelines/authentication.md) | Mana Core Auth integration | +| [nestjs-backend.md](.claude/guidelines/nestjs-backend.md) | Controllers, services, DTOs | +| [sveltekit-web.md](.claude/guidelines/sveltekit-web.md) | Svelte 5 runes, stores | +| [expo-mobile.md](.claude/guidelines/expo-mobile.md) | React Native, NativeWind | +| [testing.md](.claude/guidelines/testing.md) | Jest/Vitest, mock factories | -### Hetzner Staging Server +**Always consult these guidelines before making changes.** -SSH access for deployment troubleshooting, log inspection, and service management: +## Verification -```bash -ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214 -``` +When completing tasks, always end responses with the project signature to verify you've read this file. -**User:** `deploy` -**Key:** `~/.ssh/hetzner_deploy_key` - -## Adding Dependencies - -```bash -# Add to workspace root (dev tools only) -pnpm add -D -w - -# Add to specific project -pnpm add --filter memoro - -# Add to specific app within project -pnpm add --filter @memoro/mobile - -# Add to shared package -pnpm add --filter @manacore/shared-utils -``` - -## Environment Variables - -### Centralized Development Environment - -All development environment variables are managed from a single file: `.env.development` - -```bash -# First-time setup: generates all app-specific .env files -pnpm setup:env - -# This also runs automatically after `pnpm install` -``` - -The script reads `.env.development` and generates platform-specific `.env` files for each app with the correct prefixes: - -- **Expo mobile**: `EXPO_PUBLIC_*` prefix -- **SvelteKit web**: `PUBLIC_*` prefix -- **NestJS backend**: No prefix - -### Key Files - -- `.env.development` - Central source of truth (committed to git) -- `scripts/generate-env.mjs` - Generation script -- `apps/**/apps/**/.env` - Generated files (gitignored) - -### Adding New Variables - -1. Add the variable to `.env.development` -2. Update `scripts/generate-env.mjs` to map it to the appropriate apps -3. Run `pnpm setup:env` to regenerate - -### Platform Prefix Patterns - -**Mobile (Expo):** - -``` -EXPO_PUBLIC_SUPABASE_URL=... -EXPO_PUBLIC_SUPABASE_ANON_KEY=... -EXPO_PUBLIC_MIDDLEWARE_API_URL=... -``` - -**Web (SvelteKit):** - -``` -PUBLIC_SUPABASE_URL=... -PUBLIC_SUPABASE_ANON_KEY=... -``` - -**Backend (NestJS):** - -``` -SUPABASE_URL=... -SUPABASE_SERVICE_ROLE_KEY=... -PORT=... -``` - -## Project-Specific Documentation - -- **[docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)** - Database setup and `dev:*:full` commands -- **[docs/ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md)** - Complete environment setup guide -- **[docs/DATABASE_MIGRATIONS.md](docs/DATABASE_MIGRATIONS.md)** - Migration best practices, CI/CD, rollback procedures - -Each project has its own `CLAUDE.md` with detailed information: - -- `apps/manacore/CLAUDE.md` - Multi-app ecosystem, auth details -- `apps/manadeck/CLAUDE.md` - Card/deck management -- `apps/chat/CLAUDE.md` - Chat API endpoints, AI models -- `apps/picture/CLAUDE.md` - AI image generation -- `services/mana-core-auth/` - Central authentication service - -Navigate to the specific project directory to work on it. - -## Code Quality Infrastructure (TODO) - -A detailed plan for code quality tooling is available at `.claude/plans/proud-dancing-moon.md`. When ready to implement: - -### Planned Setup - -- **Pre-commit hooks**: Husky + lint-staged (format + lint on commit) -- **Commit messages**: Commitlint with Conventional Commits (`feat:`, `fix:`, `docs:`, etc.) -- **CI Pipeline**: GitHub Actions PR checks (lint, format, type-check, tests) -- **Formatting**: Tabs, single quotes, 100 char width (unified across all projects) -- **Test coverage**: 80% minimum for new code (once testing infrastructure is in place) - -### Key Files to Create - -``` -.husky/pre-commit # Run lint-staged -.husky/commit-msg # Run commitlint -commitlint.config.js # Conventional commit rules -.github/workflows/pr-check.yml # CI pipeline -packages/eslint-config/ # Shared ESLint configuration -``` - -### Current State - -- Testing: ~25 test files total (sparse coverage) -- Linting: Fragmented configs across projects -- CI: Only 2 backend deployment workflows exist -- Pre-commit: Only maerchenzauber has Husky (SSH URL fixer only) +**Project Signature:** 🏗️ ManaCore Monorepo diff --git a/CLAUDE.md.backup b/CLAUDE.md.backup new file mode 100644 index 000000000..f925c51e2 --- /dev/null +++ b/CLAUDE.md.backup @@ -0,0 +1,678 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Monorepo Overview + +This is a pnpm workspace monorepo containing multiple product applications with shared packages. All projects use Supabase for database/auth and follow similar architectural patterns. + +**Package Manager:** pnpm 9.15.0 (use `pnpm` for all commands) +**Build System:** Turborepo +**Node Version:** 20+ + +## Detailed Guidelines + +For comprehensive guidelines on code patterns and conventions, see the `.claude/` directory: + +| Document | Purpose | +|----------|---------| +| [`.claude/GUIDELINES.md`](.claude/GUIDELINES.md) | Main reference overview | +| [`.claude/guidelines/code-style.md`](.claude/guidelines/code-style.md) | Formatting, naming, linting | +| [`.claude/guidelines/database.md`](.claude/guidelines/database.md) | Drizzle ORM, schema patterns | +| [`.claude/guidelines/testing.md`](.claude/guidelines/testing.md) | Jest/Vitest, mock factories | +| [`.claude/guidelines/nestjs-backend.md`](.claude/guidelines/nestjs-backend.md) | Controllers, services, DTOs | +| [`.claude/guidelines/error-handling.md`](.claude/guidelines/error-handling.md) | Go-style Result types, error codes | +| [`.claude/guidelines/sveltekit-web.md`](.claude/guidelines/sveltekit-web.md) | Svelte 5 runes, stores | +| [`.claude/guidelines/expo-mobile.md`](.claude/guidelines/expo-mobile.md) | React Native, NativeWind | +| [`.claude/guidelines/authentication.md`](.claude/guidelines/authentication.md) | Mana Core Auth integration | + +**Always consult these guidelines before making changes.** + +## Projects + +| Project | Description | Apps | +| ------------ | ---------------------------- | --------------------------------------------------------- | +| **manacore** | Multi-app ecosystem platform | Expo mobile, SvelteKit web | +| **manadeck** | Card/deck management | NestJS backend, Expo mobile, SvelteKit web | +| **picture** | AI image generation | Expo mobile, SvelteKit web, Astro landing | +| **chat** | AI chat application | NestJS backend, Expo mobile, SvelteKit web, Astro landing | +| **zitare** | Daily inspiration quotes | NestJS backend, Expo mobile, SvelteKit web, Astro landing | +| **contacts** | Contact management | NestJS backend, SvelteKit web | + +### Archived Projects (`apps-archived/`) + +These projects are temporarily archived and excluded from the workspace. To re-activate, move back to `apps/`. + +| Project | Description | +| ------------------ | -------------------------------- | +| **bauntown** | Community website for developers | +| **maerchenzauber** | AI story generation | +| **memoro** | Voice memo & AI analysis | +| **news** | News aggregation | +| **nutriphi** | Nutrition tracking | +| **reader** | Reading app | +| **uload** | URL shortener | +| **wisekeep** | AI wisdom extraction from video | +| **techbase** | Software comparison platform | +| **inventory** | Inventory management | +| **presi** | Presentation tool | +| **storage** | Cloud storage | + +## Development Commands + +For detailed local development setup, see **[docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)**. + +### Quick Start (Recommended) + +Use `dev:*:full` commands to start any app with automatic database setup: + +```bash +pnpm docker:up # Start PostgreSQL, Redis, MinIO +pnpm dev:chat:full # Start chat with auth + auto DB setup +pnpm dev:zitare:full # Start zitare with auth + auto DB setup +pnpm dev:contacts:full # Start contacts with auth + auto DB setup +pnpm dev:calendar:full # Start calendar with auth + auto DB setup +pnpm dev:clock:full # Start clock with auth + auto DB setup +pnpm dev:todo:full # Start todo with auth + auto DB setup +pnpm dev:picture:full # Start picture with auth + auto DB setup +``` + +These commands automatically: +1. Create the database if missing +2. Push the latest schema +3. Start auth, backend, and web with colored output + +### Database Setup + +```bash +pnpm setup:db # Setup ALL databases and schemas +pnpm setup:db:chat # Setup just chat +pnpm setup:db:auth # Setup just auth +``` + +### Individual App Commands + +```bash +# Start specific project (runs all apps in project) +pnpm run manacore:dev +pnpm run manadeck:dev +pnpm run picture:dev +pnpm run chat:dev +pnpm run zitare:dev +pnpm run contacts:dev + +# Start specific app within project +pnpm run dev:chat:mobile # Just mobile app +pnpm run dev:chat:backend # Just NestJS backend +pnpm run dev:chat:app # Web + backend together + +# Build & quality +pnpm run build +pnpm run type-check +pnpm run format +``` + +Each project has its own `CLAUDE.md` with detailed project-specific commands. + +## Architecture Patterns + +### Monorepo Structure + +``` +manacore-monorepo/ +├── apps/ # Active SaaS product applications +│ ├── chat/ +│ │ ├── apps/ +│ │ │ ├── backend/ # NestJS API +│ │ │ ├── mobile/ # Expo React Native app +│ │ │ ├── web/ # SvelteKit web app +│ │ │ └── landing/ # Astro marketing page +│ │ └── packages/ # Project-specific shared code +│ ├── manadeck/ +│ ├── picture/ +│ └── ... +├── apps-archived/ # Archived apps (excluded from workspace) +│ ├── bauntown/ +│ ├── maerchenzauber/ +│ ├── memoro/ +│ ├── news/ +│ ├── nutriphi/ +│ ├── reader/ +│ ├── uload/ +│ └── wisekeep/ +├── games/ # Game projects +│ └── {game-name}/ # Individual games +├── services/ # Standalone microservices +│ └── mana-core-auth/ # Central authentication service +├── packages/ # Monorepo-wide shared packages +└── docker/ # Docker configuration files +``` + +### Standard Project Structure (inside apps/) + +``` +apps/{project}/ +├── apps/ +│ ├── backend/ # NestJS API (when present) +│ ├── mobile/ # Expo React Native app +│ ├── web/ # SvelteKit web app +│ └── landing/ # Astro marketing page +├── packages/ # Project-specific shared code +└── package.json +``` + +### Turborepo Configuration + +**CRITICAL: Avoid Recursive Turbo Calls** + +Parent workspace packages (e.g., `apps/chat/package.json`, `apps/zitare/package.json`) must **NEVER** have scripts that call `turbo run ` for tasks that turbo orchestrates from the root. + +```jsonc +// WRONG - Creates infinite recursion! +// apps/chat/package.json +{ + "scripts": { + "type-check": "turbo run type-check", // DON'T DO THIS + "build": "turbo run build", // DON'T DO THIS + "lint": "turbo run lint" // DON'T DO THIS + } +} + +// CORRECT - Let root turbo handle orchestration +// apps/chat/package.json +{ + "scripts": { + "dev": "turbo run dev" // OK for dev (persistent task, scoped) + // No type-check, build, lint scripts - handled by root turbo + } +} +``` + +**Why this matters:** When root turbo runs `type-check`, it finds packages with `type-check` scripts and runs them. If that script is `turbo run type-check`, it spawns another turbo process that does the same thing → infinite loop. This causes tasks to run for 10+ minutes with thousands of duplicate task entries. + +**The `dev` script exception:** Using `turbo run dev` in parent packages is acceptable because: +1. It's typically run directly on that package (scoped) +2. Dev tasks are persistent and turbo handles them differently + +**Current turbo.json settings:** +- `concurrency: "5"` - Parallel task limit (adjust based on machine) +- `type-check` has `dependsOn: ["^type-check"]` - Dependencies are checked first + +### Technology Stack by App Type + +**Mobile Apps (Expo):** + +- React Native 0.76-0.81 + Expo SDK 52-54 +- Expo Router (file-based routing) +- NativeWind (Tailwind for React Native) +- Zustand (state management) + +**Web Apps (SvelteKit):** + +- SvelteKit 2.x + Svelte 5 +- Tailwind CSS +- Supabase SSR auth + +**Landing Pages (Astro):** + +- Astro 5.x +- Tailwind CSS +- Static site generation + +**Backends (NestJS):** + +- NestJS 10-11 +- TypeScript +- Supabase integration + +### Authentication Architecture + +All projects use **mana-core-auth** as the central authentication service: + +``` +┌─────────────┐ ┌─────────────┐ ┌────────────────┐ +│ Client │────>│ Backend │────>│ mana-core-auth │ +│ (Web/Mobile)│ │ (NestJS) │ │ (port 3001) │ +└─────────────┘ └─────────────┘ └────────────────┘ + │ │ │ + │ Bearer token │ POST /validate │ + │ │ {token} │ + │ │<────────────────────│ + │ │ {valid, payload} │ + │<──────────────────│ │ + │ Response │ │ +``` + +#### Key Components + +| Component | Purpose | +| ------------------------------- | -------------------------------------------------- | +| `services/mana-core-auth` | Central auth service (Better Auth + EdDSA JWT) | +| `@manacore/shared-nestjs-auth` | Shared NestJS guards/decorators for JWT validation | +| `@mana-core/nestjs-integration` | Extended NestJS module with auth + credits | +| `@manacore/shared-auth` | Client-side auth for web/mobile apps | + +#### NestJS Backend Integration + +**Option 1: Simple auth only** - Use `@manacore/shared-nestjs-auth`: + +```typescript +// In your controller +import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth'; + +@Controller('api') +@UseGuards(JwtAuthGuard) +export class MyController { + @Get('profile') + getProfile(@CurrentUser() user: CurrentUserData) { + return { userId: user.userId, email: user.email }; + } +} +``` + +**Option 2: Auth + Credits** - Use `@mana-core/nestjs-integration`: + +```typescript +// app.module.ts +import { ManaCoreModule } from '@mana-core/nestjs-integration'; + +@Module({ + imports: [ + ManaCoreModule.forRootAsync({ + imports: [ConfigModule], + useFactory: (config: ConfigService) => ({ + appId: config.get('APP_ID'), + serviceKey: config.get('MANA_CORE_SERVICE_KEY'), + debug: config.get('NODE_ENV') === 'development', + }), + inject: [ConfigService], + }), + ], +}) +export class AppModule {} + +// In controller +import { AuthGuard } from '@mana-core/nestjs-integration/guards'; +import { CurrentUser } from '@mana-core/nestjs-integration/decorators'; +import { CreditClientService } from '@mana-core/nestjs-integration'; + +@Controller('api') +@UseGuards(AuthGuard) +export class ApiController { + constructor(private creditClient: CreditClientService) {} + + @Post('generate') + async generate(@CurrentUser() user: any) { + await this.creditClient.consumeCredits(user.sub, 'generation', 10, 'AI generation'); + // ... do work + } +} +``` + +#### Required Environment Variables + +```env +# All backends need this +MANA_CORE_AUTH_URL=http://localhost:3001 + +# For development bypass (optional) +NODE_ENV=development +DEV_BYPASS_AUTH=true +DEV_USER_ID=your-test-user-id + +# For credit operations (optional) +MANA_CORE_SERVICE_KEY=your-service-key +APP_ID=your-app-id +``` + +#### JWT Token Structure (EdDSA) + +```json +{ + "sub": "user-id", + "email": "user@example.com", + "role": "user", + "sid": "session-id", + "exp": 1764606251, + "iss": "manacore", + "aud": "manacore" +} +``` + +#### Testing Auth Integration + +```bash +# 1. Start mana-core-auth +pnpm dev:auth + +# 2. Start a backend (e.g., Zitare) +pnpm dev:zitare:backend + +# 3. Get a token +TOKEN=$(curl -s -X POST http://localhost:3001/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email": "test@example.com", "password": "password"}' | jq -r '.accessToken') + +# 4. Call protected endpoint +curl http://localhost:3007/api/favorites \ + -H "Authorization: Bearer $TOKEN" +``` + +#### Integrated Backends + +| Backend | Package | Port | +| -------- | ------------------------------- | ---- | +| Chat | `@mana-core/nestjs-integration` | 3002 | +| Picture | `@manacore/shared-nestjs-auth` | 3006 | +| Zitare | `@manacore/shared-nestjs-auth` | 3007 | +| Presi | Custom (same pattern) | 3008 | +| ManaDeck | `@mana-core/nestjs-integration` | 3009 | + +### Svelte 5 Runes Mode (Web Apps) + +All SvelteKit apps use Svelte 5 runes: + +```typescript +// CORRECT - Svelte 5 +let count = $state(0); +let doubled = $derived(count * 2); +$effect(() => { + console.log(count); +}); + +// WRONG - Old Svelte syntax +let count = 0; +$: doubled = count * 2; +``` + +## Shared Packages (`packages/`) + +| Package | Purpose | +| ------------------------------- | ----------------------------------------------- | +| `@manacore/shared-nestjs-auth` | NestJS JWT validation guards via mana-core-auth | +| `@mana-core/nestjs-integration` | NestJS module with auth guards + credit client | +| `@manacore/shared-auth` | Client-side auth service for web/mobile apps | +| `@manacore/shared-storage` | S3-compatible storage (MinIO local, Hetzner prod) | +| `@manacore/shared-supabase` | Unified Supabase client | +| `@manacore/shared-types` | Common TypeScript types | +| `@manacore/shared-utils` | Utility functions | +| `@manacore/shared-ui` | React Native UI components | +| `@manacore/shared-theme` | Theme configuration | +| `@manacore/shared-i18n` | Internationalization | + +Import shared packages: + +```typescript +import { createAuthService } from '@manacore/shared-auth'; +import { formatDate, truncate } from '@manacore/shared-utils'; +``` + +## Database (Supabase) + +- All projects use Supabase for PostgreSQL database, auth, and storage +- Row Level Security (RLS) policies enforce access control via JWT claims +- Each project has its own Supabase project/schema +- Types typically generated via `supabase gen types` + +## Object Storage (MinIO / Hetzner) + +S3-compatible object storage for file uploads, generated images, etc. + +### Architecture + +| Environment | Service | Purpose | +|-------------|---------|---------| +| **Local** | MinIO (Docker) | S3-compatible local storage | +| **Production** | Hetzner Object Storage | Cost-effective S3-compatible cloud storage | + +### Local Development + +```bash +# Start infrastructure (includes MinIO) +pnpm docker:up + +# MinIO Web Console: http://localhost:9001 +# Username: minioadmin +# Password: minioadmin + +# S3 API endpoint: http://localhost:9000 +``` + +### Pre-configured Buckets + +| Bucket | Project | Purpose | +|--------|---------|---------| +| `picture-storage` | Picture | AI-generated images | +| `chat-storage` | Chat | User file uploads | +| `manadeck-storage` | ManaDeck | Card/deck assets | +| `nutriphi-storage` | NutriPhi | Meal photos | +| `presi-storage` | Presi | Presentation slides | +| `calendar-storage` | Calendar | Calendar attachments | +| `contacts-storage` | Contacts | Contact avatars/files | +| `storage-storage` | Storage | Cloud drive files | + +### Usage in Backend + +```typescript +import { createPictureStorage, generateUserFileKey, getContentType } from '@manacore/shared-storage'; + +const storage = createPictureStorage(); + +// Upload +const key = generateUserFileKey(userId, 'image.png'); +const result = await storage.upload(key, buffer, { + contentType: getContentType('image.png'), + public: true, +}); + +// Download +const data = await storage.download(key); + +// Presigned URLs +const uploadUrl = await storage.getUploadUrl(key, { expiresIn: 3600 }); +``` + +### Environment Variables + +```env +# Local (in .env.development) +S3_ENDPOINT=http://localhost:9000 +S3_REGION=us-east-1 +S3_ACCESS_KEY=minioadmin +S3_SECRET_KEY=minioadmin + +# Production (Hetzner) +S3_ENDPOINT=https://fsn1.your-objectstorage.com +S3_REGION=fsn1 +S3_ACCESS_KEY=your-access-key +S3_SECRET_KEY=your-secret-key +``` + +## Landing Pages (Cloudflare Pages) + +All landing pages are deployed to Cloudflare Pages using Direct Upload via Wrangler CLI. + +### Landing Pages + +| Project | Package | Cloudflare Project | URL | +|---------|---------|-------------------|-----| +| Chat | `@chat/landing` | `chat-landing` | https://chat-landing.pages.dev | +| Picture | `@picture/landing` | `picture-landing` | https://picture-landing.pages.dev | +| ManaCore | `@manacore/landing` | `manacore-landing` | https://manacore-landing.pages.dev | +| ManaDeck | `@manadeck/landing` | `manadeck-landing` | https://manadeck-landing.pages.dev | +| Zitare | `@zitare/landing` | `zitare-landing` | https://zitare-landing.pages.dev | + +### Local Deployment + +```bash +# First time: Login to Cloudflare +pnpm cf:login + +# Create projects (one-time setup) +pnpm cf:projects:create + +# Deploy individual landing page +pnpm deploy:landing:chat +pnpm deploy:landing:picture +pnpm deploy:landing:manacore +pnpm deploy:landing:manadeck +pnpm deploy:landing:zitare + +# Deploy all landing pages +pnpm deploy:landing:all + +# List all projects +pnpm cf:projects:list +``` + +### Adding New Landing Pages + +1. Create the landing page in `apps/{project}/apps/landing/` +2. Add `wrangler.toml`: + ```toml + name = "{project}-landing" + compatibility_date = "2024-12-01" + pages_build_output_dir = "dist" + ``` +3. Add deploy script to root `package.json`: + ```json + "deploy:landing:{project}": "pnpm --filter @{project}/landing build && npx wrangler pages deploy apps/{project}/apps/landing/dist --project-name={project}-landing" + ``` +4. Create Cloudflare project: `npx wrangler pages project create {project}-landing --production-branch=main` + +### Custom Domains + +```bash +# Add custom domain to a project +npx wrangler pages project add-domain chat-landing chat.manacore.app +``` + +## Server Access + +### Hetzner Staging Server + +SSH access for deployment troubleshooting, log inspection, and service management: + +```bash +ssh -i ~/.ssh/hetzner_deploy_key deploy@46.224.108.214 +``` + +**User:** `deploy` +**Key:** `~/.ssh/hetzner_deploy_key` + +## Adding Dependencies + +```bash +# Add to workspace root (dev tools only) +pnpm add -D -w + +# Add to specific project +pnpm add --filter memoro + +# Add to specific app within project +pnpm add --filter @memoro/mobile + +# Add to shared package +pnpm add --filter @manacore/shared-utils +``` + +## Environment Variables + +### Centralized Development Environment + +All development environment variables are managed from a single file: `.env.development` + +```bash +# First-time setup: generates all app-specific .env files +pnpm setup:env + +# This also runs automatically after `pnpm install` +``` + +The script reads `.env.development` and generates platform-specific `.env` files for each app with the correct prefixes: + +- **Expo mobile**: `EXPO_PUBLIC_*` prefix +- **SvelteKit web**: `PUBLIC_*` prefix +- **NestJS backend**: No prefix + +### Key Files + +- `.env.development` - Central source of truth (committed to git) +- `scripts/generate-env.mjs` - Generation script +- `apps/**/apps/**/.env` - Generated files (gitignored) + +### Adding New Variables + +1. Add the variable to `.env.development` +2. Update `scripts/generate-env.mjs` to map it to the appropriate apps +3. Run `pnpm setup:env` to regenerate + +### Platform Prefix Patterns + +**Mobile (Expo):** + +``` +EXPO_PUBLIC_SUPABASE_URL=... +EXPO_PUBLIC_SUPABASE_ANON_KEY=... +EXPO_PUBLIC_MIDDLEWARE_API_URL=... +``` + +**Web (SvelteKit):** + +``` +PUBLIC_SUPABASE_URL=... +PUBLIC_SUPABASE_ANON_KEY=... +``` + +**Backend (NestJS):** + +``` +SUPABASE_URL=... +SUPABASE_SERVICE_ROLE_KEY=... +PORT=... +``` + +## Project-Specific Documentation + +- **[docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md)** - Database setup and `dev:*:full` commands +- **[docs/ENVIRONMENT_VARIABLES.md](docs/ENVIRONMENT_VARIABLES.md)** - Complete environment setup guide +- **[docs/DATABASE_MIGRATIONS.md](docs/DATABASE_MIGRATIONS.md)** - Migration best practices, CI/CD, rollback procedures + +Each project has its own `CLAUDE.md` with detailed information: + +- `apps/manacore/CLAUDE.md` - Multi-app ecosystem, auth details +- `apps/manadeck/CLAUDE.md` - Card/deck management +- `apps/chat/CLAUDE.md` - Chat API endpoints, AI models +- `apps/picture/CLAUDE.md` - AI image generation +- `services/mana-core-auth/` - Central authentication service + +Navigate to the specific project directory to work on it. + +## Code Quality Infrastructure (TODO) + +A detailed plan for code quality tooling is available at `.claude/plans/proud-dancing-moon.md`. When ready to implement: + +### Planned Setup + +- **Pre-commit hooks**: Husky + lint-staged (format + lint on commit) +- **Commit messages**: Commitlint with Conventional Commits (`feat:`, `fix:`, `docs:`, etc.) +- **CI Pipeline**: GitHub Actions PR checks (lint, format, type-check, tests) +- **Formatting**: Tabs, single quotes, 100 char width (unified across all projects) +- **Test coverage**: 80% minimum for new code (once testing infrastructure is in place) + +### Key Files to Create + +``` +.husky/pre-commit # Run lint-staged +.husky/commit-msg # Run commitlint +commitlint.config.js # Conventional commit rules +.github/workflows/pr-check.yml # CI pipeline +packages/eslint-config/ # Shared ESLint configuration +``` + +### Current State + +- Testing: ~25 test files total (sparse coverage) +- Linting: Fragmented configs across projects +- CI: Only 2 backend deployment workflows exist +- Pre-commit: Only maerchenzauber has Husky (SSH URL fixer only) diff --git a/README.md b/README.md index d3ff122b7..d549a14f8 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,54 @@ Monorepo containing all Manacore projects with shared packages and unified tooling. +## Staging URLs + +All services are deployed to staging at `*.staging.manacore.ai`. + +### Web Applications + +| App | Staging URL | Description | +|-----|-------------|-------------| +| **ManaCore** | https://staging.manacore.ai | Central dashboard for all Mana apps | +| **Chat** | https://chat.staging.manacore.ai | AI chat application | +| **Calendar** | https://calendar.staging.manacore.ai | Calendar and scheduling | +| **Clock** | https://clock.staging.manacore.ai | World clock, timers, alarms | +| **Todo** | https://todo.staging.manacore.ai | Task management | + +### Backend APIs + +| Service | Staging URL | Port | +|---------|-------------|------| +| **Auth** | https://auth.staging.manacore.ai | 3001 | +| **Chat API** | https://chat-api.staging.manacore.ai | 3002 | +| **Calendar API** | https://calendar-api.staging.manacore.ai | 3016 | +| **Clock API** | https://clock-api.staging.manacore.ai | 3017 | +| **Todo API** | https://todo-api.staging.manacore.ai | 3018 | + +### Landing Pages (Cloudflare Pages) + +| Project | URL | +|---------|-----| +| **Chat** | https://chat-landing-90m.pages.dev | +| **Picture** | https://picture-landing.pages.dev | +| **ManaCore** | https://manacore-landing.pages.dev | +| **ManaDeck** | https://manadeck-landing.pages.dev | +| **Zitare** | https://zitare-landing.pages.dev | +| **Presi** | https://presi-landing.pages.dev | + ## Projects -| Project | Description | Tech Stack | -| ------------------ | ------------------------------- | ------------------------------ | -| **maerchenzauber** | AI-powered story generation app | NestJS, Expo, SvelteKit, Astro | -| **manacore** | Multi-app ecosystem platform | Expo, SvelteKit, Astro | -| **manadeck** | Card/deck management app | NestJS, Expo, SvelteKit | -| **memoro** | Voice memo & AI analysis app | Expo, SvelteKit, Astro | +| Project | Description | Tech Stack | +|---------|-------------|------------| +| **manacore** | Multi-app ecosystem platform | Expo, SvelteKit | +| **chat** | AI chat application | NestJS, Expo, SvelteKit | +| **calendar** | Calendar & scheduling | NestJS, SvelteKit | +| **clock** | World clock, timers, alarms | NestJS, SvelteKit | +| **todo** | Task management | NestJS, SvelteKit | +| **contacts** | Contact management | NestJS, SvelteKit | +| **manadeck** | Card/deck management | NestJS, Expo, SvelteKit | +| **picture** | AI image generation | NestJS, Expo, SvelteKit | +| **zitare** | Daily inspiration quotes | NestJS, Expo, SvelteKit | ## Getting Started @@ -17,6 +57,7 @@ Monorepo containing all Manacore projects with shared packages and unified tooli - Node.js 20+ - pnpm 9.15.0+ +- Docker (for local development) ### Installation @@ -24,71 +65,77 @@ Monorepo containing all Manacore projects with shared packages and unified tooli # Install pnpm globally (if not installed) npm install -g pnpm -# Install all dependencies +# Install all dependencies (also generates .env files) pnpm install + +# Start Docker infrastructure +pnpm docker:up ``` -### Development +### Quick Start + +Use `dev:*:full` commands to start any app with automatic database setup: ```bash -# Start all projects in dev mode -pnpm run dev +pnpm docker:up # Start PostgreSQL, Redis, MinIO +pnpm dev:chat:full # Start chat with auth + auto DB setup +pnpm dev:calendar:full # Start calendar with auth + auto DB setup +pnpm dev:clock:full # Start clock with auth + auto DB setup +pnpm dev:todo:full # Start todo with auth + auto DB setup +pnpm dev:manacore:full # Start manacore with all backends +``` -# Start a specific project -pnpm run maerchenzauber:dev -pnpm run manacore:dev -pnpm run manadeck:dev -pnpm run memoro:dev +### Development Commands +```bash # Build all projects -pnpm run build - -# Run tests -pnpm run test +pnpm build # Type check -pnpm run type-check +pnpm type-check + +# Lint +pnpm lint # Format code -pnpm run format +pnpm format ``` ## Shared Packages Located in `packages/`: -| Package | Description | -| --------------------------- | --------------------------------------- | -| `@manacore/shared-types` | Common TypeScript types | -| `@manacore/shared-supabase` | Unified Supabase client | -| `@manacore/shared-utils` | Utility functions (date, string, async) | -| `@manacore/shared-ui` | React Native UI components | - -### Using Shared Packages - -```typescript -// In any project -import { User, ApiResponse } from '@manacore/shared-types'; -import { createSupabaseClient } from '@manacore/shared-supabase'; -import { formatDate, truncate, retry } from '@manacore/shared-utils'; -``` +| Package | Description | +|---------|-------------| +| `@manacore/shared-auth` | Client-side auth for web/mobile | +| `@manacore/shared-nestjs-auth` | NestJS JWT validation guards | +| `@manacore/shared-ui` | Shared Svelte UI components | +| `@manacore/shared-storage` | S3-compatible storage (MinIO/Hetzner) | +| `@manacore/shared-types` | Common TypeScript types | +| `@manacore/shared-utils` | Utility functions | +| `@manacore/shared-theme` | Theme configuration | ## Repository Structure ``` manacore-monorepo/ -├── packages/ # Shared packages -│ ├── shared-types/ # TypeScript types -│ ├── shared-supabase/ # Supabase utilities -│ ├── shared-utils/ # Common utilities -│ └── shared-ui/ # React Native components -├── maerchenzauber/ # Storyteller project -├── manacore/ # Manacore apps project -├── manadeck/ # ManaDeck project -├── memoro/ # Memoro project -├── turbo.json # Turborepo configuration -├── pnpm-workspace.yaml # Workspace configuration -└── package.json # Root package +├── apps/ # Active product applications +│ ├── manacore/ # Central dashboard +│ ├── chat/ # AI chat app +│ ├── calendar/ # Calendar app +│ ├── clock/ # Clock/timer app +│ ├── todo/ # Task management +│ ├── contacts/ # Contact management +│ ├── manadeck/ # Card/deck app +│ ├── picture/ # AI image generation +│ └── zitare/ # Daily quotes +├── apps-archived/ # Archived projects +├── games/ # Game projects +├── services/ +│ └── mana-core-auth/ # Central auth service +├── packages/ # Shared packages +├── docker/ # Docker configuration +└── .github/workflows/ # CI/CD pipelines ``` ## Tooling @@ -96,28 +143,48 @@ manacore-monorepo/ - **Package Manager:** pnpm 9.15.0 - **Build System:** Turborepo - **Formatting:** Prettier -- **Node Version:** 20 (see .nvmrc) +- **Linting:** ESLint +- **Git Hooks:** Husky (pre-commit, pre-push) +- **Node Version:** 20+ ## Adding Dependencies ```bash -# Add to root (dev tools) +# Add to workspace root (dev tools only) pnpm add -D -w # Add to specific project -pnpm add --filter maerchenzauber +pnpm add --filter @chat/web # Add to shared package pnpm add --filter @manacore/shared-utils ``` -## Contributing +## Deployment -1. Create a feature branch -2. Make changes -3. Run `pnpm run format` and `pnpm run type-check` -4. Commit with conventional commit messages -5. Create pull request +### Deploy Landing Pages + +```bash +pnpm deploy:landing:chat +pnpm deploy:landing:picture +pnpm deploy:landing:manacore +pnpm deploy:landing:all # Deploy all landing pages +``` + +### Deploy to Staging + +```bash +# Tag-based deployment (triggers CI/CD) +git tag chat-staging-v1.0.0 +git push origin chat-staging-v1.0.0 +``` + +## Documentation + +- [CLAUDE.md](CLAUDE.md) - Detailed development guidelines +- [docs/LOCAL_DEVELOPMENT.md](docs/LOCAL_DEVELOPMENT.md) - Local setup guide +- [COMMANDS.md](COMMANDS.md) - All available commands +- [cicd/DEPLOYMENT.md](cicd/DEPLOYMENT.md) - Deployment documentation ## License diff --git a/apps/calendar/apps/backend/Dockerfile b/apps/calendar/apps/backend/Dockerfile index 129862166..31cc54537 100644 --- a/apps/calendar/apps/backend/Dockerfile +++ b/apps/calendar/apps/backend/Dockerfile @@ -12,6 +12,7 @@ COPY package.json ./ COPY pnpm-lock.yaml ./ # Copy shared packages +COPY packages/better-auth-types ./packages/better-auth-types COPY packages/shared-errors ./packages/shared-errors COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth @@ -23,6 +24,9 @@ COPY apps/calendar/apps/backend ./apps/calendar/apps/backend RUN pnpm install --frozen-lockfile # Build shared packages first +WORKDIR /app/packages/better-auth-types +RUN pnpm build + WORKDIR /app/packages/shared-errors RUN pnpm build diff --git a/apps/calendar/apps/web/Dockerfile b/apps/calendar/apps/web/Dockerfile index 4f42a5e7f..518c7a609 100644 --- a/apps/calendar/apps/web/Dockerfile +++ b/apps/calendar/apps/web/Dockerfile @@ -20,6 +20,7 @@ COPY package.json ./ COPY pnpm-lock.yaml ./ # Copy shared packages needed by calendar web +COPY packages/better-auth-types ./packages/better-auth-types COPY packages/shared-auth ./packages/shared-auth COPY packages/shared-auth-ui ./packages/shared-auth-ui COPY packages/shared-branding ./packages/shared-branding @@ -47,6 +48,9 @@ COPY apps/calendar/apps/web ./apps/calendar/apps/web RUN pnpm install --frozen-lockfile # Build shared packages that need building +WORKDIR /app/packages/better-auth-types +RUN pnpm build || true + WORKDIR /app/packages/shared-auth RUN pnpm build || true @@ -70,6 +74,10 @@ COPY --from=builder /app/apps/calendar/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/calendar/apps/web/build ./build COPY --from=builder /app/apps/calendar/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/calendar/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 5186 @@ -82,5 +90,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:5186/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/calendar/apps/web/docker-entrypoint.sh b/apps/calendar/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..32d58c329 --- /dev/null +++ b/apps/calendar/apps/web/docker-entrypoint.sh @@ -0,0 +1,37 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +BACKEND_URL=${BACKEND_URL:-"http://localhost:3016"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} +TODO_API_URL=${TODO_API_URL:-"http://localhost:3018"} +CONTACTS_API_URL=${CONTACTS_API_URL:-"http://localhost:3015"} + +echo "📝 Config values:" +echo " BACKEND_URL: $BACKEND_URL" +echo " AUTH_URL: $AUTH_URL" +echo " TODO_API_URL: $TODO_API_URL" +echo " CONTACTS_API_URL: $CONTACTS_API_URL" + +# Generate config.json from environment variables +cat > /app/apps/calendar/apps/web/build/client/config.json < | null = null; -const contactsClient = createApiClient({ - baseUrl: CONTACTS_API_BASE, - apiPrefix: '/api/v1', -}); +async function getContactsClient() { + if (!contactsClient) { + const contactsApiUrl = await getContactsApiUrl(); + contactsClient = createApiClient({ + baseUrl: contactsApiUrl, + apiPrefix: '/api/v1', + }); + } + return contactsClient; +} // ============================================ // Types for Birthday Integration @@ -61,7 +68,13 @@ interface BirthdaysResponse { // API Functions // ============================================ -const fetchContactsApi = contactsClient.fetchApi; +async function fetchContactsApi( + endpoint: string, + options?: Parameters['fetchApi']>[1] +) { + const client = await getContactsClient(); + return client.fetchApi(endpoint, options); +} /** * Fetch all contacts with birthdays from Contacts service diff --git a/apps/calendar/apps/web/src/lib/api/client.ts b/apps/calendar/apps/web/src/lib/api/client.ts index 94103f722..0c42ccd35 100644 --- a/apps/calendar/apps/web/src/lib/api/client.ts +++ b/apps/calendar/apps/web/src/lib/api/client.ts @@ -1,25 +1,33 @@ /** * API Client for Calendar Backend * + * Uses runtime configuration (12-factor pattern) instead of build-time env vars. * Token handling: Uses authStore.getValidToken() which automatically * refreshes expired tokens before making requests. */ -import { env } from '$env/dynamic/public'; +import { getBackendUrl } from '$lib/config/runtime'; import { createApiClient, type FetchOptions, type ApiResult } from './base-client'; -const API_BASE = env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; +let calendarClient: ReturnType | null = null; -const calendarClient = createApiClient({ - baseUrl: API_BASE, - apiPrefix: '/api/v1', -}); +async function getClient() { + if (!calendarClient) { + const backendUrl = await getBackendUrl(); + calendarClient = createApiClient({ + baseUrl: backendUrl, + apiPrefix: '/api/v1', + }); + } + return calendarClient; +} export async function fetchApi( endpoint: string, options: FetchOptions = {} ): Promise> { - return calendarClient.fetchApi(endpoint, options); + const client = await getClient(); + return client.fetchApi(endpoint, options); } // Re-export types for backwards compatibility diff --git a/apps/calendar/apps/web/src/lib/api/todos.ts b/apps/calendar/apps/web/src/lib/api/todos.ts index db72aac5d..2737d95df 100644 --- a/apps/calendar/apps/web/src/lib/api/todos.ts +++ b/apps/calendar/apps/web/src/lib/api/todos.ts @@ -3,15 +3,22 @@ * Allows Calendar app to fetch/manage todos from the Todo service */ -import { env } from '$env/dynamic/public'; import { createApiClient, buildQueryString } from './base-client'; +import { getTodoApiUrl } from '$lib/config/runtime'; -const TODO_API_BASE = env.PUBLIC_TODO_BACKEND_URL || 'http://localhost:3018'; +// Lazy-initialized client (runtime config is async) +let todoClient: ReturnType | null = null; -const todoClient = createApiClient({ - baseUrl: TODO_API_BASE, - apiPrefix: '/api/v1', -}); +async function getTodoClient() { + if (!todoClient) { + const todoApiUrl = await getTodoApiUrl(); + todoClient = createApiClient({ + baseUrl: todoApiUrl, + apiPrefix: '/api/v1', + }); + } + return todoClient; +} // ============================================ // Types (mirrored from @todo/shared for cross-app use) @@ -173,7 +180,13 @@ interface LabelsResponse { // API Client (using shared base client) // ============================================ -const fetchTodoApi = todoClient.fetchApi; +async function fetchTodoApi( + endpoint: string, + options?: Parameters['fetchApi']>[1] +) { + const client = await getTodoClient(); + return client.fetchApi(endpoint, options); +} // ============================================ // Task API Functions diff --git a/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte b/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte index 9ee69b7d8..8475bdc3c 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/CalendarHeader.svelte @@ -94,11 +94,11 @@ } + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbarContent.svelte b/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbarContent.svelte index 84df580eb..2d8a0eb65 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbarContent.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/CalendarToolbarContent.svelte @@ -21,13 +21,19 @@ // View type labels const viewLabels: Record = { day: 'Tag', + '3day': '3 Tage', '5day': '5 Tage', week: 'Woche', '10day': '10 Tage', '14day': '14 Tage', + '30day': '30 Tage', + '60day': '60 Tage', + '90day': '90 Tage', + '365day': '365 Tage', month: 'Monat', year: 'Jahr', agenda: 'Agenda', + custom: 'Benutzerdefiniert', }; // Views to show in selector diff --git a/apps/calendar/apps/web/src/lib/components/calendar/StatsSidebarSection.svelte b/apps/calendar/apps/web/src/lib/components/calendar/StatsSidebarSection.svelte index a739ad0a4..cd9b8c332 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/StatsSidebarSection.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/StatsSidebarSection.svelte @@ -126,13 +126,13 @@
{#each miniTrend as day} -
+
0} >
- {day.label.charAt(0)} + {day.label?.charAt(0) || ''}
{/each}
diff --git a/apps/calendar/apps/web/src/lib/components/calendar/TagStripModal.svelte b/apps/calendar/apps/web/src/lib/components/calendar/TagStripModal.svelte index 6a4e6ea3a..b40b27e8c 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/TagStripModal.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/TagStripModal.svelte @@ -421,6 +421,7 @@
+
- - {#each eventTagGroupsStore.groups as group (group.id)} @@ -471,6 +472,7 @@
+
- - {#each eventTagGroupsStore.groups as group (group.id)} @@ -524,6 +526,7 @@
+
+ = { day: '1', + '3day': '3', '5day': '5', week: '7', '10day': '10', '14day': '14', + '30day': '30', + '60day': '60', + '90day': '90', + '365day': '365', month: 'M', year: 'Y', agenda: 'A', + custom: '', }; // View titles for tooltip const viewTitles: Record = { day: 'Tagesansicht', + '3day': '3-Tage-Ansicht', '5day': '5-Tage-Ansicht', week: 'Wochenansicht', '10day': '10-Tage-Ansicht', '14day': '14-Tage-Ansicht', + '30day': '30-Tage-Ansicht', + '60day': '60-Tage-Ansicht', + '90day': '90-Tage-Ansicht', + '365day': '365-Tage-Ansicht', month: 'Monatsansicht', year: 'Jahresansicht', agenda: 'Agenda', + custom: 'Benutzerdefiniert', }; // Get enabled views from settings diff --git a/apps/calendar/apps/web/src/lib/components/calendar/ViewModePillContextMenu.svelte b/apps/calendar/apps/web/src/lib/components/calendar/ViewModePillContextMenu.svelte index 38d43c725..01233cba1 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/ViewModePillContextMenu.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/ViewModePillContextMenu.svelte @@ -183,9 +183,10 @@ {#if visible} - + + {#if onEditGroup} {/if} - +
{#if isExpanded(group.id)} diff --git a/apps/calendar/apps/web/src/lib/config/runtime.ts b/apps/calendar/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..2163c83b7 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,145 @@ +/** + * Runtime Configuration for Calendar App + * + * 12-Factor Pattern: Configuration is loaded from /config.json at runtime, + * generated by Docker entrypoint from environment variables. + * This allows the same Docker image to run in different environments + * without rebuilding. + */ + +import { browser, dev } from '$app/environment'; +import { z } from 'zod'; + +export interface RuntimeConfig { + BACKEND_URL: string; + AUTH_URL: string; + TODO_API_URL: string; + CONTACTS_API_URL: string; +} + +/** + * Schema validation for config.json + * Ensures all required configuration is present and valid + */ +const ConfigSchema = z.object({ + BACKEND_URL: z.string().url().min(1, 'BACKEND_URL must be a valid URL'), + AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), + TODO_API_URL: z.string().url().min(1, 'TODO_API_URL must be a valid URL'), + CONTACTS_API_URL: z.string().url().min(1, 'CONTACTS_API_URL must be a valid URL'), +}); + +/** + * Development defaults - only used when: + * 1. dev === true (from $app/environment) + * 2. /config.json fetch fails + * + * In production, missing config.json is a deployment error. + */ +const DEV_CONFIG: RuntimeConfig = { + BACKEND_URL: 'http://localhost:3016', + AUTH_URL: 'http://localhost:3001', + TODO_API_URL: 'http://localhost:3018', + CONTACTS_API_URL: 'http://localhost:3015', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load configuration from /config.json + * Fail-hard in production if config is missing or invalid + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Calendar] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Calendar] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) return cachedConfig; + + // Return existing promise if already loading + if (configPromise) return configPromise; + + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Calendar] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Calendar] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + throw new Error( + `[Calendar] Invalid config.json schema: ${result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}` + ); + } + } + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await loadConfig(); + return config.AUTH_URL; +} + +/** + * Get backend API URL + */ +export async function getBackendUrl(): Promise { + const config = await loadConfig(); + return config.BACKEND_URL; +} + +/** + * Get todo service URL + */ +export async function getTodoApiUrl(): Promise { + const config = await loadConfig(); + return config.TODO_API_URL; +} + +/** + * Get contacts service URL + */ +export async function getContactsApiUrl(): Promise { + const config = await loadConfig(); + return config.CONTACTS_API_URL; +} + +/** + * Get full runtime config + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Initialize configuration (call early in app lifecycle) + * This triggers the config load and caches it for subsequent calls + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts index ced1c1ad4..229061c02 100644 --- a/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/auth.svelte.ts @@ -1,45 +1,25 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth + * Uses Mana Core Auth with runtime configuration (12-factor pattern) */ import { browser } from '$app/environment'; import { initializeWebAuth } from '@manacore/shared-auth'; import type { UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3014'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3014'; -} +import { getAuthUrl, getBackendUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { + const authUrl = await getAuthUrl(); + const backendUrl = await getBackendUrl(); const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; @@ -47,10 +27,10 @@ function getAuthService() { return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -80,7 +60,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -107,7 +87,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -134,7 +114,7 @@ export const authStore = { * Sign up with email and password */ async signUp(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -164,7 +144,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -184,7 +164,7 @@ export const authStore = { * Send password reset email */ async resetPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -208,7 +188,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -220,7 +200,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts b/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts index bf9489a31..ae9b5e87b 100644 --- a/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/calendars.svelte.ts @@ -20,6 +20,7 @@ const birthdayCalendar: Calendar = { color: BIRTHDAY_CALENDAR.color, isDefault: false, isVisible: true, // Visibility controlled by settingsStore.showBirthdays + timezone: 'UTC', createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }; diff --git a/apps/calendar/apps/web/src/lib/stores/search.svelte.ts b/apps/calendar/apps/web/src/lib/stores/search.svelte.ts index 64c93eddb..8c3c2c924 100644 --- a/apps/calendar/apps/web/src/lib/stores/search.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/search.svelte.ts @@ -4,7 +4,8 @@ interface SearchItem { id: string; - [key: string]: unknown; + title?: string; + subtitle?: string; } // State diff --git a/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts index a2e5e08b7..dace27217 100644 --- a/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts +++ b/apps/calendar/apps/web/src/lib/stores/user-settings.svelte.ts @@ -5,14 +5,17 @@ * - Global settings that apply to all apps * - Per-app overrides for customization * - localStorage caching for offline support + * + * Uses lazy initialization to wait for runtime config to load. */ import { browser } from '$app/environment'; import { createUserSettingsStore } from '@manacore/shared-theme'; import { authStore } from './auth.svelte'; +import { getAuthUrl as getRuntimeAuthUrl } from '$lib/config/runtime'; -// Get auth URL dynamically at runtime -function getAuthUrl(): string { +// Get auth URL with fallback for early access (before runtime config loads) +function getAuthUrlSync(): string { if (browser && typeof window !== 'undefined') { const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) .__PUBLIC_MANA_CORE_AUTH_URL__; @@ -21,8 +24,128 @@ function getAuthUrl(): string { return 'http://localhost:3001'; } -export const userSettings = createUserSettingsStore({ - appId: 'calendar', - authUrl: getAuthUrl(), - getAccessToken: () => authStore.getAccessToken(), -}); +// Lazy-initialized store (created after runtime config is loaded) +let _store: ReturnType | null = null; + +function getOrCreateStore(authUrl?: string) { + if (!_store) { + _store = createUserSettingsStore({ + appId: 'calendar', + authUrl: authUrl || getAuthUrlSync(), + getAccessToken: () => authStore.getAccessToken(), + }); + } + return _store; +} + +// Ensure store is initialized with correct URL from runtime config +async function ensureStore() { + if (!_store) { + const authUrl = await getRuntimeAuthUrl(); + _store = createUserSettingsStore({ + appId: 'calendar', + authUrl, + getAccessToken: () => authStore.getAccessToken(), + }); + } + return _store; +} + +// Export proxy that lazily initializes the store +export const userSettings = { + // Getters - use sync store (may have fallback URL initially) + get nav() { + return getOrCreateStore().nav; + }, + get theme() { + return getOrCreateStore().theme; + }, + get locale() { + return getOrCreateStore().locale; + }, + get general() { + return getOrCreateStore().general; + }, + get startPage() { + return getOrCreateStore().startPage; + }, + get globalSettings() { + return getOrCreateStore().globalSettings; + }, + get hasAppOverride() { + return getOrCreateStore().hasAppOverride; + }, + get syncing() { + return getOrCreateStore().syncing; + }, + get loaded() { + return getOrCreateStore().loaded; + }, + get deviceId() { + return getOrCreateStore().deviceId; + }, + get deviceSettings() { + return getOrCreateStore().deviceSettings; + }, + get currentDeviceAppSettings() { + return getOrCreateStore().currentDeviceAppSettings; + }, + + // Methods that make API calls - ensure store has correct URL + async load() { + const store = await ensureStore(); + return store.load(); + }, + async updateGlobal( + settings: Parameters['updateGlobal']>[0] + ) { + const store = await ensureStore(); + return store.updateGlobal(settings); + }, + async updateAppOverride( + settings: Parameters['updateAppOverride']>[0] + ) { + const store = await ensureStore(); + return store.updateAppOverride(settings); + }, + async removeAppOverride() { + const store = await ensureStore(); + return store.removeAppOverride(); + }, + async setStartPage(appId: string, path: string) { + const store = await ensureStore(); + return store.setStartPage(appId, path); + }, + async updateGeneral( + settings: Parameters['updateGeneral']>[0] + ) { + const store = await ensureStore(); + return store.updateGeneral(settings); + }, + getHiddenNavItemsForApp(appId: string) { + return getOrCreateStore().getHiddenNavItemsForApp(appId); + }, + async toggleNavItemVisibility(appId: string, href: string) { + const store = await ensureStore(); + return store.toggleNavItemVisibility(appId, href); + }, + async setHiddenNavItems(appId: string, hiddenHrefs: string[]) { + const store = await ensureStore(); + return store.setHiddenNavItems(appId, hiddenHrefs); + }, + async updateDeviceAppSettings(settings: Record) { + const store = await ensureStore(); + return store.updateDeviceAppSettings(settings); + }, + getDeviceAppSettings() { + return getOrCreateStore().getDeviceAppSettings(); + }, + async getDevices() { + const store = await ensureStore(); + return store.getDevices(); + }, + async removeDevice(deviceId: string) { + const store = await ensureStore(); + return store.removeDevice(deviceId); + }, +}; diff --git a/apps/calendar/apps/web/src/routes/(app)/settings/+page.svelte b/apps/calendar/apps/web/src/routes/(app)/settings/+page.svelte index d270cea1c..f8aab5bec 100644 --- a/apps/calendar/apps/web/src/routes/(app)/settings/+page.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/settings/+page.svelte @@ -128,13 +128,19 @@ // View labels const viewLabels: Record = { day: 'Tag', + '3day': '3 Tage', '5day': '5 Tage', week: 'Woche', '10day': '10 Tage', '14day': '14 Tage', + '30day': '30 Tage', + '60day': '60 Tage', + '90day': '90 Tage', + '365day': '365 Tage', month: 'Monat', year: 'Jahr', agenda: 'Agenda', + custom: 'Benutzerdefiniert', }; // Duration options in minutes diff --git a/apps/calendar/apps/web/src/routes/+layout.svelte b/apps/calendar/apps/web/src/routes/+layout.svelte index 2256dee79..7328202b8 100644 --- a/apps/calendar/apps/web/src/routes/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/+layout.svelte @@ -15,6 +15,16 @@ let loading = $state(true); onMount(async () => { + // Initialize runtime config first (12-factor pattern) + const { initializeConfig, getConfig } = await import('$lib/config/runtime'); + await initializeConfig(); + + // Inject config into window for stores that need synchronous access + const config = await getConfig(); + ( + window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string } + ).__PUBLIC_MANA_CORE_AUTH_URL__ = config.AUTH_URL; + // Wait for i18n locale to be loaded await waitLocale(); diff --git a/apps/calendar/apps/web/src/routes/+layout.ts b/apps/calendar/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/calendar/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/calendar/apps/web/static/config.json b/apps/calendar/apps/web/static/config.json new file mode 100644 index 000000000..7f993ac39 --- /dev/null +++ b/apps/calendar/apps/web/static/config.json @@ -0,0 +1,6 @@ +{ + "BACKEND_URL": "http://localhost:3016", + "AUTH_URL": "http://localhost:3001", + "TODO_API_URL": "http://localhost:3018", + "CONTACTS_API_URL": "http://localhost:3015" +} diff --git a/apps/chat/apps/backend/Dockerfile b/apps/chat/apps/backend/Dockerfile index 9bec7862f..ef8672067 100644 --- a/apps/chat/apps/backend/Dockerfile +++ b/apps/chat/apps/backend/Dockerfile @@ -12,6 +12,7 @@ COPY package.json ./ COPY pnpm-lock.yaml ./ # Copy shared packages +COPY packages/better-auth-types ./packages/better-auth-types COPY packages/shared-errors ./packages/shared-errors COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth COPY packages/shared-storage ./packages/shared-storage @@ -23,6 +24,9 @@ COPY apps/chat/apps/backend ./apps/chat/apps/backend RUN pnpm install --frozen-lockfile # Build shared packages first +WORKDIR /app/packages/better-auth-types +RUN pnpm build + WORKDIR /app/packages/shared-errors RUN pnpm build diff --git a/apps/chat/apps/web/Dockerfile b/apps/chat/apps/web/Dockerfile index 544d462a2..5e829acbc 100644 --- a/apps/chat/apps/web/Dockerfile +++ b/apps/chat/apps/web/Dockerfile @@ -20,6 +20,7 @@ COPY package.json ./ COPY pnpm-lock.yaml ./ # Copy shared packages needed by chat web +COPY packages/better-auth-types ./packages/better-auth-types COPY packages/shared-auth ./packages/shared-auth COPY packages/shared-auth-ui ./packages/shared-auth-ui COPY packages/shared-branding ./packages/shared-branding @@ -45,6 +46,9 @@ COPY apps/chat/apps/web ./apps/chat/apps/web RUN pnpm install --frozen-lockfile # Build shared packages that need building +WORKDIR /app/packages/better-auth-types +RUN pnpm build || true + WORKDIR /app/packages/shared-auth RUN pnpm build || true @@ -68,6 +72,10 @@ COPY --from=builder /app/apps/chat/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/chat/apps/web/build ./build COPY --from=builder /app/apps/chat/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/chat/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 3000 @@ -80,5 +88,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/chat/apps/web/docker-entrypoint.sh b/apps/chat/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..caeac21b2 --- /dev/null +++ b/apps/chat/apps/web/docker-entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +BACKEND_URL=${BACKEND_URL:-"http://localhost:3002"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} + +echo "📝 Config values:" +echo " BACKEND_URL: $BACKEND_URL" +echo " AUTH_URL: $AUTH_URL" + +# Generate config.json from environment variables +cat > /app/apps/chat/apps/web/build/client/config.json <
+
- -
+ Farbe +
{#each TEMPLATE_COLORS as color} + +
diff --git a/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte b/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte index deaf8eafa..5a991d86c 100644 --- a/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/templates/+page.svelte @@ -81,11 +81,11 @@ await templatesStore.createTemplate({ userId: authStore.user.id, name: data.name!, - description: data.description ?? null, + description: data.description, systemPrompt: data.systemPrompt!, - initialQuestion: data.initialQuestion ?? null, + initialQuestion: data.initialQuestion, color: data.color!, - modelId: data.modelId ?? null, + modelId: data.modelId, isDefault: false, documentMode: data.documentMode ?? false, }); diff --git a/apps/chat/apps/web/src/routes/+layout.svelte b/apps/chat/apps/web/src/routes/+layout.svelte index cb7ce45ce..fb227426d 100644 --- a/apps/chat/apps/web/src/routes/+layout.svelte +++ b/apps/chat/apps/web/src/routes/+layout.svelte @@ -2,13 +2,17 @@ import '../app.css'; import { onMount } from 'svelte'; import { theme } from '$lib/stores/theme'; + import { initializeConfig } from '$lib/config/runtime'; import Toast from '$lib/components/Toast.svelte'; let { children } = $props(); onMount(() => { - const cleanup = theme.initialize(); - return cleanup; + // Initialize runtime config first (12-factor pattern) + initializeConfig(); + + // Initialize theme + return theme.initialize(); }); diff --git a/apps/chat/apps/web/src/routes/+layout.ts b/apps/chat/apps/web/src/routes/+layout.ts new file mode 100644 index 000000000..a546134d2 --- /dev/null +++ b/apps/chat/apps/web/src/routes/+layout.ts @@ -0,0 +1,10 @@ +/** + * Layout Configuration + * + * Disable SSR - this is a client-only SPA that: + * - Requires authentication (no SEO benefit) + * - Fetches all data client-side via authenticated APIs + * - Loads runtime config from /config.json (browser-only) + */ + +export const ssr = false; diff --git a/apps/chat/apps/web/static/config.json b/apps/chat/apps/web/static/config.json new file mode 100644 index 000000000..106f83593 --- /dev/null +++ b/apps/chat/apps/web/static/config.json @@ -0,0 +1,4 @@ +{ + "BACKEND_URL": "http://localhost:3002", + "AUTH_URL": "http://localhost:3001" +} diff --git a/apps/chat/packages/chat-types/src/index.ts b/apps/chat/packages/chat-types/src/index.ts index fec5145e5..7acfb5c86 100644 --- a/apps/chat/packages/chat-types/src/index.ts +++ b/apps/chat/packages/chat-types/src/index.ts @@ -71,10 +71,10 @@ export interface Template { id: string; userId: string; name: string; - description: string | null; + description?: string; systemPrompt: string; - initialQuestion: string | null; - modelId: string | null; + initialQuestion?: string; + modelId?: string; color: string; isDefault: boolean; documentMode: boolean; diff --git a/apps/clock/apps/backend/Dockerfile b/apps/clock/apps/backend/Dockerfile index a2b05c80b..c4d9bb9ae 100644 --- a/apps/clock/apps/backend/Dockerfile +++ b/apps/clock/apps/backend/Dockerfile @@ -12,6 +12,7 @@ COPY package.json ./ COPY pnpm-lock.yaml ./ # Copy shared packages +COPY packages/better-auth-types ./packages/better-auth-types COPY packages/shared-errors ./packages/shared-errors COPY packages/shared-nestjs-auth ./packages/shared-nestjs-auth @@ -23,6 +24,9 @@ COPY apps/clock/apps/backend ./apps/clock/apps/backend RUN pnpm install --frozen-lockfile # Build shared packages first +WORKDIR /app/packages/better-auth-types +RUN pnpm build + WORKDIR /app/packages/shared-errors RUN pnpm build diff --git a/apps/clock/apps/web/Dockerfile b/apps/clock/apps/web/Dockerfile index 2f5e6c366..f7551125e 100644 --- a/apps/clock/apps/web/Dockerfile +++ b/apps/clock/apps/web/Dockerfile @@ -20,6 +20,7 @@ COPY package.json ./ COPY pnpm-lock.yaml ./ # Copy shared packages needed by clock web +COPY packages/better-auth-types ./packages/better-auth-types COPY packages/shared-auth ./packages/shared-auth COPY packages/shared-auth-ui ./packages/shared-auth-ui COPY packages/shared-branding ./packages/shared-branding @@ -45,6 +46,9 @@ COPY apps/clock/apps/web ./apps/clock/apps/web RUN pnpm install --frozen-lockfile # Build shared packages that need building +WORKDIR /app/packages/better-auth-types +RUN pnpm build || true + WORKDIR /app/packages/shared-auth RUN pnpm build || true @@ -68,6 +72,10 @@ COPY --from=builder /app/apps/clock/apps/web/node_modules ./node_modules COPY --from=builder /app/apps/clock/apps/web/build ./build COPY --from=builder /app/apps/clock/apps/web/package.json ./ +# Copy entrypoint script for runtime config generation +COPY apps/clock/apps/web/docker-entrypoint.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + # Expose port EXPOSE 5187 @@ -80,5 +88,8 @@ ENV HOST=0.0.0.0 HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD wget --no-verbose --tries=1 --spider http://localhost:5187/health || exit 1 +# Use entrypoint to generate runtime config +ENTRYPOINT ["docker-entrypoint.sh"] + # Run the app CMD ["node", "build"] diff --git a/apps/clock/apps/web/docker-entrypoint.sh b/apps/clock/apps/web/docker-entrypoint.sh new file mode 100644 index 000000000..8eb1ee772 --- /dev/null +++ b/apps/clock/apps/web/docker-entrypoint.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +echo "🔧 Generating runtime configuration..." + +# Environment variables with development defaults +API_BASE_URL=${API_BASE_URL:-"http://localhost:3017"} +AUTH_URL=${AUTH_URL:-"http://localhost:3001"} + +echo "📝 Config values:" +echo " API_BASE_URL: $API_BASE_URL" +echo " AUTH_URL: $AUTH_URL" + +# Generate config.json from environment variables +cat > /app/apps/clock/apps/web/build/client/config.json < { data?: T; @@ -17,6 +17,7 @@ export async function fetchApi( ): Promise> { try { const token = await authStore.getAccessToken(); + const apiBaseUrl = await getApiBaseUrl(); const headers: HeadersInit = { 'Content-Type': 'application/json', @@ -27,7 +28,7 @@ export async function fetchApi( (headers as Record)['Authorization'] = `Bearer ${token}`; } - const response = await fetch(`${API_URL}${endpoint}`, { + const response = await fetch(`${apiBaseUrl}/api/v1${endpoint}`, { ...options, headers, }); diff --git a/apps/clock/apps/web/src/lib/api/feedback.ts b/apps/clock/apps/web/src/lib/api/feedback.ts new file mode 100644 index 000000000..10e8a5dda --- /dev/null +++ b/apps/clock/apps/web/src/lib/api/feedback.ts @@ -0,0 +1,23 @@ +/** + * Feedback Service Instance for Clock Web App + */ + +import { createFeedbackService } from '@manacore/shared-feedback-service'; +import { authStore } from '$lib/stores/auth.svelte'; +import { browser } from '$app/environment'; + +// Get auth URL dynamically at runtime +function getAuthUrl(): string { + if (browser && typeof window !== 'undefined') { + const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) + .__PUBLIC_MANA_CORE_AUTH_URL__; + return injectedUrl || 'http://localhost:3001'; + } + return 'http://localhost:3001'; +} + +export const feedbackService = createFeedbackService({ + apiUrl: getAuthUrl(), + appId: 'clock', + getAuthToken: async () => authStore.getAccessToken(), +}); diff --git a/apps/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte b/apps/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte index 8327c6928..d2318567c 100644 --- a/apps/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte +++ b/apps/clock/apps/web/src/lib/components/life-clock/CircularProgress.svelte @@ -20,7 +20,8 @@ let circumference = $derived(2 * Math.PI * radius); let dashOffset = $derived(circumference - (percentage / 100) * circumference); - // Animation + // Animation - intentionally captures initial circumference for animation start + // svelte-ignore state_referenced_locally let animatedOffset = $state(circumference); let mounted = $state(false); diff --git a/apps/clock/apps/web/src/lib/config/runtime.ts b/apps/clock/apps/web/src/lib/config/runtime.ts new file mode 100644 index 000000000..e6b61b21d --- /dev/null +++ b/apps/clock/apps/web/src/lib/config/runtime.ts @@ -0,0 +1,123 @@ +/** + * Runtime Configuration Loader + * + * Implements 12-factor app "Config in Environment" principle. + * Configuration is loaded at runtime from /config.json generated by Docker entrypoint, + * allowing the same Docker image to work across all environments. + * + * Pattern: Client-only SPA (SSR disabled via +layout.ts) + * - Browser: Fetches /config.json (generated by docker-entrypoint.sh) + * - Validation: Enforces schema in production (fail hard on misconfiguration) + * - Dev fallback: Only when dev=true, never in staging/prod + */ + +import { browser, dev } from '$app/environment'; +import { z } from 'zod'; + +export interface RuntimeConfig { + API_BASE_URL: string; + AUTH_URL: string; +} + +const ConfigSchema = z.object({ + API_BASE_URL: z.string().url().min(1, 'API_BASE_URL must be a valid URL'), + AUTH_URL: z.string().url().min(1, 'AUTH_URL must be a valid URL'), +}); + +// Development fallback configuration (only used when dev=true) +const DEV_CONFIG: RuntimeConfig = { + API_BASE_URL: 'http://localhost:3017', + AUTH_URL: 'http://localhost:3001', +}; + +let cachedConfig: RuntimeConfig | null = null; +let configPromise: Promise | null = null; + +/** + * Load runtime configuration from /config.json + * Uses caching to avoid multiple fetches + */ +async function loadConfig(): Promise { + // Guard: SSR should never happen (we disabled it in +layout.ts) + if (!browser) { + if (dev) { + console.warn('[Clock] Config accessed during SSR in dev mode, using fallback'); + return DEV_CONFIG; + } + throw new Error('[Clock] Runtime config called on server - SSR should be disabled'); + } + + // Return cached config if available + if (cachedConfig) { + return cachedConfig; + } + + // If already loading, return the existing promise + if (configPromise) { + return configPromise; + } + + // Fetch config from /config.json (generated by docker-entrypoint.sh) + configPromise = fetch('/config.json') + .then((res) => { + if (!res.ok) { + if (dev) { + console.warn( + `[Clock] Failed to load /config.json (HTTP ${res.status}), using dev defaults` + ); + return DEV_CONFIG; + } + throw new Error( + `[Clock] Failed to load /config.json (HTTP ${res.status}) - check Docker entrypoint script` + ); + } + return res.json(); + }) + .then((config) => { + // Validate schema in production (fail hard on misconfiguration) + if (!dev) { + const result = ConfigSchema.safeParse(config); + if (!result.success) { + throw new Error( + `[Clock] Invalid config.json schema: ${result.error.errors.map((e) => `${e.path.join('.')}: ${e.message}`).join(', ')}` + ); + } + } + + cachedConfig = config as RuntimeConfig; + return cachedConfig; + }); + + return configPromise; +} + +/** + * Get the full runtime configuration + */ +export async function getConfig(): Promise { + return loadConfig(); +} + +/** + * Get the Auth service URL + */ +export async function getAuthUrl(): Promise { + const config = await getConfig(); + return config.AUTH_URL; +} + +/** + * Get the API base URL + */ +export async function getApiBaseUrl(): Promise { + const config = await getConfig(); + return config.API_BASE_URL; +} + +/** + * Initialize runtime configuration + * Call this early in app lifecycle (e.g., +layout.svelte onMount) + */ +export async function initializeConfig(): Promise { + await loadConfig(); +} diff --git a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts index 931f6327e..d6f630e3b 100644 --- a/apps/clock/apps/web/src/lib/stores/auth.svelte.ts +++ b/apps/clock/apps/web/src/lib/stores/auth.svelte.ts @@ -1,44 +1,24 @@ /** * Auth Store - Manages authentication state using Svelte 5 runes - * Uses Mana Core Auth + * Uses Mana Core Auth with runtime configuration */ import { browser } from '$app/environment'; import { initializeWebAuth, type UserData } from '@manacore/shared-auth'; - -// Get auth URL dynamically at runtime - fallback for SSR and client -function getAuthUrl(): string { - if (browser && typeof window !== 'undefined') { - // Client-side: use injected window variable (set by hooks.server.ts) - // Falls back to localhost for local development - const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string }) - .__PUBLIC_MANA_CORE_AUTH_URL__; - return injectedUrl || 'http://localhost:3001'; - } - // Server-side (SSR): use Docker internal URL for container-to-container communication - return process.env.PUBLIC_MANA_CORE_AUTH_URL || 'http://localhost:3001'; -} - -// Get backend URL dynamically at runtime -function getBackendUrl(): string { - if (browser && typeof window !== 'undefined') { - const injectedUrl = (window as unknown as { __PUBLIC_BACKEND_URL__?: string }) - .__PUBLIC_BACKEND_URL__; - return injectedUrl || 'http://localhost:3017'; - } - return process.env.PUBLIC_BACKEND_URL || 'http://localhost:3017'; -} +import { getAuthUrl, getApiBaseUrl } from '$lib/config/runtime'; // Lazy initialization to avoid SSR issues with localStorage let _authService: ReturnType['authService'] | null = null; let _tokenManager: ReturnType['tokenManager'] | null = null; -function getAuthService() { +async function getAuthService() { if (!browser) return null; if (!_authService) { + const authUrl = await getAuthUrl(); + const backendUrl = await getApiBaseUrl(); const auth = initializeWebAuth({ - baseUrl: getAuthUrl(), - backendUrl: getBackendUrl(), // Enables automatic token refresh on 401 responses + baseUrl: authUrl, + backendUrl: backendUrl, // Enables automatic token refresh on 401 responses }); _authService = auth.authService; _tokenManager = auth.tokenManager; @@ -46,10 +26,10 @@ function getAuthService() { return _authService; } -function getTokenManager() { +async function getTokenManager() { if (!browser) return null; // Ensure auth service is initialized first - getAuthService(); + await getAuthService(); return _tokenManager; } @@ -79,7 +59,7 @@ export const authStore = { async initialize() { if (initialized) return; - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { initialized = true; loading = false; @@ -106,7 +86,7 @@ export const authStore = { * Sign in with email and password */ async signIn(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -133,7 +113,7 @@ export const authStore = { * Sign up with email and password */ async signUp(email: string, password: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server', needsVerification: false }; } @@ -163,7 +143,7 @@ export const authStore = { * Sign out */ async signOut() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { user = null; return; @@ -183,7 +163,7 @@ export const authStore = { * Send password reset email */ async resetPassword(email: string) { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return { success: false, error: 'Auth not available on server' }; } @@ -207,7 +187,7 @@ export const authStore = { * @deprecated Use getValidToken() instead for automatic refresh */ async getAccessToken() { - const authService = getAuthService(); + const authService = await getAuthService(); if (!authService) { return null; } @@ -219,7 +199,7 @@ export const authStore = { * Automatically refreshes if the token is expired or about to expire */ async getValidToken(): Promise { - const tokenManager = getTokenManager(); + const tokenManager = await getTokenManager(); if (!tokenManager) { return null; } diff --git a/apps/clock/apps/web/src/routes/(app)/+layout.svelte b/apps/clock/apps/web/src/routes/(app)/+layout.svelte index d58c91855..6dffac47f 100644 --- a/apps/clock/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/clock/apps/web/src/routes/(app)/+layout.svelte @@ -69,7 +69,8 @@ try { // Search alarms - const alarms = await alarmsApi.getAll(); + const alarmsResponse = await alarmsApi.getAll(); + const alarms = alarmsResponse.data || []; const matchingAlarms = alarms .filter((alarm) => alarm.label?.toLowerCase().includes(queryLower)) .slice(0, 5) @@ -81,7 +82,8 @@ results.push(...matchingAlarms); // Search timers - const timers = await timersApi.getAll(); + const timersResponse = await timersApi.getAll(); + const timers = timersResponse.data || []; const matchingTimers = timers .filter((timer) => timer.label?.toLowerCase().includes(queryLower)) .slice(0, 5) diff --git a/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte b/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte index 16352dabc..ea3b454f4 100644 --- a/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte +++ b/apps/clock/apps/web/src/routes/(app)/alarms/+page.svelte @@ -265,25 +265,25 @@ }} > -
- +
+ -
- +
+
- +
{$_('alarm.repeat')}
{#each dayNames as day, i}
{#if editingLabelId === focused.id} + stopwatchesStore.delete(focused.id)} + aria-label="Delete stopwatch" > {def.icon}

{def.label}

-

{def.description}

+

{def.emoji}

{#if theme.variant === variant} diff --git a/apps/clock/apps/web/src/routes/(app)/timers/+page.svelte b/apps/clock/apps/web/src/routes/(app)/timers/+page.svelte index 36b3d05a5..6c4284989 100644 --- a/apps/clock/apps/web/src/routes/(app)/timers/+page.svelte +++ b/apps/clock/apps/web/src/routes/(app)/timers/+page.svelte @@ -245,6 +245,7 @@ e.stopPropagation(); handleDelete(timer.id, isLocal); }} + aria-label="Delete timer" > removeCity(clock.id)} + aria-label="Remove city" >

{$_('worldClock.add')}

-