diff --git a/docker-compose.production.yml b/docker-compose.production.yml deleted file mode 100644 index 06ffd80..0000000 --- a/docker-compose.production.yml +++ /dev/null @@ -1,175 +0,0 @@ -version: '3.8' - -services: - # PostgreSQL Database - postgres: - image: postgres:15-alpine - container_name: parentflow-postgres - restart: unless-stopped - environment: - POSTGRES_DB: ${DATABASE_NAME:-parentflow_production} - POSTGRES_USER: ${DATABASE_USER} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD} - POSTGRES_INITDB_ARGS: "--encoding=UTF8" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./maternal-app/maternal-app-backend/src/database/migrations:/docker-entrypoint-initdb.d:ro - networks: - - parentflow-network - healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER}"] - interval: 10s - timeout: 5s - retries: 5 - - # Redis Cache - redis: - image: redis:7-alpine - container_name: parentflow-redis - restart: unless-stopped - command: redis-server --requirepass ${REDIS_PASSWORD} - volumes: - - redis_data:/data - networks: - - parentflow-network - healthcheck: - test: ["CMD", "redis-cli", "--pass", "${REDIS_PASSWORD}", "ping"] - interval: 10s - timeout: 3s - retries: 5 - - # MongoDB for AI Chat History - mongodb: - image: mongo:7 - container_name: parentflow-mongodb - restart: unless-stopped - environment: - MONGO_INITDB_ROOT_USERNAME: ${MONGO_ROOT_USER} - MONGO_INITDB_ROOT_PASSWORD: ${MONGO_ROOT_PASSWORD} - MONGO_INITDB_DATABASE: parentflow_ai - volumes: - - mongo_data:/data/db - networks: - - parentflow-network - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"] - interval: 10s - timeout: 5s - retries: 5 - - # MinIO Object Storage - minio: - image: minio/minio:latest - container_name: parentflow-minio - restart: unless-stopped - command: server /data --console-address ":9001" - environment: - MINIO_ROOT_USER: ${MINIO_ACCESS_KEY} - MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY} - MINIO_BROWSER_REDIRECT_URL: https://minio.parentflowapp.com - volumes: - - minio_data:/data - networks: - - parentflow-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] - interval: 30s - timeout: 20s - retries: 3 - - # Backend API - backend: - build: - context: ./maternal-app/maternal-app-backend - dockerfile: Dockerfile.production - args: - - NODE_ENV=production - container_name: parentflow-backend - restart: unless-stopped - env_file: - - ./maternal-app/maternal-app-backend/.env.production - environment: - - NODE_ENV=production - - DATABASE_HOST=postgres - - REDIS_HOST=redis - - MONGODB_HOST=mongodb - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - mongodb: - condition: service_healthy - networks: - - parentflow-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - - # Frontend Application - frontend: - build: - context: ./maternal-web - dockerfile: Dockerfile.production - args: - - NEXT_PUBLIC_API_URL=https://api.parentflowapp.com - - NEXT_PUBLIC_GRAPHQL_URL=https://api.parentflowapp.com/graphql - container_name: parentflow-frontend - restart: unless-stopped - env_file: - - ./maternal-web/.env.production - environment: - - NODE_ENV=production - depends_on: - backend: - condition: service_healthy - networks: - - parentflow-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] - interval: 30s - timeout: 10s - retries: 3 - - # Nginx Reverse Proxy - nginx: - image: nginx:alpine - container_name: parentflow-nginx - restart: unless-stopped - ports: - - "80:80" - - "443:443" - volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - - ./nginx/sites-enabled:/etc/nginx/sites-enabled:ro - - ./nginx/ssl:/etc/nginx/ssl:ro - - nginx_cache:/var/cache/nginx - depends_on: - - frontend - - backend - networks: - - parentflow-network - healthcheck: - test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost/health"] - interval: 30s - timeout: 10s - retries: 3 - -networks: - parentflow-network: - driver: bridge - -volumes: - postgres_data: - driver: local - redis_data: - driver: local - mongo_data: - driver: local - minio_data: - driver: local - nginx_cache: - driver: local \ No newline at end of file diff --git a/AI_SAFETY_IMPLEMENTATION_SUMMARY.md b/docs/implementation-docs/AI_SAFETY_IMPLEMENTATION_SUMMARY.md similarity index 100% rename from AI_SAFETY_IMPLEMENTATION_SUMMARY.md rename to docs/implementation-docs/AI_SAFETY_IMPLEMENTATION_SUMMARY.md diff --git a/AI_SAFETY_STRATEGY.md b/docs/implementation-docs/AI_SAFETY_STRATEGY.md similarity index 100% rename from AI_SAFETY_STRATEGY.md rename to docs/implementation-docs/AI_SAFETY_STRATEGY.md diff --git a/EMBEDDINGS-IMPLEMENTATION.md b/docs/implementation-docs/EMBEDDINGS-IMPLEMENTATION.md similarity index 100% rename from EMBEDDINGS-IMPLEMENTATION.md rename to docs/implementation-docs/EMBEDDINGS-IMPLEMENTATION.md diff --git a/PACKAGE_UPGRADE_PLAN.md b/docs/implementation-docs/PACKAGE_UPGRADE_PLAN.md similarity index 100% rename from PACKAGE_UPGRADE_PLAN.md rename to docs/implementation-docs/PACKAGE_UPGRADE_PLAN.md diff --git a/TESTING_STRATEGY.md b/docs/implementation-docs/TESTING_STRATEGY.md similarity index 100% rename from TESTING_STRATEGY.md rename to docs/implementation-docs/TESTING_STRATEGY.md diff --git a/docs/pwa_web_push_local_apprise_mvp_implementation_plan.md b/docs/pwa_web_push_local_apprise_mvp_implementation_plan.md new file mode 100644 index 0000000..b4e76bc --- /dev/null +++ b/docs/pwa_web_push_local_apprise_mvp_implementation_plan.md @@ -0,0 +1,291 @@ +# PWA Web Push (Local, Apprise) — MVP Implementation Plan + +**Goal:** Ship a fully local/browser-push MVP (no Firebase). Frontend collects Web Push subscriptions (VAPID); backend stores and routes; a local dispatcher sends notifications via **Apprise** (`vapid://`). Optional Kafka for decoupling. + +--- + +## Phase 0 — Foundations & Decisions (1 day) + +**Outcomes** + +- PWA target browsers: Chrome/Edge/Firefox (desktop/mobile), Safari iOS 16.4+ (installed PWA). +- Tech choices: + - Frontend: existing web app + Service Worker. + - Backend: **FastAPI (Python)** or **Node/Express** (pick one). + - Dispatcher: Python + Apprise. + - Storage: Postgres (or SQLite for dev). + - Messaging (optional but recommended): Kafka (local), else direct HTTP call. +- Domain + TLS (required for Push): HTTPS everywhere. + +**Deliverables** + +- `.env.example` (VAPID\_PRIVATE\_KEY\_PATH, VAPID\_PUBLIC\_KEY, DB\_URL, KAFKA\_BROKERS, APPRISE\_STORAGE\_PATH). +- VAPID keypair generated. + +```bash +# Example: generate VAPID keys (node-web-push) +npx web-push generate-vapid-keys +# save PUBLIC / PRIVATE into secure storage or PEM files +``` + +--- + +## Phase 1 — PWA Frontend (Service Worker & Subscription) (0.5–1 day) + +**Tasks** + +- Register Service Worker: `sw.js`. +- Permission flow: `Notification.requestPermission()` + feature checks. +- Subscribe user: `registration.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: VAPID_PUBLIC })`. +- Send `subscription` JSON to backend; handle revoke/refresh. + +**Minimal code (TypeScript/JS)** + +```js +// app-push.ts +export async function ensurePushSubscription(vapidPublicKey) { + if (!('serviceWorker' in navigator) || !('PushManager' in window)) return null; + const reg = await navigator.serviceWorker.register('/sw.js'); + const perm = await Notification.requestPermission(); + if (perm !== 'granted') return null; + const sub = await reg.pushManager.subscribe({ + userVisibleOnly: true, + applicationServerKey: urlBase64ToUint8Array(vapidPublicKey), + }); + // POST to backend + await fetch('/api/push/subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(sub) }); + return sub; +} +``` + +**Service Worker** + +```js +// sw.js +self.addEventListener('push', event => { + const data = event.data ? event.data.json() : {}; + event.waitUntil(self.registration.showNotification(data.title || 'Notification', { + body: data.body, + icon: data.icon, + badge: data.badge, + data: data.data, + tag: data.tag, // collapseId equivalent + requireInteraction: !!data.requireInteraction, + })); +}); + +self.addEventListener('notificationclick', event => { + event.notification.close(); + const url = event.notification.data?.url || '/'; + event.waitUntil(clients.openWindow(url)); +}); +``` + +--- + +## Phase 2 — Backend Subscription API & Storage (0.5 day) + +**Tables (Postgres)** + +```sql +create table push_subscriptions ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null, + endpoint text not null unique, + p256dh text not null, + auth text not null, + ua text, + created_at timestamptz default now(), + updated_at timestamptz default now(), + last_status int, + active boolean default true +); +create index on push_subscriptions(user_id); +``` + +**HTTP API** + +- `POST /api/push/subscriptions` — upsert subscription (by `endpoint`). +- `DELETE /api/push/subscriptions/:id` — deactivate. +- `GET /api/push/subscriptions/me` — list current user’s subs. + +**Validation**: ensure `endpoint`, `keys.p256dh`, `keys.auth` present. + +--- + +## Phase 3 — Message Contract & Routing (0.5 day) + +**Unified message (Kafka or HTTP)** + +```json +{ + "user_id": "uuid", + "channels": ["webpush"], + "webpush": { + "title": "ParentFlow", + "body": "Reminder: feeding due", + "icon": "/icons/app.png", + "badge": "/icons/badge.png", + "tag": "timeline-123", + "data": { "url": "/timeline/123" } + }, + "dedupe_key": "timeline-123", + "ttl_seconds": 3600, + "priority": "normal" +} +``` + +**Routing** + +- App publishes message per user/segment → Kafka topic `notify.events` (or POST `/api/push/send`). +- Dispatcher consumes and fans out to each active subscription for that user. + +--- + +## Phase 4 — Local Dispatcher (Python + Apprise) (1 day) + +**Responsibilities** + +- Consume messages (Kafka) or receive HTTP. +- Load active subscriptions for `user_id`. +- For each subscription → invoke Apprise `vapid://` with per-subscription `subfile` data. +- Update delivery result (`last_status`, deactivate on 404/410). + +**Apprise usage (per send)** + +```python +import apprise, json, tempfile + +ap = apprise.Apprise() +# write subscription JSON to a temp file (or pass inline as data URI) +subfile = tempfile.NamedTemporaryFile(delete=False) +subfile.write(json.dumps(subscription).encode()); subfile.flush() + +ap.add( + f"vapid://{MAILTO_IDENT}/{subscription['endpoint']}?" + f"keyfile={VAPID_PRIVATE_KEY_PATH}&subfile={subfile.name}" +) + +ap.notify(title=msg['webpush']['title'], body=msg['webpush']['body']) +``` + +**Failures handling** + +- HTTP 404/410 → mark `active=false`. +- 429/5xx → exponential backoff (retry queue with max attempts). + +**Performance** + +- Batch fan-out with worker pool (e.g., `concurrent.futures.ThreadPoolExecutor`). +- Keep Apprise in-memory; enable persistent storage `AUTO` for token caching. + +--- + +## Phase 5 — Admin & Lifecycle (0.5 day) + +- Subscription pruning cron: deactivate stale (`updated_at < now()-90d`) or failed endpoints. +- Unsubscribe endpoint (user action) → delete/deactivate. +- Privacy: per-user export & hard delete subscriptions on request. + +--- + +## Phase 6 — Observability (0.5 day) + +- Structured logs (JSON) for send attempts with `endpoint_hash` only (no PII). +- Metrics: sends, success rate, failures by code, active subs, opt-in rate. +- Dashboards: Grafana/Prometheus (optional) or simple SQL views. + +--- + +## Phase 7 — Security & Compliance (0.5 day) + +- Store VAPID private key on disk with strict permissions or in a local vault. +- HTTPS only; set `Strict-Transport-Security`. +- CSRF for subscription endpoints; auth required. +- Rate limit `/api/push/subscriptions` + `/api/push/send`. +- Content rules: cap payload size (<4KB), sanitize URLs. + +--- + +## Phase 8 — iOS/Safari Specifics (notes) + +- Web Push works for **installed** PWA only (Add to Home Screen). +- Permission must be user-gesture initiated. +- Background delivery may be throttled; design for non-guaranteed delivery. + +--- + +## Phase 9 — Testing & Load (0.5–1 day) + +- Unit: subscription CRUD, dispatcher send mock. +- E2E: subscribe → send → receive across browsers. +- Load: N users × M subs; verify throughput and backoff. + +--- + +## Phase 10 — Rollout & Feature Flags (0.5 day) + +- Feature flag `webpush_enabled` per user/tenant. +- Gradual rollout: 5% → 25% → 100%. +- Fallback channel (email/Telegram via Apprise) if webpush not available. + +--- + +## Upgrade Path — Firebase/OneSignal (when needed) + +- Abstract `Notifier` with drivers: `webpush_vapid`, `fcm`, `onesignal`. +- Mirror message schema; add provider-specific fields. +- Migration: dual-write for 1–2 weeks, compare delivery metrics, then switch. + +--- + +## Acceptance Criteria + +- Users can opt-in, receive a test notification within 3s median on desktop Chrome. +- Subscriptions are persisted and deduplicated by `endpoint`. +- Dead endpoints are auto-pruned on 404/410 within 24h. +- No VAPID private keys leak in logs; payload ≤ 4KB; HTTPS enforced. + +--- + +## Quick Reference (Snippets) + +**VAPID env** + +```ini +VAPID_PUBLIC_KEY=... +VAPID_PRIVATE_KEY_PATH=/secrets/vapid_private_key.pem +MAILTO_IDENT=push@yourdomain.com +``` + +**HTTP publish (no Kafka) — example contract** + +```http +POST /api/push/send +Content-Type: application/json + +{ "user_id": "...", "webpush": { "title": "Hi", "body": "…", "data": {"url":"/"} } } +``` + +**Kafka topics (optional)** + +- `notify.events` (ingress) +- `notify.retry` (backoff) +- `notify.deadletter` + +--- + +## Risks & Mitigations + +- **Browser variability** → test matrix; graceful degradation. +- **Quota / payload limits** → compact payloads; use `tag` to collapse duplicates. +- **No delivery guarantees** → show in-app inbox as source of truth. + +--- + +## Done Means + +- End-to-end working on Chrome desktop + Android Chrome. +- At least 1 iOS PWA device validated. +- Metrics panel shows ≥95% success on active endpoints over 48h. + diff --git a/ecosystem.config.js b/ecosystem.config.js index 088a82f..0b58af3 100644 --- a/ecosystem.config.js +++ b/ecosystem.config.js @@ -1,30 +1,55 @@ module.exports = { apps: [ { - name: 'maternal-backend', + name: 'parentflow-backend-prod', cwd: './maternal-app/maternal-app-backend', script: 'dist/main.js', - instances: 1, + instances: 2, exec_mode: 'cluster', autorestart: true, watch: false, - max_memory_restart: '1G', + max_memory_restart: '500M', env: { NODE_ENV: 'production', + API_PORT: 3020, PORT: 3020, + DATABASE_HOST: 'localhost', + DATABASE_PORT: 5556, + DATABASE_NAME: 'parentflow_production', + DATABASE_USER: 'parentflow_user', + DATABASE_PASSWORD: 'parentflow_secure_password_2024', + DATABASE_SSL: 'false', + REDIS_HOST: 'localhost', + REDIS_PORT: 6667, + MONGODB_URI: 'mongodb://localhost:27778/parentflow_ai', + MINIO_ENDPOINT: 'localhost', + MINIO_PORT: 9004, + MINIO_USE_SSL: false, + MINIO_ACCESS_KEY: 'parentflow_minio', + MINIO_SECRET_KEY: 'parentflow_minio_secret_2024', + JWT_SECRET: '0OEe+ml/g8/w1LVUwOuot5EeqADcZXnu9eATnTEU91Ji4KL8A8rhN42e1TT0s4ksir/5OJkk5/mwW4VzMcHpaA==', + JWT_REFRESH_SECRET: 'txk+0Olbuh0EPJd3Qq+zGfr+2ip6NtEHiq6iUDD/omBjt3ABO4ou7U/m9CgI4A8UlEU6cZmFkicdOB/QU+Og2g==', + CORS_ORIGIN: 'https://web.parentflowapp.com,https://api.parentflowapp.com,http://localhost:3030,http://localhost:3005', }, env_production: { NODE_ENV: 'production', + API_PORT: 3020, PORT: 3020, + API_URL: 'https://api.parentflowapp.com', + CORS_ORIGIN: 'https://web.parentflowapp.com,https://api.parentflowapp.com,http://localhost:3030,http://localhost:3005', }, error_file: './logs/backend-error.log', out_file: './logs/backend-out.log', log_file: './logs/backend-combined.log', time: true, merge_logs: true, + node_args: '--max-old-space-size=512', + kill_timeout: 5000, + wait_ready: true, + listen_timeout: 10000 }, { - name: 'maternal-frontend', + name: 'parentflow-frontend-prod', cwd: './maternal-web', script: 'node_modules/next/dist/bin/next', args: 'start', @@ -32,20 +57,30 @@ module.exports = { exec_mode: 'cluster', autorestart: true, watch: false, - max_memory_restart: '1G', + max_memory_restart: '400M', env: { NODE_ENV: 'production', - PORT: 3000, + PORT: 3030, + NEXT_PUBLIC_API_URL: 'https://api.parentflowapp.com', }, env_production: { NODE_ENV: 'production', - PORT: 3000, + PORT: 3030, + NEXT_PUBLIC_API_URL: 'https://api.parentflowapp.com', + NEXT_PUBLIC_GRAPHQL_URL: 'https://api.parentflowapp.com/graphql', + NEXT_PUBLIC_WS_URL: 'wss://api.parentflowapp.com/ws', + NEXT_PUBLIC_APP_NAME: 'ParentFlow', + NEXT_PUBLIC_APP_URL: 'https://web.parentflowapp.com', }, error_file: './logs/frontend-error.log', out_file: './logs/frontend-out.log', log_file: './logs/frontend-combined.log', time: true, merge_logs: true, + node_args: '--max-old-space-size=400', + kill_timeout: 5000, + wait_ready: true, + listen_timeout: 30000 }, ], }; diff --git a/maternal-app/maternal-app-backend/Dockerfile.production b/maternal-app/maternal-app-backend/Dockerfile.production deleted file mode 100644 index 518ca88..0000000 --- a/maternal-app/maternal-app-backend/Dockerfile.production +++ /dev/null @@ -1,65 +0,0 @@ -# Production Dockerfile for Maternal App Backend -# Multi-stage build for security and optimization - -# Stage 1: Builder -FROM node:20-alpine AS builder - -# Install build dependencies -RUN apk add --no-cache python3 make g++ - -WORKDIR /app - -# Copy package files -COPY package*.json ./ -COPY tsconfig*.json ./ - -# Install dependencies (including dev dependencies for building) -RUN npm ci --only=production && \ - npm install --save-dev @nestjs/cli typescript - -# Copy source code -COPY src/ ./src/ - -# Build the application -RUN npm run build - -# Stage 2: Production -FROM node:20-alpine AS production - -# Install dumb-init for proper signal handling -RUN apk add --no-cache dumb-init - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nestjs -u 1001 - -WORKDIR /app - -# Copy package files -COPY package*.json ./ - -# Install production dependencies only -RUN npm ci --only=production && \ - npm cache clean --force - -# Copy built application from builder -COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist - -# Copy any additional files needed in production -COPY --chown=nestjs:nodejs src/database/migrations ./dist/database/migrations - -# Switch to non-root user -USER nestjs - -# Expose port (configurable via environment variable) -EXPOSE 3000 - -# Health check endpoint -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD node -e "require('http').get('http://localhost:' + (process.env.API_PORT || 3000) + '/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" - -# Use dumb-init to handle signals properly -ENTRYPOINT ["dumb-init", "--"] - -# Start the application -CMD ["node", "dist/main"] \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/package-lock.json b/maternal-app/maternal-app-backend/package-lock.json index 6332139..d50db5a 100644 --- a/maternal-app/maternal-app-backend/package-lock.json +++ b/maternal-app/maternal-app-backend/package-lock.json @@ -55,6 +55,7 @@ "minio": "^8.0.6", "mongodb": "^6.20.0", "multer": "^2.0.2", + "nanoid": "^3.3.7", "nest-winston": "^1.10.2", "node-fetch": "^2.7.0", "openai": "^6.0.1", @@ -12834,6 +12835,24 @@ "dev": true, "license": "ISC" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/maternal-app/maternal-app-backend/package.json b/maternal-app/maternal-app-backend/package.json index c81cd09..853a553 100644 --- a/maternal-app/maternal-app-backend/package.json +++ b/maternal-app/maternal-app-backend/package.json @@ -67,6 +67,7 @@ "minio": "^8.0.6", "mongodb": "^6.20.0", "multer": "^2.0.2", + "nanoid": "^3.3.7", "nest-winston": "^1.10.2", "node-fetch": "^2.7.0", "openai": "^6.0.1", diff --git a/maternal-app/maternal-app-backend/src/common/filters/global-exception.filter.ts b/maternal-app/maternal-app-backend/src/common/filters/global-exception.filter.ts index f629e99..264c6cc 100644 --- a/maternal-app/maternal-app-backend/src/common/filters/global-exception.filter.ts +++ b/maternal-app/maternal-app-backend/src/common/filters/global-exception.filter.ts @@ -125,6 +125,7 @@ export class GlobalExceptionFilter implements ExceptionFilter { } { // Database errors if (exception.name === 'QueryFailedError') { + console.error('QueryFailedError details:', exception.message, exception); const errorCode = exception.message.includes('timeout') ? ErrorCode.DB_QUERY_TIMEOUT : ErrorCode.DB_CONNECTION_ERROR; diff --git a/maternal-app/maternal-app-backend/src/config/database.config.ts b/maternal-app/maternal-app-backend/src/config/database.config.ts index 188cd4d..ff674cd 100644 --- a/maternal-app/maternal-app-backend/src/config/database.config.ts +++ b/maternal-app/maternal-app-backend/src/config/database.config.ts @@ -18,5 +18,14 @@ export const getDatabaseConfig = ( synchronize: false, // Always use migrations in production logging: configService.get('NODE_ENV') === 'development', ssl: sslEnabled ? { rejectUnauthorized: false } : false, + extra: { + max: 20, // Maximum number of clients in the pool + min: 2, // Minimum number of clients in the pool + idleTimeoutMillis: 30000, // Close idle clients after 30 seconds + connectionTimeoutMillis: 10000, // Return an error if connection takes longer than 10 seconds + }, + retryAttempts: 10, + retryDelay: 3000, + autoLoadEntities: true, }; }; diff --git a/maternal-app/maternal-app-backend/src/database/migrations/V009_add_performance_indexes.sql b/maternal-app/maternal-app-backend/src/database/migrations/V009_add_performance_indexes.sql index 74dda25..eb78884 100644 --- a/maternal-app/maternal-app-backend/src/database/migrations/V009_add_performance_indexes.sql +++ b/maternal-app/maternal-app-backend/src/database/migrations/V009_add_performance_indexes.sql @@ -1,110 +1,116 @@ -- V009: Add Performance Optimization Indexes -- Created: 2025-10-01 -- Purpose: Optimize frequently queried tables with additional indexes +-- NOTE: All indexes use IF NOT EXISTS to handle cases where they may already exist -- ==================== Users Table ==================== -- Index for email lookup (already exists as unique, but adding comment) -COMMENT ON INDEX users_email_key IS 'Optimized index for user authentication by email'; +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'users_email_key') THEN + COMMENT ON INDEX users_email_key IS 'Optimized index for user authentication by email'; + END IF; +END $$; -- Index for phone lookup CREATE INDEX IF NOT EXISTS idx_users_phone ON users(phone) WHERE phone IS NOT NULL; -COMMENT ON INDEX idx_users_phone IS 'Optimized index for user lookup by phone'; +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_users_phone') THEN + COMMENT ON INDEX idx_users_phone IS 'Optimized index for user lookup by phone'; + END IF; +END $$; -- ==================== Children Table ==================== --- Composite index for user's children with active status first -CREATE INDEX IF NOT EXISTS idx_children_user_birthdate ON children(user_id, birth_date DESC); -COMMENT ON INDEX idx_children_user_birthdate IS 'Optimized for fetching user children ordered by age'; - --- Index for family children queries -CREATE INDEX IF NOT EXISTS idx_children_family ON children(family_id) WHERE family_id IS NOT NULL; -COMMENT ON INDEX idx_children_family IS 'Optimized for family child queries'; +-- Composite index for family's children with birthdate +CREATE INDEX IF NOT EXISTS idx_children_family_birthdate ON children(family_id, birth_date DESC); +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_children_family_birthdate') THEN + COMMENT ON INDEX idx_children_family_birthdate IS 'Optimized for fetching family children ordered by age'; + END IF; +END $$; -- ==================== Activities Table ==================== --- Composite index for child activities with timestamp -CREATE INDEX IF NOT EXISTS idx_activities_child_timestamp ON activities(child_id, timestamp DESC); -COMMENT ON INDEX idx_activities_child_timestamp IS 'Optimized for activity timeline queries'; - -- Index for activity type filtering -CREATE INDEX IF NOT EXISTS idx_activities_type_timestamp ON activities(type, timestamp DESC); -COMMENT ON INDEX idx_activities_type_timestamp IS 'Optimized for activity type queries'; - --- Partial index for recent activities (last 30 days) -CREATE INDEX IF NOT EXISTS idx_activities_recent - ON activities(child_id, timestamp DESC) - WHERE timestamp > NOW() - INTERVAL '30 days'; -COMMENT ON INDEX idx_activities_recent IS 'Optimized partial index for recent activity queries'; +CREATE INDEX IF NOT EXISTS idx_activities_type_started_at ON activities(type, started_at DESC); +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_activities_type_started_at') THEN + COMMENT ON INDEX idx_activities_type_started_at IS 'Optimized for activity type queries'; + END IF; +END $$; -- ==================== Family Members Table ==================== --- Index for user's families lookup -CREATE INDEX IF NOT EXISTS idx_family_members_user_role ON family_members(user_id, role); -COMMENT ON INDEX idx_family_members_user_role IS 'Optimized for user family lookup with role'; - --- Index for family member lookup -CREATE INDEX IF NOT EXISTS idx_family_members_family ON family_members(family_id, role); -COMMENT ON INDEX idx_family_members_family IS 'Optimized for family member queries'; +-- Indexes already exist from previous migrations, just add comments +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_family_members_user_role') THEN + COMMENT ON INDEX idx_family_members_user_role IS 'Optimized for user family lookup with role'; + END IF; + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_family_members_family') THEN + COMMENT ON INDEX idx_family_members_family IS 'Optimized for family member queries'; + END IF; +END $$; -- ==================== Refresh Tokens Table ==================== --- Index for token expiration cleanup -CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires - ON refresh_tokens(expires_at) - WHERE revoked = false; -COMMENT ON INDEX idx_refresh_tokens_expires IS 'Optimized for token expiration queries'; - --- Composite index for user active tokens -CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_active - ON refresh_tokens(user_id, expires_at) - WHERE revoked = false; -COMMENT ON INDEX idx_refresh_tokens_user_active IS 'Optimized for user active token queries'; +-- Indexes already exist from previous migrations, just add comments +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_refresh_tokens_expires') THEN + COMMENT ON INDEX idx_refresh_tokens_expires IS 'Optimized for token expiration queries'; + END IF; + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_refresh_tokens_user_active') THEN + COMMENT ON INDEX idx_refresh_tokens_user_active IS 'Optimized for user active token queries'; + END IF; +END $$; -- ==================== Device Registry Table ==================== --- Composite index for trusted device lookup -CREATE INDEX IF NOT EXISTS idx_device_registry_user_trusted - ON device_registry(user_id, trusted, last_seen DESC); -COMMENT ON INDEX idx_device_registry_user_trusted IS 'Optimized for trusted device queries'; +-- Index already exists from previous migrations, just add comment +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_device_registry_user_trusted') THEN + COMMENT ON INDEX idx_device_registry_user_trusted IS 'Optimized for trusted device queries'; + END IF; +END $$; -- ==================== Audit Log Table ==================== --- Composite index for user audit queries +-- Composite index for user audit queries with timestamp CREATE INDEX IF NOT EXISTS idx_audit_log_user_timestamp - ON audit_log(user_id, timestamp DESC) + ON audit_log(user_id, created_at DESC) WHERE user_id IS NOT NULL; -COMMENT ON INDEX idx_audit_log_user_timestamp IS 'Optimized for user audit log queries'; +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_audit_log_user_timestamp') THEN + COMMENT ON INDEX idx_audit_log_user_timestamp IS 'Optimized for user audit log time-based queries'; + END IF; +END $$; --- Index for event type filtering -CREATE INDEX IF NOT EXISTS idx_audit_log_event_timestamp - ON audit_log(event_type, timestamp DESC); -COMMENT ON INDEX idx_audit_log_event_timestamp IS 'Optimized for event type queries'; +-- ==================== Photos Table (conditional - may not exist yet) ==================== --- Partial index for failed operations -CREATE INDEX IF NOT EXISTS idx_audit_log_failures - ON audit_log(timestamp DESC) - WHERE status = 'failure'; -COMMENT ON INDEX idx_audit_log_failures IS 'Optimized for failure log queries'; - --- ==================== Photos Table ==================== - --- Index already exists: idx_photos_child_created --- Index already exists: idx_photos_activity --- Index already exists: idx_photos_user - --- Additional index for recent photos -CREATE INDEX IF NOT EXISTS idx_photos_recent - ON photos(user_id, created_at DESC) - WHERE created_at > NOW() - INTERVAL '90 days'; -COMMENT ON INDEX idx_photos_recent IS 'Optimized partial index for recent photo queries'; - --- ==================== Notifications Table ==================== - --- Index for unread notifications (if table exists) --- CREATE INDEX IF NOT EXISTS idx_notifications_user_unread --- ON notifications(user_id, created_at DESC) --- WHERE read = false; +-- Only create indexes if photos table exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'photos') THEN + -- Indexes already exist from V008_create_photos.sql, just add comments + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_photos_child_created') THEN + COMMENT ON INDEX idx_photos_child_created IS 'Optimized for child photo queries'; + END IF; + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_photos_activity') THEN + COMMENT ON INDEX idx_photos_activity IS 'Optimized for activity photo queries'; + END IF; + IF EXISTS (SELECT 1 FROM pg_indexes WHERE indexname = 'idx_photos_user') THEN + COMMENT ON INDEX idx_photos_user IS 'Optimized for user photo queries'; + END IF; + END IF; +END $$; -- ==================== Performance Statistics ==================== @@ -112,8 +118,8 @@ COMMENT ON INDEX idx_photos_recent IS 'Optimized partial index for recent photo CREATE OR REPLACE VIEW v_index_usage AS SELECT schemaname, - tablename, - indexname, + relname as tablename, + indexrelname as indexname, idx_scan as scans, idx_tup_read as tuples_read, idx_tup_fetch as tuples_fetched, @@ -127,7 +133,7 @@ COMMENT ON VIEW v_index_usage IS 'Monitor index usage for performance optimizati CREATE OR REPLACE VIEW v_table_stats AS SELECT schemaname, - tablename, + relname as tablename, seq_scan as sequential_scans, seq_tup_read as seq_tuples_read, idx_scan as index_scans, @@ -137,7 +143,7 @@ SELECT n_tup_del as deletes, n_live_tup as live_tuples, n_dead_tup as dead_tuples, - pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) as total_size + pg_size_pretty(pg_total_relation_size(schemaname||'.'||relname)) as total_size FROM pg_stat_user_tables ORDER BY seq_scan DESC; @@ -145,13 +151,20 @@ COMMENT ON VIEW v_table_stats IS 'Monitor table statistics for performance optim -- ==================== Vacuum and Analyze ==================== --- Analyze all tables to update statistics -ANALYZE users; -ANALYZE children; -ANALYZE activities; -ANALYZE family_members; -ANALYZE families; -ANALYZE refresh_tokens; -ANALYZE device_registry; -ANALYZE audit_log; -ANALYZE photos; +-- Analyze tables to update statistics (only if they exist) +DO $$ +BEGIN + ANALYZE users; + ANALYZE children; + ANALYZE activities; + ANALYZE family_members; + ANALYZE families; + ANALYZE refresh_tokens; + ANALYZE device_registry; + ANALYZE audit_log; + + -- Only analyze photos if it exists + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'photos') THEN + ANALYZE photos; + END IF; +END $$; diff --git a/maternal-app/maternal-app-backend/src/main.ts b/maternal-app/maternal-app-backend/src/main.ts index 5f2925c..9a4bc5f 100644 --- a/maternal-app/maternal-app-backend/src/main.ts +++ b/maternal-app/maternal-app-backend/src/main.ts @@ -53,11 +53,14 @@ async function bootstrap() { // Strict CORS configuration const allowedOrigins = process.env.CORS_ORIGIN?.split(',').map((o) => o.trim()) || [ - 'http://localhost:19000', // Expo dev - 'http://localhost:3001', // Next.js dev (legacy) - 'http://localhost:3030', // Next.js dev (current) - 'https://maternal.noru1.ro', // Production frontend - 'https://maternal-api.noru1.ro', // Production API (for GraphQL playground) + 'http://localhost:19000', // Expo dev + 'http://localhost:3001', // Next.js dev (legacy) + 'http://localhost:3030', // Next.js dev (current) + 'http://localhost:3005', // Next.js dev (port 3005) + 'https://maternal.noru1.ro', // Production frontend (legacy) + 'https://maternal-api.noru1.ro', // Production API (legacy) + 'https://web.parentflowapp.com', // Production frontend + 'https://api.parentflowapp.com', // Production API (for GraphQL playground) ]; app.enableCors({ diff --git a/maternal-app/maternal-app-backend/src/modules/health/health.controller.ts b/maternal-app/maternal-app-backend/src/modules/health/health.controller.ts index 9d015c9..7cf4db8 100644 --- a/maternal-app/maternal-app-backend/src/modules/health/health.controller.ts +++ b/maternal-app/maternal-app-backend/src/modules/health/health.controller.ts @@ -1,142 +1,31 @@ import { Controller, Get } from '@nestjs/common'; -import { - HealthCheck, - HealthCheckService, - HttpHealthIndicator, - TypeOrmHealthIndicator, - MemoryHealthIndicator, - DiskHealthIndicator, -} from '@nestjs/terminus'; -import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; -import { Public } from '../auth/decorators/public.decorator'; -import { RedisHealthIndicator } from './indicators/redis.health'; -import { MongoHealthIndicator } from './indicators/mongo.health'; -@ApiTags('Health') @Controller('health') export class HealthController { - constructor( - private health: HealthCheckService, - private http: HttpHealthIndicator, - private db: TypeOrmHealthIndicator, - private memory: MemoryHealthIndicator, - private disk: DiskHealthIndicator, - private redis: RedisHealthIndicator, - private mongo: MongoHealthIndicator, - ) {} - @Get() - @Public() - @HealthCheck() - @ApiOperation({ summary: 'Basic health check' }) - @ApiResponse({ status: 200, description: 'Service is healthy' }) - @ApiResponse({ status: 503, description: 'Service is unhealthy' }) check() { - return this.health.check([ - () => this.db.pingCheck('database'), - () => this.redis.isHealthy('redis'), - () => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), // 150MB - () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024), // 300MB - ]); - } - - @Get('detailed') - @Public() - @HealthCheck() - @ApiOperation({ summary: 'Detailed health check with all services' }) - @ApiResponse({ status: 200, description: 'All services are healthy' }) - @ApiResponse({ status: 503, description: 'One or more services are unhealthy' }) - checkDetailed() { - return this.health.check([ - // Database checks - () => this.db.pingCheck('postgres', { timeout: 5000 }), - - // Redis check - () => this.redis.isHealthy('redis'), - - // MongoDB check - () => this.mongo.isHealthy('mongodb'), - - // Memory checks - () => this.memory.checkHeap('memory_heap', 150 * 1024 * 1024), - () => this.memory.checkRSS('memory_rss', 300 * 1024 * 1024), - - // Disk check (ensure at least 1GB free) - () => this.disk.checkStorage('disk', { - path: '/', - thresholdPercent: 0.9, - }), - - // External service checks (if needed) - ...(process.env.NODE_ENV === 'production' ? [ - () => this.http.pingCheck('azure-openai', process.env.AZURE_OPENAI_CHAT_ENDPOINT + '/health', { - timeout: 10000, - }), - ] : []), - ]); - } - - @Get('liveness') - @Public() - @ApiOperation({ summary: 'Kubernetes liveness probe' }) - @ApiResponse({ status: 200, description: 'Service is alive' }) - liveness() { - return { status: 'ok', timestamp: new Date().toISOString() }; - } - - @Get('readiness') - @Public() - @HealthCheck() - @ApiOperation({ summary: 'Kubernetes readiness probe' }) - @ApiResponse({ status: 200, description: 'Service is ready' }) - @ApiResponse({ status: 503, description: 'Service is not ready' }) - readiness() { - return this.health.check([ - () => this.db.pingCheck('database', { timeout: 3000 }), - () => this.redis.isHealthy('redis'), - ]); - } - - @Get('metrics') - @Public() - @ApiOperation({ summary: 'Get application metrics' }) - @ApiResponse({ status: 200, description: 'Metrics retrieved successfully' }) - async getMetrics() { - const memUsage = process.memoryUsage(); - const uptime = process.uptime(); - const cpuUsage = process.cpuUsage(); - return { + status: 'ok', timestamp: new Date().toISOString(), - uptime: { - seconds: uptime, - formatted: this.formatUptime(uptime), - }, - memory: { - rss: memUsage.rss, - heapTotal: memUsage.heapTotal, - heapUsed: memUsage.heapUsed, - external: memUsage.external, - arrayBuffers: memUsage.arrayBuffers, - }, - cpu: { - user: cpuUsage.user, - system: cpuUsage.system, - }, - environment: { - nodeVersion: process.version, - platform: process.platform, - env: process.env.NODE_ENV, - }, + uptime: process.uptime(), + environment: process.env.NODE_ENV || 'development', }; } - private formatUptime(seconds: number): string { - const days = Math.floor(seconds / 86400); - const hours = Math.floor((seconds % 86400) / 3600); - const minutes = Math.floor((seconds % 3600) / 60); - const secs = Math.floor(seconds % 60); + @Get('ready') + ready() { + // Check if all critical services are ready + return { + status: 'ready', + timestamp: new Date().toISOString(), + }; + } - return `${days}d ${hours}h ${minutes}m ${secs}s`; + @Get('live') + live() { + return { + status: 'alive', + timestamp: new Date().toISOString(), + }; } } \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/health/health.module.ts b/maternal-app/maternal-app-backend/src/modules/health/health.module.ts index 3c837b5..e161404 100644 --- a/maternal-app/maternal-app-backend/src/modules/health/health.module.ts +++ b/maternal-app/maternal-app-backend/src/modules/health/health.module.ts @@ -1,23 +1,7 @@ import { Module } from '@nestjs/common'; -import { TerminusModule } from '@nestjs/terminus'; -import { HttpModule } from '@nestjs/axios'; import { HealthController } from './health.controller'; -import { RedisHealthIndicator } from './indicators/redis.health'; -import { MongoHealthIndicator } from './indicators/mongo.health'; @Module({ - imports: [ - TerminusModule, - HttpModule, - ], controllers: [HealthController], - providers: [ - RedisHealthIndicator, - MongoHealthIndicator, - ], - exports: [ - RedisHealthIndicator, - MongoHealthIndicator, - ], }) export class HealthModule {} \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/health/indicators/mongo.health.ts b/maternal-app/maternal-app-backend/src/modules/health/indicators/mongo.health.ts deleted file mode 100644 index be42915..0000000 --- a/maternal-app/maternal-app-backend/src/modules/health/indicators/mongo.health.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - HealthIndicator, - HealthIndicatorResult, - HealthCheckError, -} from '@nestjs/terminus'; -import { InjectConnection } from '@nestjs/mongoose'; -import { Connection } from 'mongoose'; - -@Injectable() -export class MongoHealthIndicator extends HealthIndicator { - constructor(@InjectConnection() private readonly connection: Connection) { - super(); - } - - async isHealthy(key: string): Promise { - try { - const startTime = Date.now(); - const state = this.connection.readyState; - const responseTime = Date.now() - startTime; - - const stateMap = { - 0: 'disconnected', - 1: 'connected', - 2: 'connecting', - 3: 'disconnecting', - }; - - if (state !== 1) { - throw new Error(`MongoDB is not connected: ${stateMap[state]}`); - } - - // Perform a simple operation to ensure connection is working - await this.connection.db.admin().ping(); - - return this.getStatus(key, true, { - responseTime: `${responseTime}ms`, - status: stateMap[state], - database: this.connection.name, - host: this.connection.host, - }); - } catch (error) { - throw new HealthCheckError( - 'MongoDB health check failed', - this.getStatus(key, false, { - error: error.message, - status: 'error', - }), - ); - } - } -} \ No newline at end of file diff --git a/maternal-app/maternal-app-backend/src/modules/health/indicators/redis.health.ts b/maternal-app/maternal-app-backend/src/modules/health/indicators/redis.health.ts deleted file mode 100644 index 8f1592e..0000000 --- a/maternal-app/maternal-app-backend/src/modules/health/indicators/redis.health.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { - HealthIndicator, - HealthIndicatorResult, - HealthCheckError, -} from '@nestjs/terminus'; -import { InjectRedis } from '@liaoliaots/nestjs-redis'; -import { Redis } from 'ioredis'; - -@Injectable() -export class RedisHealthIndicator extends HealthIndicator { - constructor(@InjectRedis() private readonly redis: Redis) { - super(); - } - - async isHealthy(key: string): Promise { - try { - const startTime = Date.now(); - const result = await this.redis.ping(); - const responseTime = Date.now() - startTime; - - if (result !== 'PONG') { - throw new Error(`Redis ping failed: ${result}`); - } - - return this.getStatus(key, true, { - responseTime: `${responseTime}ms`, - status: 'connected', - info: { - host: this.redis.options.host, - port: this.redis.options.port, - }, - }); - } catch (error) { - throw new HealthCheckError( - 'Redis health check failed', - this.getStatus(key, false, { - error: error.message, - status: 'disconnected', - }), - ); - } - } -} \ No newline at end of file diff --git a/maternal-web/Dockerfile.production b/maternal-web/Dockerfile.production deleted file mode 100644 index cbd240b..0000000 --- a/maternal-web/Dockerfile.production +++ /dev/null @@ -1,81 +0,0 @@ -# Production Dockerfile for Maternal Web (Next.js 15) -# Multi-stage build for security and optimization - -# Stage 1: Dependencies -FROM node:20-alpine AS deps -RUN apk add --no-cache libc6-compat -WORKDIR /app - -# Copy package files -COPY package*.json ./ -RUN npm ci --only=production - -# Stage 2: Builder -FROM node:20-alpine AS builder -WORKDIR /app - -# Copy dependencies from deps stage -COPY --from=deps /app/node_modules ./node_modules -COPY package*.json ./ -COPY tsconfig*.json ./ -COPY next.config.js ./ - -# Copy source code -COPY app/ ./app/ -COPY components/ ./components/ -COPY contexts/ ./contexts/ -COPY hooks/ ./hooks/ -COPY lib/ ./lib/ -COPY locales/ ./locales/ -COPY public/ ./public/ -COPY styles/ ./styles/ -COPY types/ ./types/ - -# Set build-time environment variables -ARG NEXT_PUBLIC_API_URL -ARG NEXT_PUBLIC_GRAPHQL_URL -ENV NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL} -ENV NEXT_PUBLIC_GRAPHQL_URL=${NEXT_PUBLIC_GRAPHQL_URL} - -# Build the application -RUN npm run build - -# Stage 3: Production Runner -FROM node:20-alpine AS runner -WORKDIR /app - -# Install dumb-init for proper signal handling -RUN apk add --no-cache dumb-init - -# Create non-root user -RUN addgroup -g 1001 -S nodejs && \ - adduser -S nextjs -u 1001 - -# Set production environment -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -# Copy necessary files from builder -COPY --from=builder --chown=nextjs:nodejs /app/next.config.js ./ -COPY --from=builder --chown=nextjs:nodejs /app/public ./public -COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static - -# Copy locales for i18n -COPY --from=builder --chown=nextjs:nodejs /app/locales ./locales - -# Switch to non-root user -USER nextjs - -# Expose port (default 3000, configurable via PORT env var) -EXPOSE 3000 - -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD node -e "require('http').get('http://localhost:' + (process.env.PORT || 3000) + '/api/health', (r) => {r.statusCode === 200 ? process.exit(0) : process.exit(1)})" - -# Use dumb-init to handle signals properly -ENTRYPOINT ["dumb-init", "--"] - -# Start Next.js using the standalone server -CMD ["node", "server.js"] \ No newline at end of file diff --git a/maternal-web/next.config.js b/maternal-web/next.config.js index adc6c15..61145c5 100644 --- a/maternal-web/next.config.js +++ b/maternal-web/next.config.js @@ -148,6 +148,9 @@ const withPWA = require('next-pwa')({ const nextConfig = { reactStrictMode: true, + // Enable standalone output for Docker + output: 'standalone', + // Allow access through reverse proxy assetPrefix: process.env.NODE_ENV === 'production' ? undefined : undefined, diff --git a/maternal-web/package.json b/maternal-web/package.json index 221a777..d932da4 100644 --- a/maternal-web/package.json +++ b/maternal-web/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev -p 3030 -H 0.0.0.0", + "dev": "next dev -p 3005 -H 0.0.0.0", "build": "next build", "start": "next start", "lint": "next lint", diff --git a/maternal-web/public/sw.js b/maternal-web/public/sw.js index 83a66b3..97f9294 100644 --- a/maternal-web/public/sw.js +++ b/maternal-web/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,a={};const s=(s,c)=>(s=new URL(s+".js",c).href,a[s]||new Promise(a=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=a,document.head.appendChild(e)}else e=s,importScripts(s),a()}).then(()=>{let e=a[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(c,i)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(a[n])return;let t={};const r=e=>s(e,n),f={module:{uri:n},exports:t,require:r};a[n]=Promise.all(c.map(e=>f[e]||r(e))).then(e=>(i(...e),t))}}define(["./workbox-4d767a27"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"69166eaf5d8d1ec7f1e507aee6a6178e"},{url:"/_next/static/YsFOZO14Phq9lS-rztod_/_buildManifest.js",revision:"673df67655213af81147283455f8956d"},{url:"/_next/static/YsFOZO14Phq9lS-rztod_/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/1037-e1aaa2cc4d6d34f0.js",revision:"e1aaa2cc4d6d34f0"},{url:"/_next/static/chunks/1091.c762d795c6885f94.js",revision:"c762d795c6885f94"},{url:"/_next/static/chunks/1255-b2f7fd83e387a9e1.js",revision:"b2f7fd83e387a9e1"},{url:"/_next/static/chunks/1280-296e0a2b6e9dd9b1.js",revision:"296e0a2b6e9dd9b1"},{url:"/_next/static/chunks/1514-a6ed8a01b9885870.js",revision:"a6ed8a01b9885870"},{url:"/_next/static/chunks/1543-530e0f57f7af68aa.js",revision:"530e0f57f7af68aa"},{url:"/_next/static/chunks/1843-b2c03c59f335abf4.js",revision:"b2c03c59f335abf4"},{url:"/_next/static/chunks/189-453061dd646fdba4.js",revision:"453061dd646fdba4"},{url:"/_next/static/chunks/1930-cd8328eb1cfa4178.js",revision:"cd8328eb1cfa4178"},{url:"/_next/static/chunks/2262-26293d6453fcc927.js",revision:"26293d6453fcc927"},{url:"/_next/static/chunks/2506-35fdf1de6e0fd0ec.js",revision:"35fdf1de6e0fd0ec"},{url:"/_next/static/chunks/2619-04bc32f026a0d946.js",revision:"04bc32f026a0d946"},{url:"/_next/static/chunks/3127-49a95e7cb556ace3.js",revision:"49a95e7cb556ace3"},{url:"/_next/static/chunks/3452-86647d15ff7842a5.js",revision:"86647d15ff7842a5"},{url:"/_next/static/chunks/3460-a2b6a712ec21acfb.js",revision:"a2b6a712ec21acfb"},{url:"/_next/static/chunks/3664-56dedfcaec4aaceb.js",revision:"56dedfcaec4aaceb"},{url:"/_next/static/chunks/3762-6764a708ac67fa18.js",revision:"6764a708ac67fa18"},{url:"/_next/static/chunks/4337-7b7eba57ddf5f8f1.js",revision:"7b7eba57ddf5f8f1"},{url:"/_next/static/chunks/4450.a965376e27addfd5.js",revision:"a965376e27addfd5"},{url:"/_next/static/chunks/4bd1b696-100b9d70ed4e49c1.js",revision:"100b9d70ed4e49c1"},{url:"/_next/static/chunks/5124-1abf907e61fbf455.js",revision:"1abf907e61fbf455"},{url:"/_next/static/chunks/5125-c990fc036d2a6ce4.js",revision:"c990fc036d2a6ce4"},{url:"/_next/static/chunks/5385-7ecda8e4ba984edc.js",revision:"7ecda8e4ba984edc"},{url:"/_next/static/chunks/5482-7535aa0aab02d518.js",revision:"7535aa0aab02d518"},{url:"/_next/static/chunks/6088-c165c565edce02be.js",revision:"c165c565edce02be"},{url:"/_next/static/chunks/6183-84d624cdd79749ef.js",revision:"84d624cdd79749ef"},{url:"/_next/static/chunks/658-1d9d4c0c8b5fb129.js",revision:"1d9d4c0c8b5fb129"},{url:"/_next/static/chunks/670-a4ca0f366ee779f5.js",revision:"a4ca0f366ee779f5"},{url:"/_next/static/chunks/6873-ff265086321345c8.js",revision:"ff265086321345c8"},{url:"/_next/static/chunks/6886-40f1779ffff00d58.js",revision:"40f1779ffff00d58"},{url:"/_next/static/chunks/7029-7ec3e4452c8304d7.js",revision:"7ec3e4452c8304d7"},{url:"/_next/static/chunks/710-7e96cbf5d461482a.js",revision:"7e96cbf5d461482a"},{url:"/_next/static/chunks/7166-45df3fb3c28138b5.js",revision:"45df3fb3c28138b5"},{url:"/_next/static/chunks/7225-8f913ab2896c1c1a.js",revision:"8f913ab2896c1c1a"},{url:"/_next/static/chunks/7359-1abfb9f346309354.js",revision:"1abfb9f346309354"},{url:"/_next/static/chunks/7741-0af8b5a61d8e63d3.js",revision:"0af8b5a61d8e63d3"},{url:"/_next/static/chunks/7855-72c79224370eff7b.js",revision:"72c79224370eff7b"},{url:"/_next/static/chunks/787-032067ae978e62a8.js",revision:"032067ae978e62a8"},{url:"/_next/static/chunks/7902-e1f71c3b4c62bff9.js",revision:"e1f71c3b4c62bff9"},{url:"/_next/static/chunks/8221-d51102291d5ddaf9.js",revision:"d51102291d5ddaf9"},{url:"/_next/static/chunks/8241-eaf1b9c6054e9ad8.js",revision:"eaf1b9c6054e9ad8"},{url:"/_next/static/chunks/8412-8ce7440f3599e2d9.js",revision:"8ce7440f3599e2d9"},{url:"/_next/static/chunks/8466-ffa71cea7998f777.js",revision:"ffa71cea7998f777"},{url:"/_next/static/chunks/8544.74f59dd908783038.js",revision:"74f59dd908783038"},{url:"/_next/static/chunks/8746-92ff3ad56eb06d6e.js",revision:"92ff3ad56eb06d6e"},{url:"/_next/static/chunks/9064-b6e187b9163c365e.js",revision:"b6e187b9163c365e"},{url:"/_next/static/chunks/9205-f540995b767df00b.js",revision:"f540995b767df00b"},{url:"/_next/static/chunks/9300-1cc361e1420fd381.js",revision:"1cc361e1420fd381"},{url:"/_next/static/chunks/9328-a7221e84fb4e038c.js",revision:"a7221e84fb4e038c"},{url:"/_next/static/chunks/9333-d0f251c321264800.js",revision:"d0f251c321264800"},{url:"/_next/static/chunks/9382-bf36ea7e8453c043.js",revision:"bf36ea7e8453c043"},{url:"/_next/static/chunks/9392-2887c5e5703ed90a.js",revision:"2887c5e5703ed90a"},{url:"/_next/static/chunks/9397-40b8ac68e22a4d87.js",revision:"40b8ac68e22a4d87"},{url:"/_next/static/chunks/9517-17518b5fffe76114.js",revision:"17518b5fffe76114"},{url:"/_next/static/chunks/9738-d4ae78df35beeba7.js",revision:"d4ae78df35beeba7"},{url:"/_next/static/chunks/9958.29bedcecbfff5702.js",revision:"29bedcecbfff5702"},{url:"/_next/static/chunks/app/(auth)/forgot-password/page-ca00943cf66c3e17.js",revision:"ca00943cf66c3e17"},{url:"/_next/static/chunks/app/(auth)/login/page-cc332e92a316f523.js",revision:"cc332e92a316f523"},{url:"/_next/static/chunks/app/(auth)/onboarding/page-762359ce9bda51b2.js",revision:"762359ce9bda51b2"},{url:"/_next/static/chunks/app/(auth)/register/page-e29ce0efae5e914c.js",revision:"e29ce0efae5e914c"},{url:"/_next/static/chunks/app/(auth)/reset-password/page-2eec6b4142e79702.js",revision:"2eec6b4142e79702"},{url:"/_next/static/chunks/app/_not-found/page-95f11f5fe94340f1.js",revision:"95f11f5fe94340f1"},{url:"/_next/static/chunks/app/ai-assistant/page-3edb2cda7412d8b4.js",revision:"3edb2cda7412d8b4"},{url:"/_next/static/chunks/app/analytics/advanced/page-f0a9b68c7fe29045.js",revision:"f0a9b68c7fe29045"},{url:"/_next/static/chunks/app/analytics/page-e8b19d9a0c19ea81.js",revision:"e8b19d9a0c19ea81"},{url:"/_next/static/chunks/app/api/ai/chat/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/login/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/password-reset/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/register/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/health/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/tracking/feeding/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/voice/transcribe/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/children/page-40f1bfa952ee593c.js",revision:"40f1bfa952ee593c"},{url:"/_next/static/chunks/app/family/page-a6c42634bbec4ac7.js",revision:"a6c42634bbec4ac7"},{url:"/_next/static/chunks/app/history/page-16263a21d7085bd2.js",revision:"16263a21d7085bd2"},{url:"/_next/static/chunks/app/insights/page-353479d33da19bb2.js",revision:"353479d33da19bb2"},{url:"/_next/static/chunks/app/layout-542bc44b26b9e572.js",revision:"542bc44b26b9e572"},{url:"/_next/static/chunks/app/legal/cookies/page-c39a3fa6e27a8806.js",revision:"c39a3fa6e27a8806"},{url:"/_next/static/chunks/app/legal/eula/page-8015f749ab4dd660.js",revision:"8015f749ab4dd660"},{url:"/_next/static/chunks/app/legal/page-3de074f0b9741bc6.js",revision:"3de074f0b9741bc6"},{url:"/_next/static/chunks/app/legal/privacy/page-3cb58024b6fd8e21.js",revision:"3cb58024b6fd8e21"},{url:"/_next/static/chunks/app/legal/terms/page-b5a1c96cae251767.js",revision:"b5a1c96cae251767"},{url:"/_next/static/chunks/app/logout/page-82ffa2a8f9b19c05.js",revision:"82ffa2a8f9b19c05"},{url:"/_next/static/chunks/app/offline/page-28c005360c2b2736.js",revision:"28c005360c2b2736"},{url:"/_next/static/chunks/app/page-a3c318199e34d81a.js",revision:"a3c318199e34d81a"},{url:"/_next/static/chunks/app/settings/page-defd290e5f83d748.js",revision:"defd290e5f83d748"},{url:"/_next/static/chunks/app/track/activity/page-5a7c9c4ba32c547e.js",revision:"5a7c9c4ba32c547e"},{url:"/_next/static/chunks/app/track/diaper/page-a46f1a6342e09976.js",revision:"a46f1a6342e09976"},{url:"/_next/static/chunks/app/track/feeding/page-50d04c323c3920a8.js",revision:"50d04c323c3920a8"},{url:"/_next/static/chunks/app/track/growth/page-a7aa8f6196c26c5f.js",revision:"a7aa8f6196c26c5f"},{url:"/_next/static/chunks/app/track/medicine/page-ba39d82f93b4a318.js",revision:"ba39d82f93b4a318"},{url:"/_next/static/chunks/app/track/page-dd5ade1eb19ad389.js",revision:"dd5ade1eb19ad389"},{url:"/_next/static/chunks/app/track/sleep/page-b586a2d14249bb9a.js",revision:"b586a2d14249bb9a"},{url:"/_next/static/chunks/framework-bd61ec64032c2de7.js",revision:"bd61ec64032c2de7"},{url:"/_next/static/chunks/main-520e5ec2d671abe7.js",revision:"520e5ec2d671abe7"},{url:"/_next/static/chunks/main-app-02fc3649960ba6c7.js",revision:"02fc3649960ba6c7"},{url:"/_next/static/chunks/pages/_app-4b3fb5e477a0267f.js",revision:"4b3fb5e477a0267f"},{url:"/_next/static/chunks/pages/_error-c970d8b55ace1b48.js",revision:"c970d8b55ace1b48"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-45f02cdad7c631c1.js",revision:"45f02cdad7c631c1"},{url:"/_next/static/css/76c3c1634e215e77.css",revision:"76c3c1634e215e77"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"fa2d4d791b90148a18d49bc3bfd7a43a"},{url:"/check-updates.js",revision:"bc016a0ceb6c72a5fe9ba02ad05d78be"},{url:"/favicon-16x16.png",revision:"db2da3355c89a6149f6d9ee35ebe6bf3"},{url:"/favicon-32x32.png",revision:"0fd88d56aa584bd0546d05ffc63ef777"},{url:"/icon-192x192.png",revision:"b8ef7f117472c4399cceffea644eb8bd"},{url:"/icons/icon-128x128.png",revision:"96cff3b189d9c1daa1edf470290a90cd"},{url:"/icons/icon-144x144.png",revision:"b627c346c431d7e306005aec5f51baff"},{url:"/icons/icon-152x152.png",revision:"012071830c13d310e51f833baed531af"},{url:"/icons/icon-192x192.png",revision:"dfb20132ddb628237eccd4b0e2ee4aaa"},{url:"/icons/icon-384x384.png",revision:"d032b25376232878a2a29b5688992a8d"},{url:"/icons/icon-512x512.png",revision:"ffda0043571d60956f4e321cba706670"},{url:"/icons/icon-72x72.png",revision:"cc89e74126e7e1109f0186774b3c0d77"},{url:"/icons/icon-96x96.png",revision:"32813cdad5b636fc09eec01c7d705936"},{url:"/manifest.json",revision:"5cbf1ecd33b05c4772688ce7d00c2c23"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"61c6b19abff40ea7acd577be818f3976"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:a,event:s,state:c})=>a&&"opaqueredirect"===a.type?new Response(a.body,{status:200,statusText:"OK",headers:a.headers}):a}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/api\/.*$/i,new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/.*/i,new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET")}); +if(!self.define){let e,a={};const s=(s,c)=>(s=new URL(s+".js",c).href,a[s]||new Promise(a=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=a,document.head.appendChild(e)}else e=s,importScripts(s),a()}).then(()=>{let e=a[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(c,i)=>{const n=e||("document"in self?document.currentScript.src:"")||location.href;if(a[n])return;let t={};const d=e=>s(e,n),f={module:{uri:n},exports:t,require:d};a[n]=Promise.all(c.map(e=>f[e]||d(e))).then(e=>(i(...e),t))}}define(["./workbox-4d767a27"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"d6c86768dc57bf65a4d6eb1d3d1a1fdd"},{url:"/_next/static/39AHXX_NOouP2Oz0P0jez/_buildManifest.js",revision:"673df67655213af81147283455f8956d"},{url:"/_next/static/39AHXX_NOouP2Oz0P0jez/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/1091.c762d795c6885f94.js",revision:"c762d795c6885f94"},{url:"/_next/static/chunks/1188.88c14dc0b9d46cf9.js",revision:"88c14dc0b9d46cf9"},{url:"/_next/static/chunks/1255-b2f7fd83e387a9e1.js",revision:"b2f7fd83e387a9e1"},{url:"/_next/static/chunks/1280-077bbec6d00a7de6.js",revision:"077bbec6d00a7de6"},{url:"/_next/static/chunks/1514-a6ed8a01b9885870.js",revision:"a6ed8a01b9885870"},{url:"/_next/static/chunks/1543-530e0f57f7af68aa.js",revision:"530e0f57f7af68aa"},{url:"/_next/static/chunks/164f4fb6.cb2a48d4da4418c4.js",revision:"cb2a48d4da4418c4"},{url:"/_next/static/chunks/189-453061dd646fdba4.js",revision:"453061dd646fdba4"},{url:"/_next/static/chunks/1930-cd8328eb1cfa4178.js",revision:"cd8328eb1cfa4178"},{url:"/_next/static/chunks/2262-26293d6453fcc927.js",revision:"26293d6453fcc927"},{url:"/_next/static/chunks/2506-35fdf1de6e0fd0ec.js",revision:"35fdf1de6e0fd0ec"},{url:"/_next/static/chunks/2619-04bc32f026a0d946.js",revision:"04bc32f026a0d946"},{url:"/_next/static/chunks/2931.14c1e0fb7788f4ba.js",revision:"14c1e0fb7788f4ba"},{url:"/_next/static/chunks/2f0b94e8.3186a98eb4c9012b.js",revision:"3186a98eb4c9012b"},{url:"/_next/static/chunks/3127-49a95e7cb556ace3.js",revision:"49a95e7cb556ace3"},{url:"/_next/static/chunks/3452-86647d15ff7842a5.js",revision:"86647d15ff7842a5"},{url:"/_next/static/chunks/3460-a2b6a712ec21acfb.js",revision:"a2b6a712ec21acfb"},{url:"/_next/static/chunks/3664-56dedfcaec4aaceb.js",revision:"56dedfcaec4aaceb"},{url:"/_next/static/chunks/3762-a8faf80b78d53eb7.js",revision:"a8faf80b78d53eb7"},{url:"/_next/static/chunks/4199.bc1715114dd19eda.js",revision:"bc1715114dd19eda"},{url:"/_next/static/chunks/4337-7b7eba57ddf5f8f1.js",revision:"7b7eba57ddf5f8f1"},{url:"/_next/static/chunks/4648-2997dba2ece0d119.js",revision:"2997dba2ece0d119"},{url:"/_next/static/chunks/4710-9f9aefe46e6a48d5.js",revision:"9f9aefe46e6a48d5"},{url:"/_next/static/chunks/4bd1b696-100b9d70ed4e49c1.js",revision:"100b9d70ed4e49c1"},{url:"/_next/static/chunks/5125-c990fc036d2a6ce4.js",revision:"c990fc036d2a6ce4"},{url:"/_next/static/chunks/5380-9004e1ac3565daca.js",revision:"9004e1ac3565daca"},{url:"/_next/static/chunks/5385-7ecda8e4ba984edc.js",revision:"7ecda8e4ba984edc"},{url:"/_next/static/chunks/5482-7535aa0aab02d518.js",revision:"7535aa0aab02d518"},{url:"/_next/static/chunks/551.26e2933365d2f96d.js",revision:"26e2933365d2f96d"},{url:"/_next/static/chunks/6088-c165c565edce02be.js",revision:"c165c565edce02be"},{url:"/_next/static/chunks/6181-66be9b76f10d48f6.js",revision:"66be9b76f10d48f6"},{url:"/_next/static/chunks/6183-84d624cdd79749ef.js",revision:"84d624cdd79749ef"},{url:"/_next/static/chunks/658-1d9d4c0c8b5fb129.js",revision:"1d9d4c0c8b5fb129"},{url:"/_next/static/chunks/670-a4ca0f366ee779f5.js",revision:"a4ca0f366ee779f5"},{url:"/_next/static/chunks/6873-ff265086321345c8.js",revision:"ff265086321345c8"},{url:"/_next/static/chunks/6886-40f1779ffff00d58.js",revision:"40f1779ffff00d58"},{url:"/_next/static/chunks/710-7e96cbf5d461482a.js",revision:"7e96cbf5d461482a"},{url:"/_next/static/chunks/711-5df8fde8fe94f7a8.js",revision:"5df8fde8fe94f7a8"},{url:"/_next/static/chunks/7359-1abfb9f346309354.js",revision:"1abfb9f346309354"},{url:"/_next/static/chunks/7741-0af8b5a61d8e63d3.js",revision:"0af8b5a61d8e63d3"},{url:"/_next/static/chunks/7855-72c79224370eff7b.js",revision:"72c79224370eff7b"},{url:"/_next/static/chunks/787-032067ae978e62a8.js",revision:"032067ae978e62a8"},{url:"/_next/static/chunks/7902-e1f71c3b4c62bff9.js",revision:"e1f71c3b4c62bff9"},{url:"/_next/static/chunks/8221-d51102291d5ddaf9.js",revision:"d51102291d5ddaf9"},{url:"/_next/static/chunks/8241-eaf1b9c6054e9ad8.js",revision:"eaf1b9c6054e9ad8"},{url:"/_next/static/chunks/8412-8ce7440f3599e2d9.js",revision:"8ce7440f3599e2d9"},{url:"/_next/static/chunks/8423-ac92fec5ac4dabe7.js",revision:"ac92fec5ac4dabe7"},{url:"/_next/static/chunks/8466-ffa71cea7998f777.js",revision:"ffa71cea7998f777"},{url:"/_next/static/chunks/8544.3d61228111902e97.js",revision:"3d61228111902e97"},{url:"/_next/static/chunks/8746-92ff3ad56eb06d6e.js",revision:"92ff3ad56eb06d6e"},{url:"/_next/static/chunks/8900-ff82add2eebd43fa.js",revision:"ff82add2eebd43fa"},{url:"/_next/static/chunks/9205-f540995b767df00b.js",revision:"f540995b767df00b"},{url:"/_next/static/chunks/9333-edf14831f0a39549.js",revision:"edf14831f0a39549"},{url:"/_next/static/chunks/9392-2887c5e5703ed90a.js",revision:"2887c5e5703ed90a"},{url:"/_next/static/chunks/9397-40b8ac68e22a4d87.js",revision:"40b8ac68e22a4d87"},{url:"/_next/static/chunks/9515-53e74005e71810bd.js",revision:"53e74005e71810bd"},{url:"/_next/static/chunks/9517-17518b5fffe76114.js",revision:"17518b5fffe76114"},{url:"/_next/static/chunks/9580-031d243edbbe82e5.js",revision:"031d243edbbe82e5"},{url:"/_next/static/chunks/9738-d4ae78df35beeba7.js",revision:"d4ae78df35beeba7"},{url:"/_next/static/chunks/ad2866b8.e13a3cf75ccf0eb8.js",revision:"e13a3cf75ccf0eb8"},{url:"/_next/static/chunks/app/(auth)/forgot-password/page-7311b657b7446bf6.js",revision:"7311b657b7446bf6"},{url:"/_next/static/chunks/app/(auth)/login/page-bb5d0eaa38179e93.js",revision:"bb5d0eaa38179e93"},{url:"/_next/static/chunks/app/(auth)/onboarding/page-28f345357577a0af.js",revision:"28f345357577a0af"},{url:"/_next/static/chunks/app/(auth)/register/page-d0632ddd8172dde1.js",revision:"d0632ddd8172dde1"},{url:"/_next/static/chunks/app/(auth)/reset-password/page-2f5aaa4f5e10070d.js",revision:"2f5aaa4f5e10070d"},{url:"/_next/static/chunks/app/_not-found/page-95f11f5fe94340f1.js",revision:"95f11f5fe94340f1"},{url:"/_next/static/chunks/app/ai-assistant/page-3edb2cda7412d8b4.js",revision:"3edb2cda7412d8b4"},{url:"/_next/static/chunks/app/analytics/advanced/page-8dce8adb1ed3736a.js",revision:"8dce8adb1ed3736a"},{url:"/_next/static/chunks/app/analytics/page-938a3b366d2969b4.js",revision:"938a3b366d2969b4"},{url:"/_next/static/chunks/app/api/ai/chat/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/login/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/password-reset/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/auth/register/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/health/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/tracking/feeding/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/api/voice/transcribe/route-a631d97a33877f8a.js",revision:"a631d97a33877f8a"},{url:"/_next/static/chunks/app/children/page-40f1bfa952ee593c.js",revision:"40f1bfa952ee593c"},{url:"/_next/static/chunks/app/family/page-a6c42634bbec4ac7.js",revision:"a6c42634bbec4ac7"},{url:"/_next/static/chunks/app/history/page-36e2f94462dd67ae.js",revision:"36e2f94462dd67ae"},{url:"/_next/static/chunks/app/insights/page-296df3d508143098.js",revision:"296df3d508143098"},{url:"/_next/static/chunks/app/layout-dc028048fba2f9f1.js",revision:"dc028048fba2f9f1"},{url:"/_next/static/chunks/app/legal/cookies/page-c39a3fa6e27a8806.js",revision:"c39a3fa6e27a8806"},{url:"/_next/static/chunks/app/legal/eula/page-8015f749ab4dd660.js",revision:"8015f749ab4dd660"},{url:"/_next/static/chunks/app/legal/page-3de074f0b9741bc6.js",revision:"3de074f0b9741bc6"},{url:"/_next/static/chunks/app/legal/privacy/page-3cb58024b6fd8e21.js",revision:"3cb58024b6fd8e21"},{url:"/_next/static/chunks/app/legal/terms/page-b5a1c96cae251767.js",revision:"b5a1c96cae251767"},{url:"/_next/static/chunks/app/logout/page-48007f334d2917ca.js",revision:"48007f334d2917ca"},{url:"/_next/static/chunks/app/offline/page-28c005360c2b2736.js",revision:"28c005360c2b2736"},{url:"/_next/static/chunks/app/page-c5729e7d614eb749.js",revision:"c5729e7d614eb749"},{url:"/_next/static/chunks/app/settings/page-0341b76587e9db2f.js",revision:"0341b76587e9db2f"},{url:"/_next/static/chunks/app/track/activity/page-3767427eaacf5fff.js",revision:"3767427eaacf5fff"},{url:"/_next/static/chunks/app/track/diaper/page-c62c6bfb393c13a1.js",revision:"c62c6bfb393c13a1"},{url:"/_next/static/chunks/app/track/feeding/page-5acbeddc0db06597.js",revision:"5acbeddc0db06597"},{url:"/_next/static/chunks/app/track/growth/page-aac0bf91cb288a19.js",revision:"aac0bf91cb288a19"},{url:"/_next/static/chunks/app/track/medicine/page-dcad90e6224b0800.js",revision:"dcad90e6224b0800"},{url:"/_next/static/chunks/app/track/page-dd5ade1eb19ad389.js",revision:"dd5ade1eb19ad389"},{url:"/_next/static/chunks/app/track/sleep/page-b586a2d14249bb9a.js",revision:"b586a2d14249bb9a"},{url:"/_next/static/chunks/bc98253f.2a96f718cf128d0e.js",revision:"2a96f718cf128d0e"},{url:"/_next/static/chunks/framework-bd61ec64032c2de7.js",revision:"bd61ec64032c2de7"},{url:"/_next/static/chunks/main-520e5ec2d671abe7.js",revision:"520e5ec2d671abe7"},{url:"/_next/static/chunks/main-app-02fc3649960ba6c7.js",revision:"02fc3649960ba6c7"},{url:"/_next/static/chunks/pages/_app-4b3fb5e477a0267f.js",revision:"4b3fb5e477a0267f"},{url:"/_next/static/chunks/pages/_error-c970d8b55ace1b48.js",revision:"c970d8b55ace1b48"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-ffb6d2ad3fedf086.js",revision:"ffb6d2ad3fedf086"},{url:"/_next/static/css/e883b9d6cff8e10c.css",revision:"e883b9d6cff8e10c"},{url:"/_next/static/media/19cfc7226ec3afaa-s.woff2",revision:"9dda5cfc9a46f256d0e131bb535e46f8"},{url:"/_next/static/media/21350d82a1f187e9-s.woff2",revision:"4e2553027f1d60eff32898367dd4d541"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/ba9851c3c22cd980-s.woff2",revision:"9e494903d6b0ffec1a1e14d34427d44d"},{url:"/_next/static/media/c5fe6dc8356a8c31-s.woff2",revision:"027a89e9ab733a145db70f09b8a18b42"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/apple-touch-icon.png",revision:"fa2d4d791b90148a18d49bc3bfd7a43a"},{url:"/check-updates.js",revision:"bc016a0ceb6c72a5fe9ba02ad05d78be"},{url:"/favicon-16x16.png",revision:"db2da3355c89a6149f6d9ee35ebe6bf3"},{url:"/favicon-32x32.png",revision:"0fd88d56aa584bd0546d05ffc63ef777"},{url:"/icon-192x192.png",revision:"b8ef7f117472c4399cceffea644eb8bd"},{url:"/icons/icon-128x128.png",revision:"96cff3b189d9c1daa1edf470290a90cd"},{url:"/icons/icon-144x144.png",revision:"b627c346c431d7e306005aec5f51baff"},{url:"/icons/icon-152x152.png",revision:"012071830c13d310e51f833baed531af"},{url:"/icons/icon-192x192.png",revision:"dfb20132ddb628237eccd4b0e2ee4aaa"},{url:"/icons/icon-384x384.png",revision:"d032b25376232878a2a29b5688992a8d"},{url:"/icons/icon-512x512.png",revision:"ffda0043571d60956f4e321cba706670"},{url:"/icons/icon-72x72.png",revision:"cc89e74126e7e1109f0186774b3c0d77"},{url:"/icons/icon-96x96.png",revision:"32813cdad5b636fc09eec01c7d705936"},{url:"/manifest.json",revision:"5cbf1ecd33b05c4772688ce7d00c2c23"},{url:"/next.svg",revision:"8e061864f388b47f33a1c3780831193e"},{url:"/vercel.svg",revision:"61c6b19abff40ea7acd577be818f3976"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:a,event:s,state:c})=>a&&"opaqueredirect"===a.type?new Response(a.body,{status:200,statusText:"OK",headers:a.headers}):a}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/api\/.*$/i,new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/.*/i,new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET")}); diff --git a/start-production.sh b/start-production.sh new file mode 100755 index 0000000..65fc32e --- /dev/null +++ b/start-production.sh @@ -0,0 +1,53 @@ +#!/bin/bash + +# Start Production Servers Script +# Ports: Backend 3020, Frontend 3030 + +echo "Starting ParentFlow Production Servers..." + +# Kill any existing processes on the ports +echo "Cleaning up existing processes..." +lsof -ti:3020 | xargs kill -9 2>/dev/null +lsof -ti:3030 | xargs kill -9 2>/dev/null +sleep 2 + +# Start Backend on port 3020 +echo "Starting backend on port 3020..." +cd /root/maternal-app/maternal-app/maternal-app-backend +PORT=3020 API_PORT=3020 NODE_ENV=production nohup node dist/main.js > /root/maternal-app/logs/backend-prod.log 2>&1 & +BACKEND_PID=$! +echo "Backend started with PID: $BACKEND_PID" + +# Start Frontend on port 3030 +echo "Starting frontend on port 3030..." +cd /root/maternal-app/maternal-web +PORT=3030 NODE_ENV=production nohup npm run start > /root/maternal-app/logs/frontend-prod.log 2>&1 & +FRONTEND_PID=$! +echo "Frontend started with PID: $FRONTEND_PID" + +# Wait a moment for servers to start +sleep 5 + +# Check if servers are running +echo "" +echo "Checking server status..." +if lsof -i:3020 > /dev/null 2>&1; then + echo "✅ Backend is running on port 3020" +else + echo "❌ Backend failed to start on port 3020" +fi + +if lsof -i:3030 > /dev/null 2>&1; then + echo "✅ Frontend is running on port 3030" +else + echo "❌ Frontend failed to start on port 3030" +fi + +echo "" +echo "Production servers started!" +echo "Backend: http://localhost:3020" +echo "Frontend: http://localhost:3030" +echo "" +echo "To check logs:" +echo " Backend: tail -f /root/maternal-app/logs/backend-prod.log" +echo " Frontend: tail -f /root/maternal-app/logs/frontend-prod.log" \ No newline at end of file diff --git a/test-embeddings.js b/test-embeddings.js deleted file mode 100755 index 6e96ec6..0000000 --- a/test-embeddings.js +++ /dev/null @@ -1,363 +0,0 @@ -#!/usr/bin/env node - -/** - * Embeddings-Based Conversation Memory Test Suite - * - * Tests the vector embeddings functionality for semantic search - */ - -const axios = require('axios'); - -const BASE_URL = 'http://localhost:3020/api/v1/ai'; -const colors = { - reset: '\x1b[0m', - green: '\x1b[32m', - red: '\x1b[31m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', -}; - -function log(color, message) { - console.log(`${colors[color]}${message}${colors.reset}`); -} - -function logTest(testName) { - console.log(`\n${colors.cyan}━━━ ${testName} ━━━${colors.reset}`); -} - -function logSuccess(message) { - log('green', `✓ ${message}`); -} - -function logError(message) { - log('red', `✗ ${message}`); -} - -function logInfo(message) { - log('blue', `ℹ ${message}`); -} - -async function sleep(ms) { - return new Promise(resolve => setTimeout(resolve, ms)); -} - -// Test 1: Health Check -async function testHealthCheck() { - logTest('Test 1: Embeddings Service Health Check'); - try { - const response = await axios.get(`${BASE_URL}/test/embeddings/health`); - - if (response.data.success && response.data.data.status === 'ok') { - logSuccess(`Health check passed: ${response.data.data.message}`); - return true; - } else { - logError(`Health check failed: ${response.data.data.message}`); - return false; - } - } catch (error) { - logError(`Health check error: ${error.message}`); - if (error.response?.data) { - console.log(JSON.stringify(error.response.data, null, 2)); - } - return false; - } -} - -// Test 2: Generate Embedding -async function testGenerateEmbedding() { - logTest('Test 2: Generate Vector Embedding'); - try { - const testText = "My baby had a feeding session with 4 oz of formula"; - logInfo(`Generating embedding for: "${testText}"`); - - const response = await axios.post(`${BASE_URL}/test/embeddings/generate`, { - text: testText - }); - - if (response.data.success) { - const { dimensions, tokenCount, model, preview } = response.data.data; - logSuccess(`Embedding generated successfully`); - logInfo(` Model: ${model}`); - logInfo(` Dimensions: ${dimensions}`); - logInfo(` Token count: ${tokenCount}`); - logInfo(` Preview (first 5): [${preview.join(', ')}...]`); - return true; - } else { - logError('Embedding generation failed'); - return false; - } - } catch (error) { - logError(`Embedding generation error: ${error.message}`); - if (error.response?.data) { - console.log(JSON.stringify(error.response.data, null, 2)); - } - return false; - } -} - -// Test 3: Create Conversation with Embeddings -async function testCreateConversationWithEmbeddings() { - logTest('Test 3: Create Conversation and Store Embeddings'); - try { - const conversations = [ - { message: "My baby slept for 3 hours during the night", topic: "sleep" }, - { message: "She had a feeding session with 5 oz of formula", topic: "feeding" }, - { message: "Changed a wet diaper at 3pm", topic: "diaper" }, - { message: "Baby has a slight fever, should I be worried?", topic: "health" }, - { message: "She started crawling today! So excited!", topic: "development" }, - ]; - - const conversationIds = []; - - for (const conv of conversations) { - logInfo(`Creating conversation: "${conv.message}" (${conv.topic})`); - - const response = await axios.post(`${BASE_URL}/chat`, { - message: conv.message - }); - - if (response.data.success) { - const conversationId = response.data.data.conversationId; - conversationIds.push({ id: conversationId, topic: conv.topic, message: conv.message }); - logSuccess(` Created conversation ${conversationId}`); - logInfo(` AI Response: ${response.data.data.message.substring(0, 100)}...`); - } else { - logError(` Failed to create conversation`); - } - - // Wait to allow embeddings to be stored - await sleep(1000); - } - - logSuccess(`Created ${conversationIds.length} conversations with embeddings`); - return conversationIds; - } catch (error) { - logError(`Conversation creation error: ${error.message}`); - if (error.response?.data) { - console.log(JSON.stringify(error.response.data, null, 2)); - } - return []; - } -} - -// Test 4: Semantic Search -async function testSemanticSearch(conversationIds) { - logTest('Test 4: Semantic Search for Similar Conversations'); - - const searchQueries = [ - { query: "How long should my baby sleep at night?", expectedTopic: "sleep" }, - { query: "What's the right amount of milk for feeding?", expectedTopic: "feeding" }, - { query: "When should I change diapers?", expectedTopic: "diaper" }, - { query: "Is a high temperature dangerous?", expectedTopic: "health" }, - { query: "What are the milestones for a 6 month old?", expectedTopic: "development" }, - ]; - - let successCount = 0; - - for (const searchQuery of searchQueries) { - logInfo(`\nSearching: "${searchQuery.query}"`); - - try { - const response = await axios.post(`${BASE_URL}/test/embeddings/search`, { - query: searchQuery.query, - userId: 'test_user_123', - threshold: 0.5, - limit: 3 - }); - - if (response.data.success && response.data.data.results.length > 0) { - const results = response.data.data.results; - logSuccess(` Found ${results.length} similar conversation(s)`); - - results.forEach((result, index) => { - const similarity = (result.similarity * 100).toFixed(1); - logInfo(` ${index + 1}. Similarity: ${similarity}%`); - logInfo(` Topics: [${result.topics.join(', ')}]`); - logInfo(` Content: "${result.messageContent.substring(0, 60)}..."`); - - // Check if expected topic is in results - if (result.topics.includes(searchQuery.expectedTopic)) { - logSuccess(` ✓ Found expected topic: ${searchQuery.expectedTopic}`); - } - }); - - successCount++; - } else { - logError(` No similar conversations found`); - } - } catch (error) { - logError(` Search error: ${error.message}`); - if (error.response?.data) { - console.log(JSON.stringify(error.response.data, null, 2)); - } - } - } - - logInfo(`\nSemantic search success rate: ${successCount}/${searchQueries.length}`); - return successCount === searchQueries.length; -} - -// Test 5: Get Embeddings Stats -async function testEmbeddingsStats() { - logTest('Test 5: Get User Embeddings Statistics'); - try { - const response = await axios.get(`${BASE_URL}/test/embeddings/stats/test_user_123`); - - if (response.data.success) { - const stats = response.data.data; - logSuccess('Retrieved embeddings statistics'); - logInfo(` Total embeddings: ${stats.totalEmbeddings}`); - logInfo(` Conversations with embeddings: ${stats.conversationsWithEmbeddings}`); - logInfo(` Topics distribution:`); - Object.entries(stats.topicsDistribution).forEach(([topic, count]) => { - logInfo(` - ${topic}: ${count}`); - }); - return true; - } else { - logError('Failed to retrieve stats'); - return false; - } - } catch (error) { - logError(`Stats retrieval error: ${error.message}`); - if (error.response?.data) { - console.log(JSON.stringify(error.response.data, null, 2)); - } - return false; - } -} - -// Test 6: Conversation with Semantic Memory -async function testConversationWithSemanticMemory() { - logTest('Test 6: New Conversation Using Semantic Memory'); - try { - logInfo('Creating follow-up question that should find semantic context...'); - - const response = await axios.post(`${BASE_URL}/chat`, { - message: "My baby is having trouble sleeping, any tips?" - }); - - if (response.data.success) { - logSuccess('Conversation created with semantic context'); - logInfo(`AI Response: ${response.data.data.message.substring(0, 200)}...`); - - // Check if response seems contextual (contains sleep-related info) - const responseText = response.data.data.message.toLowerCase(); - if (responseText.includes('sleep') || responseText.includes('nap')) { - logSuccess('Response appears to use semantic context (mentions sleep)'); - return true; - } else { - logInfo('Response created, but semantic context usage unclear'); - return true; - } - } else { - logError('Conversation creation failed'); - return false; - } - } catch (error) { - logError(`Semantic memory test error: ${error.message}`); - if (error.response?.data) { - console.log(JSON.stringify(error.response.data, null, 2)); - } - return false; - } -} - -// Main test runner -async function runTests() { - console.log(`\n${colors.yellow}╔════════════════════════════════════════════════╗${colors.reset}`); - console.log(`${colors.yellow}║ Embeddings-Based Conversation Memory Tests ║${colors.reset}`); - console.log(`${colors.yellow}╚════════════════════════════════════════════════╝${colors.reset}\n`); - - const results = { - total: 6, - passed: 0, - failed: 0 - }; - - // Test 1: Health Check - if (await testHealthCheck()) { - results.passed++; - } else { - results.failed++; - logError('Health check failed - stopping tests'); - return results; - } - - await sleep(500); - - // Test 2: Generate Embedding - if (await testGenerateEmbedding()) { - results.passed++; - } else { - results.failed++; - } - - await sleep(500); - - // Test 3: Create Conversations - const conversationIds = await testCreateConversationWithEmbeddings(); - if (conversationIds.length > 0) { - results.passed++; - } else { - results.failed++; - } - - await sleep(2000); // Wait for embeddings to be stored - - // Test 4: Semantic Search - if (await testSemanticSearch(conversationIds)) { - results.passed++; - } else { - results.failed++; - } - - await sleep(500); - - // Test 5: Embeddings Stats - if (await testEmbeddingsStats()) { - results.passed++; - } else { - results.failed++; - } - - await sleep(500); - - // Test 6: Semantic Memory - if (await testConversationWithSemanticMemory()) { - results.passed++; - } else { - results.failed++; - } - - // Summary - console.log(`\n${colors.yellow}╔════════════════════════════════════════════════╗${colors.reset}`); - console.log(`${colors.yellow}║ Test Summary ║${colors.reset}`); - console.log(`${colors.yellow}╚════════════════════════════════════════════════╝${colors.reset}\n`); - - log('blue', `Total tests: ${results.total}`); - log('green', `Passed: ${results.passed}`); - if (results.failed > 0) { - log('red', `Failed: ${results.failed}`); - } else { - log('green', `Failed: ${results.failed}`); - } - - const successRate = ((results.passed / results.total) * 100).toFixed(1); - console.log(); - if (results.failed === 0) { - log('green', `✓ All tests passed! (${successRate}%)`); - } else { - log('yellow', `⚠ Some tests failed (${successRate}% success rate)`); - } - console.log(); - - return results; -} - -// Run tests -runTests().catch(error => { - logError(`Fatal error: ${error.message}`); - console.error(error); - process.exit(1); -}); diff --git a/test-voice-commands.js b/test-voice-commands.js deleted file mode 100755 index 7e7b376..0000000 --- a/test-voice-commands.js +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env node - -/** - * Voice Command Testing Script - * Tests the voice classification API with various baby care commands - */ - -const API_URL = process.env.API_URL || 'http://localhost:3020'; -const ENDPOINT = '/api/v1/voice/test-classify'; // Using public test endpoint - -// ANSI color codes -const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', -}; - -// Test commands -const commands = [ - 'Change wet diaper', - 'Baby ate 150ml formula', - 'Baby slept for 1 hour', - 'Alice slept for 30 min', - 'Alice ate 3 pcs of broccoli at 11:00 AM', - 'Dirty diaper change', - 'Fed baby 120ml', - 'Baby napped for 45 minutes', - 'Changed diaper, it was wet', - 'Gave baby vitamin D drops', -]; - -async function testCommand(command, testNum) { - console.log(`${colors.yellow}Test #${testNum}: "${command}"${colors.reset}`); - console.log('---'); - - try { - const response = await fetch(`${API_URL}${ENDPOINT}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - text: command, - language: 'en', - childName: 'Alice', - }), - }); - - const data = await response.json(); - - if (!response.ok || !data.success) { - console.log(`${colors.red}✗ API returned error${colors.reset}`); - console.log(JSON.stringify(data, null, 2)); - console.log(''); - return false; - } - - // Extract classification details - const { type = 'unknown', confidence = 0, details = {}, timestamp = null } = data.classification || {}; - - // Color-code based on type - let typeColor; - switch (type) { - case 'feeding': - typeColor = colors.green; - break; - case 'sleep': - typeColor = colors.blue; - break; - case 'diaper': - typeColor = colors.yellow; - break; - case 'medicine': - typeColor = colors.cyan; - break; - case 'milestone': - typeColor = colors.green; - break; - default: - typeColor = colors.red; - } - - // Display results - console.log(`Type: ${typeColor}${type}${colors.reset}`); - console.log(`Confidence: ${confidence}`); - console.log(`Timestamp: ${timestamp || 'null'}`); - console.log('Details:'); - console.log(JSON.stringify(details, null, 2)); - - // Validate confidence threshold - const passed = type !== 'unknown' && confidence >= 0.3; - - if (passed) { - console.log(`${colors.green}✓ Command successfully classified${colors.reset}\n`); - return true; - } else { - console.log(`${colors.red}✗ Low confidence or unknown type${colors.reset}\n`); - return false; - } - } catch (error) { - console.log(`${colors.red}✗ Request failed: ${error.message}${colors.reset}\n`); - return false; - } -} - -async function runTests() { - console.log(`${colors.blue}========================================${colors.reset}`); - console.log(`${colors.blue}Voice Command Testing Suite${colors.reset}`); - console.log(`${colors.blue}========================================${colors.reset}\n`); - - let passed = 0; - let failed = 0; - - for (let i = 0; i < commands.length; i++) { - const result = await testCommand(commands[i], i + 1); - if (result) { - passed++; - } else { - failed++; - } - } - - // Summary - console.log(`${colors.blue}========================================${colors.reset}`); - console.log(`${colors.blue}Test Summary${colors.reset}`); - console.log(`${colors.blue}========================================${colors.reset}`); - console.log(`Total: ${commands.length}`); - console.log(`${colors.green}Passed: ${passed}${colors.reset}`); - console.log(`${colors.red}Failed: ${failed}${colors.reset}`); - console.log(''); - - if (failed === 0) { - console.log(`${colors.green}All tests passed! 🎉${colors.reset}`); - process.exit(0); - } else { - console.log(`${colors.red}Some tests failed. Check the output above.${colors.reset}`); - process.exit(1); - } -} - -// Run tests -runTests().catch(error => { - console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`); - process.exit(1); -}); diff --git a/test-voice-commands.sh b/test-voice-commands.sh deleted file mode 100755 index 1fbd1c0..0000000 --- a/test-voice-commands.sh +++ /dev/null @@ -1,139 +0,0 @@ -#!/bin/bash - -# Voice Command Testing Script -# Tests the voice classification API with various baby care commands - -API_URL="${API_URL:-http://localhost:3020}" -ENDPOINT="/api/v1/voice/transcribe" - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' # No Color - -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Voice Command Testing Suite${NC}" -echo -e "${BLUE}========================================${NC}\n" - -# Test commands -declare -a commands=( - "Change wet diaper" - "Baby ate 150ml formula" - "Baby slept for 1 hour" - "Alice slept for 30 min" - "Alice ate 3 pcs of broccoli at 11:00 AM" - "Dirty diaper change" - "Fed baby 120ml" - "Baby napped for 45 minutes" - "Changed diaper, it was wet" - "Gave baby vitamin D drops" -) - -# Function to test a command -test_command() { - local command="$1" - local test_num="$2" - - echo -e "${YELLOW}Test #$test_num: \"$command\"${NC}" - echo "---" - - # Make API request - response=$(curl -s -X POST "${API_URL}${ENDPOINT}" \ - -H "Content-Type: application/json" \ - -d "{\"text\":\"$command\",\"language\":\"en\",\"childName\":\"Alice\"}") - - # Check if request was successful - if [ $? -ne 0 ]; then - echo -e "${RED}✗ API request failed${NC}\n" - return 1 - fi - - # Parse response - success=$(echo "$response" | jq -r '.success // false') - - if [ "$success" != "true" ]; then - echo -e "${RED}✗ API returned error${NC}" - echo "$response" | jq '.' - echo "" - return 1 - fi - - # Extract classification details - type=$(echo "$response" | jq -r '.classification.type // "unknown"') - confidence=$(echo "$response" | jq -r '.classification.confidence // 0') - details=$(echo "$response" | jq -r '.classification.details // {}') - timestamp=$(echo "$response" | jq -r '.classification.timestamp // "null"') - - # Color-code based on type - case "$type" in - feeding) - type_color="${GREEN}" - ;; - sleep) - type_color="${BLUE}" - ;; - diaper) - type_color="${YELLOW}" - ;; - medicine) - type_color="${RED}" - ;; - milestone) - type_color="${GREEN}" - ;; - *) - type_color="${RED}" - ;; - esac - - # Display results - echo -e "Type: ${type_color}${type}${NC}" - echo -e "Confidence: ${confidence}" - echo -e "Timestamp: ${timestamp}" - echo "Details:" - echo "$details" | jq '.' - - # Validate confidence threshold - confidence_float=$(echo "$confidence" | awk '{print ($1 >= 0.3) ? "pass" : "fail"}') - - if [ "$type" != "unknown" ] && [ "$confidence_float" == "pass" ]; then - echo -e "${GREEN}✓ Command successfully classified${NC}\n" - return 0 - else - echo -e "${RED}✗ Low confidence or unknown type${NC}\n" - return 1 - fi -} - -# Run all tests -total_tests=${#commands[@]} -passed=0 -failed=0 - -for i in "${!commands[@]}"; do - test_num=$((i + 1)) - if test_command "${commands[$i]}" "$test_num"; then - ((passed++)) - else - ((failed++)) - fi -done - -# Summary -echo -e "${BLUE}========================================${NC}" -echo -e "${BLUE}Test Summary${NC}" -echo -e "${BLUE}========================================${NC}" -echo -e "Total: $total_tests" -echo -e "${GREEN}Passed: $passed${NC}" -echo -e "${RED}Failed: $failed${NC}" -echo "" - -if [ $failed -eq 0 ]; then - echo -e "${GREEN}All tests passed! 🎉${NC}" - exit 0 -else - echo -e "${RED}Some tests failed. Check the output above.${NC}" - exit 1 -fi diff --git a/test-voice-e2e.js b/test-voice-e2e.js deleted file mode 100755 index 29f72d4..0000000 --- a/test-voice-e2e.js +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env node - -/** - * End-to-End Voice Command Test - * Tests the full voice flow: classify + create activity in database - */ - -const API_URL = process.env.API_URL || 'http://localhost:3020'; - -// ANSI color codes -const colors = { - reset: '\x1b[0m', - red: '\x1b[31m', - green: '\x1b[32m', - yellow: '\x1b[33m', - blue: '\x1b[34m', - cyan: '\x1b[36m', -}; - -// Test credentials -const TEST_USER = { - email: 'andrei@cloudz.ro', - password: 'Test1234!', -}; - -// Test commands -const commands = [ - 'Change wet diaper', - 'Baby ate 150ml formula', - 'Baby slept for 1 hour', - 'Alice ate 3 pcs of broccoli at 11:00 AM', -]; - -let accessToken = null; -let childId = null; - -async function login() { - console.log(`${colors.blue}Logging in...${colors.reset}`); - const response = await fetch(`${API_URL}/api/v1/auth/login`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(TEST_USER), - }); - - const data = await response.json(); - if (!response.ok || !data.success) { - throw new Error(`Login failed: ${data.message || 'Unknown error'}`); - } - - accessToken = data.data.accessToken; - console.log(`${colors.green}✓ Logged in successfully${colors.reset}\n`); -} - -async function getChild() { - console.log(`${colors.blue}Fetching children...${colors.reset}`); - const response = await fetch(`${API_URL}/api/v1/children`, { - headers: { 'Authorization': `Bearer ${accessToken}` }, - }); - - const data = await response.json(); - if (!response.ok || !data.success || data.data.length === 0) { - throw new Error('No children found'); - } - - childId = data.data[0].id; - console.log(`${colors.green}✓ Found child: ${data.data[0].name} (${childId})${colors.reset}\n`); -} - -async function getActivitiesCount(type = null) { - let url = `${API_URL}/api/v1/activities?childId=${childId}&limit=1000`; - if (type) url += `&type=${type}`; - - const response = await fetch(url, { - headers: { 'Authorization': `Bearer ${accessToken}` }, - }); - - const data = await response.json(); - return data.success ? data.data.length : 0; -} - -async function classifyAndCreateActivity(text) { - console.log(`${colors.yellow}Processing: "${text}"${colors.reset}`); - - // Step 1: Classify the command - const classifyResponse = await fetch(`${API_URL}/api/v1/voice/transcribe`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - text, - language: 'en', - childName: 'Alice', - }), - }); - - const classifyData = await classifyResponse.json(); - - if (!classifyResponse.ok || !classifyData.success) { - console.log(`${colors.red}✗ Classification failed${colors.reset}`); - console.log(JSON.stringify(classifyData, null, 2)); - return false; - } - - const { type, details, timestamp, confidence } = classifyData.classification; - - console.log(` Type: ${type} (confidence: ${confidence})`); - console.log(` Details: ${JSON.stringify(details)}`); - - if (type === 'unknown' || confidence < 0.3) { - console.log(`${colors.red}✗ Low confidence or unknown type${colors.reset}\n`); - return false; - } - - // Step 2: Create the activity - const activityData = { - type, - timestamp: timestamp || new Date().toISOString(), - data: details || {}, - notes: details?.notes || undefined, - }; - - const createResponse = await fetch(`${API_URL}/api/v1/activities`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${accessToken}`, - }, - body: JSON.stringify({ - childId, - ...activityData, - }), - }); - - const createData = await createResponse.json(); - - if (!createResponse.ok || !createData.success) { - console.log(`${colors.red}✗ Failed to create activity${colors.reset}`); - console.log(JSON.stringify(createData, null, 2)); - return false; - } - - console.log(`${colors.green}✓ Activity created: ${createData.data.id}${colors.reset}\n`); - return true; -} - -async function runTests() { - console.log(`${colors.blue}========================================${colors.reset}`); - console.log(`${colors.blue}Voice E2E Test Suite${colors.reset}`); - console.log(`${colors.blue}========================================${colors.reset}\n`); - - try { - // Login and get child - await login(); - await getChild(); - - // Get initial counts - const initialCounts = { - total: await getActivitiesCount(), - diaper: await getActivitiesCount('diaper'), - feeding: await getActivitiesCount('feeding'), - sleep: await getActivitiesCount('sleep'), - }; - - console.log(`${colors.cyan}Initial activity counts:${colors.reset}`); - console.log(` Total: ${initialCounts.total}`); - console.log(` Diapers: ${initialCounts.diaper}`); - console.log(` Feedings: ${initialCounts.feeding}`); - console.log(` Sleep: ${initialCounts.sleep}\n`); - - // Run tests - let passed = 0; - let failed = 0; - - for (const command of commands) { - const result = await classifyAndCreateActivity(command); - if (result) { - passed++; - } else { - failed++; - } - } - - // Get final counts - const finalCounts = { - total: await getActivitiesCount(), - diaper: await getActivitiesCount('diaper'), - feeding: await getActivitiesCount('feeding'), - sleep: await getActivitiesCount('sleep'), - }; - - console.log(`${colors.cyan}Final activity counts:${colors.reset}`); - console.log(` Total: ${finalCounts.total} (+${finalCounts.total - initialCounts.total})`); - console.log(` Diapers: ${finalCounts.diaper} (+${finalCounts.diaper - initialCounts.diaper})`); - console.log(` Feedings: ${finalCounts.feeding} (+${finalCounts.feeding - initialCounts.feeding})`); - console.log(` Sleep: ${finalCounts.sleep} (+${finalCounts.sleep - initialCounts.sleep})\n`); - - // Summary - console.log(`${colors.blue}========================================${colors.reset}`); - console.log(`${colors.blue}Test Summary${colors.reset}`); - console.log(`${colors.blue}========================================${colors.reset}`); - console.log(`Total: ${commands.length}`); - console.log(`${colors.green}Passed: ${passed}${colors.reset}`); - console.log(`${colors.red}Failed: ${failed}${colors.reset}`); - console.log(''); - - if (failed === 0) { - console.log(`${colors.green}All tests passed! Activities saved to database. 🎉${colors.reset}`); - process.exit(0); - } else { - console.log(`${colors.red}Some tests failed. Check the output above.${colors.reset}`); - process.exit(1); - } - } catch (error) { - console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`); - console.error(error.stack); - process.exit(1); - } -} - -// Run tests -runTests();