Commit History
-
fix(clients): resolve file import dialog error and SSE 401
CognxSafeTrackFile import (Bug 1): - CrmAIAssistant had its own <input type="file"> that called onFileUpload onChange, which then called .click() on #crm-file-upload — a second programmatic file dialog triggered from an onChange handler. Chrome 126+ blocks this as it lacks a direct "user activation". - Fix: replace the embedded <input> with <label htmlFor="crm-file-upload"> so the click routes directly to FileImporter's input. Remove the now-unused onFileUpload prop entirely. SSE 401 (Bug 2): - EventSource (browser API) cannot send custom headers — the JWT token never reached the server, causing a 401 on every SSE connection. - Fix: pass the token as ?token= query param in the EventSource URL (MainLayout + CrmConversationalDashboard). - Backend: verifyJwt now injects the query param token into the Authorization header before calling jwtVerify(), so no route changes are needed.87dcd87 -
feat(observability): log Meta Graph API version at startup
CognxSafeTrackBoth API and Worker now emit a ✅ info log at boot showing the active META_GRAPH_API_VERSION, or a ⚠️ warn if the version is below v21.0 (which signals a likely-deprecated version). Visible directly in Railway logs without digging into the code.d66ae60 -
fix: upgrade Meta Graph API from deprecated v18/v19 to v22.0
CognxSafeTrackAll hardcoded v18.0 and v19.0 references replaced with a centralized META_GRAPH_API_VERSION env var (default: v22.0). v19.0 was deprecated in February 2026 causing messaging_limit_tier to silently disappear from the /clients daily-limit card. Also improves error visibility: the catch block in fetchMetaStatus now logs the full error instead of swallowing it silently, and a warn is emitted when messaging_limit_tier is absent even on a successful response (signals a missing BUSINESS_MANAGEMENT permission).b25d16e
-
fix: restore Limite Quotidienne and complete admin i18n
CognxSafeTrackFix messaging_limit_tier disappearing in /clients page: - fetchMetaStatus was querying WhatsAppPhoneNumber separately, which is tenant-filtered by Prisma extension — returns null when called from another org's JWT context (super-admin viewing all orgs) - Fix: include phoneNumbers in the Organization query (excluded from tenant filtering) and use org.phoneNumbers[0].id directly i18n — pages fully translated (fr/en/pt): - LoginPage: org ID, email, password, subtitles, button, footer - ResetPasswordPage: all 4 states (request/sent/new/success) - BillingPage: 62 keys — wallet, transactions, chart, AI assistant, credit packs, QUICK_QUESTIONS, TX_LABELS, FEATURE_LABELS - ClientsManagementView (/clients): 20 key groups — status badges, WABA labels, tier/quality info, all modals (billing, credits, create org, personality, WA setup), validation errors, toasts Fix TemplatesPage status badges showing raw key strings: - Add 5 missing keys to all 3 locale files: status_approved, sta…4f90920 -
fix(audit): resolve all remaining technical debt issues
CognxSafeTrackC2 - analytics SQL injection: UNION detection, ALLOWED_TABLES allowlist, remove SQL from error responses to prevent schema leakage H1/H2 - override-feedback: move organizationId validation to top of handler before any DB mutation; scope enrollment.findFirst to org H6 - UserListPage: add pagination controls (Précédent/Suivant, 20 per page) L4 - TrackDaysPage: await load() before closing editor to prevent stale data race L5 - admin.ts: replace getOrganizationId() with req.organizationId at 3 call sites, remove now-unused import M1 - EntityResolver: replace any[] types with proper Prisma payload types (ResolvedUser/Contact/Organization/Enrollment with exact include shapes) L8 - AI command fuzzy org match: return pending_confirmation when org found by partial name match, inject orgId for unambiguous confirmation retrya888244 -
fix(security): resolve critical and high technical debt from audit
CognxSafeTrackCRITICAL: - Exclude passwordHash from enrollment list response (admin.ts) - Add sanitizeOrg() to strip API keys/tokens from all org GET responses - Fix CORS wildcard on SSE stream; use request origin instead - Restrict super-admin routes to SUPER_ADMIN role only (was also allowing ADMIN) - Replace all Zod .parse() with .safeParse() → proper 400s instead of 500s (ai.ts, admin.ts) HIGH: - Increase ExerciseHandler Redis dedup lock from 30s to 300s - Add take:500 to unbounded trainingData.findMany (OOM risk) - Add take limits to message/contact findMany queries - Add Redis lock in EnrollHandler to prevent duplicate enrollments (TOCTOU) - Wrap override-feedback in try/catch; return 404 on P2025 - Use atomic increment for wallet credits (was TOCTOU race condition) - Scope messages/send to requesting org (was cross-org accessible) MEDIUM: - decryptSecrets returns new object instead of mutating argument in-place - GeminiProvider.generateStructuredData now validates response with Zod schema -…a8e18d6 -
chore(scripts): clean up copy-wa-config utility script
CognxSafeTrack338c360 -
feat(super-admin): WhatsApp OTP registration and template creation
CognxSafeTrack- WhatsApp service: add registerPhoneNumber() and verifyPhoneNumber() methods - Super-admin routes: POST /whatsapp/numbers/register and /verify for Meta OTP flow - Super-admin routes: POST /whatsapp/templates to submit templates to Meta for approval - WhatsAppNumbers page: 2-step modal (orgId + phoneNumberId + PIN → OTP code) - WhatsAppTemplates page: full creation modal with category cards, char counters, live preview bubble170eb5d -
feat(super-admin): org delete, user password reset, complete CRUD coverage
CognxSafeTrackBackend: - DELETE /v1/super-admin/organizations/:id — soft-delete via subscriptionStatus='DELETED' + isHardStopped=true - POST /v1/super-admin/users/:id/reset-password — generates 1h JWT reset token, emails link via scheduleEmail (same flow as /forgot-password) Frontend: - OrganizationsManager: red trash icon per row calls DELETE; confirmation dialog before destructive action; suspend icon color changed to amber - UsersManager: key icon per row calls reset-password endpoint; guards against users with no email addressb8629ec -
feat: complete super-admin interface + enforce Day 11 photo requirement
CognxSafeTrackSuper-admin (3 missing pages + 3 new backend routes): - AuditLogs page: date-range / action filter, paginated table with inline JSON detail expansion - WhatsAppTemplates page: org directory with lazy per-org template fetch; category/status badges; expandable template panel per row - WhatsAppProfiles page: card grid showing org branding (logo, color, WABA ID, token age); inline edit form with live logo preview and color picker; PATCH /v1/super-admin/whatsapp/profiles/:id - Backend: GET /audit-logs, GET/PATCH /whatsapp/profiles, GET /whatsapp/templates added to super-admin.ts - SuperAdminLayout: 10 nav items (added Templates WA, Profils WA, Audit Logs); SuperAdminRouter wired to all new routes Day 11 photo enforcement (tasks/todo.md item resolved): - FeedbackHandler: if currentDay === 11 and no imageUrl, send multilingual photo prompt and return early — exercise cannot be validated without a team member photo8f2286f -
fix(super-admin): move routes to JWT-only scope to bypass tenant extension
CognxSafeTrackSuper-admin routes need cross-org Prisma access. The previous registration inside the tenant-guarded scope called runWithTenant('xamle-admin-org'), which caused the Prisma extension to silently filter all User, Message, WalletTransaction and other queries to a single org. Moving to a separate scope with JWT-only auth (no runWithTenant) means getOrganizationId() returns null inside these handlers, so the tenant extension skips filtering — enabling true cross-org stats and queries.ea8815c -
feat: add XAMLÉ Platform Admin super-admin interface
CognxSafeTrackWhen selectedOrgId === 'xamle-admin-org', the admin switches to a full platform management dashboard instead of the normal org layout. Backend (apps/api): - New /v1/super-admin/* routes (SUPER_ADMIN/ADMIN role required): platform stats, organizations CRUD + suspend + impersonate, cross-org users + role change, WhatsApp numbers, billing transactions + manual credits, monitoring health, AI agentic command - Fixed SQL injection in analytics text-to-SQL (UNION/DROP/comment blocking) - Removed JWT secret fallback — crashes on startup if JWT_SECRET is missing - Added ORG_ADMIN role guard to all /v1/admin/* routes Frontend (apps/admin): - SuperAdminRouter: nested routes under /platform/* - SuperAdminLayout: dark sidebar (slate-950/violet), 7 nav items - PlatformDashboard: 6 KPI cards, system health, active alerts - OrganizationsManager: searchable table, suspend/edit drawer, create modal - UsersManager: cross-org table with inline role-change - WhatsAppNumbers: all registered numbers …6282d86 -
feat(whatsapp-setup): make token optional when org already has one stored
CognxSafeTrackIf an org already has a systemUserToken, the /whatsapp-setup route now reuses it automatically. Admins can reconfigure WABA ID + Phone Number ID without re-entering the token — useful for adding a new phone number to an existing connected account.de46926 -
feat(settings): add WhatsApp reconfiguration form to SettingsPage
CognxSafeTrackBusiness owners can now connect or reconnect their WhatsApp account directly from Settings without going through the onboarding wizard. Form accepts WABA ID, System User Token, Phone Number ID, and phone number.ec8f103 -
fix(crm-agent): replace generateText+JSON.parse with generateStructuredData for intent detection
CognxSafeTrackThe CRM command agent was always falling back to GENERAL_CHAT and showing the menu again because LLMs wrap JSON in markdown fences, breaking JSON.parse. Switch to generateStructuredData with a Zod schema which guarantees structured output regardless of LLM quirks. Also exposes generateStructuredData as a public method on AIService.3b3d6cd -
fix(audit): resolve remaining TypeScript any types, string enums, and Json validation
CognxSafeTrack +1: Claude Sonnet 4.6TypeScript types (ai-sdk): - Define AIServicePrismaClient structural interface — replaces prisma: any - Define TenantSecrets interface — replaces getTenantSecrets: () => Promise<any> - Define BusinessProfileContext interface — replaces businessProfile?: any in 3 methods - Define BusinessProfileExtraction interface — extractBusinessProfile now fully typed - Define ContactData interface — replaces contact: any in generateCrmCampaign - exerciseCriteria? typed as Record<string, unknown> instead of any - searchResults now typed as SearchResult[] instead of any[] DB enums: - CampaignHistory.status: String → CampaignStatus enum (SENT | DELIVERED | READ | FAILED) - AnalyticsLog.eventType: String → AnalyticsEventType enum (CLICK | READ | RESPONSE) - Migration 20260514000001 applied to production DB Zod validation: - TrackDaySchema: add exerciseCriteria (z.record) and badges (z.array(z.string)) - ai.ts /crm/generate-campaign: enforce contact.phoneNumber (required) via Zod Runtime safety: - wh…2a3e2a1 -
fix(audit): resolve remaining minor technical debt issues
CognxSafeTrack +1: Claude Sonnet 4.6AI/BYOK: - generateText now respects tenant BYOK keys via getProvidersForTenant() (was using global registry) - generateText now applies exponential backoff retry via AIService.withRetry() Backend: - notifications.ts: return 503 instead of 200+undefined when VAPID_PUBLIC_KEY absent - BillingPage: support WA number now reads from VITE_SUPPORT_WA_NUMBER env var (was hardcoded fake number) - KnowledgeBasePage: show toast.error on delete failure (was silently swallowed) DB: - Add @@index([organizationId, exerciseStatus]) on UserProgress for live-feed query performance - Add @@index([createdAt]) on AuditLog for time-range admin queries - Migration 20260513000005 applied to production DB i18n: - Add knowledge.delete_error translation key to all 4 locales (fr, en, es, pt) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>d74c1b3
-
fix(audit): resolve 15 critical and major technical debt issues
CognxSafeTrack +1: Claude Sonnet 4.6Security: - campaigns.ts: prevent cross-tenant write by using req.organizationId instead of URL param - billing.ts: restrict org mode change to SUPER_ADMIN only (was incorrectly allowing ADMIN) - CommandHandler: guard SEED command with NODE_ENV !== 'production' - search.ts: return [] instead of fabricated mock data when SERPER_API_KEY absent - scheduler: filter out hard-stopped organizations from nudge and lesson delivery - payments.ts: HMAC signature verification for Wave and Orange Money webhooks + fix track.priceAmount Data integrity: - schema: TrackDay.dayNumber Float → Int, Enrollment.currentDay Float → Int - schema: add @@unique([userId, trackId]) to Enrollment to prevent race condition double-enrollment - Migration 20260513000004 applied to production DB Performance: - admin.ts /live-feed: fix N+1 queries with batch findMany + in-memory map - organizations.ts /:id/messages: add pagination (was unbounded, OOM risk) - crm-service.ts listMessages: add skip/limit/total support Co…97f541f -
chore: remove Stripe — replaced by Orange Money / Wave payment gateway
CognxSafeTrack +1: Claude Sonnet 4.6Delete stripe.ts route file, remove import and /v1/stripe registration from app.ts, drop stripe npm dependency, remove deprecated stripeWebhookRoute stub from payments.ts, and remove stripeCustomerId from admin Organization interface. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>641e72b -
feat: complete agentic audit roadmap — retry, KB generate, real costs, traceId, rate-limit
CognxSafeTrack +1: Claude Sonnet 4.6- ai-sdk: exponential backoff retry (3 attempts, 1s→2s) in callWithFailover; skips 4xx and QuotaExceededError - api/whatsapp: traceId (crypto.randomUUID) propagated to worker bridge via x-trace-id header - api/whatsapp: route-level rate limit 500/min on POST /webhook, returns 200 on rejection - api/organizations: POST /:id/kb/generate — GPT-4o-mini JSON mode generates FAQ pairs, embeds via IndexingService - api/indexing-service: new service batching OpenAI text-embedding-3-small + pgvector INSERT - admin/AnalyticsPage: real cost from UsageEvent aggregate + byFeature breakdown table - admin/KnowledgeBasePage: KB auto-generation panel (description textarea, generate button, FAQ preview) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>b438786 -
fix: security hardening, real AI costs, traceId propagation & audit logging
CognxSafeTrack +1: Claude Sonnet 4.6- S1: enforceOrgIsolation — organizationId always from JWT for non-SUPER_ADMIN (header ignored) - S4: auditService.log() on ORG_MODE_CHANGED, ORG_PERSONALITY_UPDATED, ORG_BYOK_KEYS_UPDATED - O1: traceId (UUID) generated at webhook, forwarded via x-trace-id header, injected into BullMQ job data, threaded through whatsapp-logic handler chain - D3: /v1/analytics/usage now aggregates real UsageEvent.costUsd + tokens (tokensIn/Out) with per-feature breakdown instead of token estimates - D4: BrandingDataSchema added to shared-types — brandingData strictly typed (logoUrl, primaryColor, orgName) instead of z.any() - A6: InboundHandler fetches user.language in parallel with org token, passes it to Whisper - A2: sendLessonDay() caches personalized lesson text in Redis 6h by (trackDayId, activity, language) - P1: sendLessonDay() parallelized — user+trackDay batched in one include, userProgress fetched in parallel - D2: ExerciseHandler Redis 30s dedup lock prevents double exercise submission Co-A…87afdf1 -
feat: agentic platform — text-to-sql, pedagogy advisor, security hardening & performance
CognxSafeTrack +1: Claude Sonnet 4.6- Analytics: POST /v1/analytics/query — natural language → GPT-4o-mini → SELECT → results (Text-to-SQL) - Analytics UI: example chips, SQL results table, 'Voir SQL' toggle - Billing: role guard on change_organization_mode (ADMIN+ only) - Nav: /ai-setup accessible to all org members (not just super admin) - Scheduler: startPedagogyAdvisor() — Wednesday 08:30 UTC cron, at-risk nudges (5 languages) + email report - S1 security: organizationId always from JWT for non-super-admin (never from header) - A6: InboundHandler passes user.language to Whisper transcription (Wolof hint: 'fr') - A2: Redis 6h cache for personalized lessons keyed by (trackDayId, activity, language) - P1: sendLessonDay() parallelized from 4 sequential DB queries to 2 (user+trackDay batch, userProgress parallel) - D2: Redis 30s dedup lock on exercise submission to prevent double-submit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>5b8761d -
feat: Claude Sonnet 4.6 provider + Stripe billing automation
CognxSafeTrack +1: Claude Sonnet 4.6AI: - packages/ai-sdk: ClaudeProvider implements LLMProvider via tool_use structured outputs + zod-to-json-schema - Claude registered at priority 120 (TEXT only), supersedes Gemini for feedback/lessons/chat — -40% cost, better French - ANTHROPIC_API_KEY added to API and worker env validation Stripe: - apps/api/src/routes/stripe.ts: POST /v1/stripe/webhook handles checkout.session.completed, subscription.updated/deleted, invoice.payment_failed/succeeded - Schema: stripeCustomerId + stripeSubscriptionId added to Organization - tenant.ts: 402 gate for isHardStopped orgs (x-api-key calls always pass) - ClientsManagementView: SUSPENDU badge on hard-stopped orgs - STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET added to config Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>4a0c3ba -
feat(crm-ux): tag management, unread badge, handoff release UI
CognxSafeTrack +1: Claude Sonnet 4.6- ContactsPage: tag filter chips, per-contact tag editing with inline input, add/remove tags via PATCH API, colSpan fix for new Tags column - MainLayout: SSE-powered unread message badge on conversations nav item, resets on navigation - UserListPage: soft-delete button, handoff status fetch + "Libérer l'IA" button in conversation modal - admin routes: DELETE /users/:id (soft-delete), GET/DELETE /users/:id/handoff endpoints Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>42a2598 -
fix(org-resolve): support legacy non-UUID custom IDs (e.g. default-org-id) + analytics orgId header
CognxSafeTrack +1: Claude Sonnet 4.6- resolveOrgId now uses findFirst with OR [slug, id] so literal custom IDs like "default-org-id" (the seed org XAMLÉ Global) are resolved correctly instead of returning 404 - ContactsPage.tsx: pass selectedOrgId to analytics/usage call — was missing the x-organization-id header, causing systematic 400 on the contacts page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>2cc44c6 -
feat(agentic-week2-4): 5 major features — AI content generator, handoff, tags, soft-delete, SSE
CognxSafeTrack +1: Claude Sonnet 4.6## Agent Créateur de Contenu (Semaine 3) - AIService.generateCurriculum() — structured GPT-4o prompt generates full Track + TrackDays - POST /v1/organizations/:id/content/ai-generate — persists curriculum directly to DB - TrackListPage: "Générer avec IA" button + modal (description, numDays, language, audience) - Auto-redirects to the generated program days after creation ## Human Handoff Detection (Semaine 4) - AIAgentHandler: detects 15 multilingual handoff keywords (FR/EN/ES) - Sets Redis flag handoff:{userId}:{orgId} (24h) — pauses AI responses - Emails org admin with contact info + link to admin UI - Handles subsequent messages while in handoff mode gracefully ## Contact Segmentation by Tags (Semaine 2) - Contact.tags String[] added to Prisma schema (migration + db:push applied) - GET /contacts/tags — list all unique tags in org - PATCH /contacts/:id/tags — set tags on a contact - GET /contacts?tags=tag1,tag2 — filter contacts by tags (hasSome) ## SSE Real-time Updates (Semaine…99f6bca -
fix(conversations): resolve /conversations blank page + slug→UUID KB routes + WABA banner
CognxSafeTrack +1: Claude Sonnet 4.6- Add missing /conversations route to App.tsx (was nav-linked but unregistered) - Apply resolveOrgId() to all 5 KB routes so slug-based org IDs (e.g. xamle-admin-org) no longer cause 404/400 - TemplatesPage: detect WABA-not-configured 400 and show amber banner instead of silently failing - KnowledgeBasePage: show toast on index-kb 400 (no KB URL) with actionable message Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>9f72dd7 -
feat(agentic-week1): conversational memory, RAG threshold, wallet alerts, weekly reports, campaign scheduling
CognxSafeTrack +1: Claude Sonnet 4.6- AIAgentHandler: Redis sliding window (20 entries, TTL 24h) for conversation history injected into system prompt — AI_AGENT now remembers context - IndexingService.searchRelevantContext: cosine threshold 0.70 — returns '' if no chunk is relevant so agent responds honestly instead of hallucinating - add-hnsw-index.ts: one-shot HNSW index script (m=16, ef=64) for ~10x faster pgvector cosine search on large knowledge bases - scheduler.ts: hourly wallet alert (email via Brevo if < 3 days runway, 6h Redis suppression) + weekly report every Monday 07:00 UTC with trend vs previous week and color-coded wallet status - queue.ts + campaigns.ts: sendAt ISO-8601 parameter on broadcast/campaign routes — BullMQ delay option schedules jobs natively, no cron needed - docs/agentic/: roadmap updated with Semaine 1 marked complete Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>98240fd -
feat(admin-chat): make AI copilot a full super-admin via platform knowledge + agentic tools
CognxSafeTrack +1: Claude Sonnet 4.6- Inject PLATFORM_KNOWLEDGE constant into all /chat system prompts: detailed descriptions of all 4 modes (EDTECH/CRM_MARKETING/AI_AGENT/CUSTOMER_SERVICE), plans & credit pricing, WhatsApp setup (WABA ID/Business ID/token), template rules (variables, MARKETING vs UTILITY), KB indexing, wallet/hard-stop logic - Remove "contactez le support" fallback — AI now answers all platform questions - Add OpenAI function-calling tools for agentic admin actions: - get_organization_settings: read full org config - change_organization_mode: switch org mode (EDTECH/CRM_MARKETING/AI_AGENT/CUSTOMER_SERVICE) - update_ai_agent_personality: update coreMission, tone, temperature - Implement two-pass completion loop: tool calls executed against Prisma, results fed back for final natural-language answer with actionPerformed metadata Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>9061927 -
feat(admin): UX non-tech complète + i18n 4 langues
CognxSafeTrack +1: Claude Sonnet 4.6- Phases 1-5 : masquage JSON avancé, chat IA global (AdminChat), onboarding WhatsApp guidé, templates assistés IA, agent IA setup guidé - i18n exhaustif : ~70 nouvelles clés en FR/EN/ES/PT pour tous les composants ajoutés (AdminChat, AIAgentSetup, SettingsPage, TemplatesPage, OnboardingWizard) - Chat IA multilingue : l'assistant répond désormais dans la langue de l'utilisateur (FR/EN/ES/PT) sur toutes les pages Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>66ff7a1 -
feat(billing): refonte UX non-tech + enrichissement contexte IA
CognxSafeTrack +1: Claude Sonnet 4.6Frontend (BillingPage): - Renommage "Wallet" → "Crédits disponibles" (langage non-technique) - Badge projection dynamique "À ce rythme : encore ~X jours" calculé depuis les transactions des 7 derniers jours - Questions suggérées cliquables (quick-reply chips) avant le 1er message - Suppression "Tokens entrants/sortants" (jargon incompréhensible) - Transactions filtrées : BYOK masqué, layout liste simple vs tableau - Section "Où vont vos crédits IA ?" remplace le jargon feature/type - Indicateur de frappe animé (3 points) dans le chat - Packs recharge avec explication "≈ N messages IA" et badge populaire - Texte WhatsApp clarifié : "Gratuit pour vous — Meta facture directement" Backend (billing.ts /chat): - Wallet balance + isHardStopped ajoutés dans le contexte IA → l'assistant peut maintenant répondre "combien de crédits me reste-t-il" - Burn rate (débit moyen 7j) calculé et injecté : "À ce rythme, votre solde durera encore X jours" - Statut wallet (SUSPENDU / bas / actif) expl…f5ea14d -
refactor(worker): Sprint 2 — dead code removal and queue consolidation
CognxSafeTrack +1: Claude Sonnet 4.6H1: Delete credit-guard.ts (worker) and credit-cache.ts (api) — both fully superseded by wallet.ts/requireCredits.ts since the wallet migration. H6: Introduce lib/queues.ts with shared whatsappQueue and notificationQueue instances. Replace all 10 inline `new Queue(...)` calls across wallet.ts, usage-tracker.ts, whatsapp-logic.ts, scheduler.ts, and 4 handlers. Removes 10 redundant Redis connections; drops `q.close()` calls on ephemeral instances that were killing the shared connection. Shutdown handler now explicitly closes both shared queues. Scratch: Move apps/api/scratch/ one-shot DB scripts to scripts/db-ops/ so they don't ship in the production API bundle. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>37fb9ce -
fix(security): Sprint 1 — five critical debt fixes
CognxSafeTrack +1: Claude Sonnet 4.6C1: Await debitWallet() in usage-tracker and broadcast/campaign handlers so billing failures are caught and logged, not silently swallowed. C2+C3: Rewrite debitWallet() as a single Prisma interactive transaction: balanceAfter now set atomically; isHardStopped flag set in the same transaction as the balance decrement — no window between debit and stop. C4: Add X-Hub-Signature-256 HMAC-SHA256 verification to POST /webhook. Uses preParsing hook for raw body capture; fails open when WHATSAPP_APP_SECRET is not configured. H3: Fix payment webhook race condition — use updateMany(where: PENDING) as atomic idempotency guard; only the request that flips PENDING→COMPLETED proceeds to enrollment creation. H7: Replace $queryRawUnsafe/$executeRawUnsafe with $queryRaw/$executeRaw Prisma.sql template literals in indexing.ts; user-controlled values (organizationId, limit) are properly parametrized. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>fae3e96 -
feat(billing): close 3 remaining wallet gaps from implementation plan
CognxSafeTrack +1: Claude Sonnet 4.6⑫ sendTextMessage() wallet debit coverage: - Add organizationId?: string to sendTextMessage() config — calls trackWhatsAppSent() automatically after each successful send - NudgeHandler, ContentHandler, EnrollHandler, MediaHandler, AdminHandler: pass organizationId in tenantConfig so every outbound message triggers the debit without touching each callsite individually ⑯ GET /billing/summary now includes wallet block: - wallet: { balance, isHardStopped, creditToFcfa, balanceFcfa } appended to every summary response ⑱ BillingPage: wallet transaction table: - Table showing last 20 transactions (date, type, description, amount, balance after) - Type labels with emoji (recharge vs debit), BYOK badge, red/green amount colouring - Inserted between feature breakdown and chat widget Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>f5126bc -
feat(billing): implement full wallet/ledger system with hard-stop enforcement
CognxSafeTrack +1: Claude Sonnet 4.6- Add walletBalance + isHardStopped to Organization schema; WalletTransaction ledger model with full audit trail - wallet.ts (worker): debitWallet (atomic $transaction), creditWallet (with AuditLog), checkWalletBalance (Redis 30s cache), alert queuing (low/critical/exhausted/restored) - wallet.ts (api): creditWallet for admin top-up with cache invalidation - usage-tracker.ts: debitWallet integrated into trackAiUsage, trackAudioUsage, trackWhatsAppSent; BYOK orgs skipped - BroadcastHandler + CampaignHandler: bulk debit after send (BROADCAST_PER_USER per recipient) - requireCredits.ts: migrated from aiCreditsUsed/Limit to walletBalance/isHardStopped; shares Redis cache key with worker - whatsapp-logic.ts: replaced assertCredits/CreditExhaustedError with checkWalletBalance/WalletExhaustedError - ai.ts: added requireCredits preHandler to all 11 AI routes (was 4) - billing.ts: /admin/allocate now calls creditWallet(); new GET /wallet route (balance + 20 last txns) - BillingPage: wallet card…0fd3320 -
feat(billing): enforce AI credit limits across all surfaces
CognxSafeTrack +1: Claude Sonnet 4.6- credit-guard.ts (worker): assertCredits() blocks AI when creditsUsed >= creditsLimit; Redis cache 60s prevents DB hit on every message - whatsapp-logic.ts: credit guard injected before handler chain; RECHARGE keyword bypasses the guard so users can always request a payment link - CommandHandler: new RECHARGE keyword sends payment URL or contact message - i18n: credit_exhausted + recharge_link + recharge_contact in 5 languages - requireCredits middleware (api): 402 on personalize-lesson, tts, generate-feedback, crm/generate-campaign - billing route: POST /v1/billing/admin/allocate (SUPER_ADMIN only) supports set/add actions + optional usage reset + Redis cache invalidation - ClientsManagementView: "Crédits IA" button (SUPER_ADMIN only) per org opens allocation modal with set/add/reset controls - BillingPage: red exhaustion banner when creditsUsed >= creditsLimit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>6484585
-
feat(billing): complete billing system, push notifications, and tech debt fixes
CognxSafeTrack +1: Claude Sonnet 4.6- Usage tracking: instrument pedagogy, audio transcription, AI Agent mode (was silently untracked) - EmailHandler: implement templates 2 (password reset), 3 (quota alert), 4 (monthly report) - Quota alert: auto-queue email + push at 85% threshold via maybeQueueQuotaAlert - Push notifications: PushHandler + send-push BullMQ job; sendPushToUser scoped by organizationId to prevent cross-tenant reads - BillingPage: add credit recharge modal with 3 packs, null guard for missing orgId - i18n: add nav.billing key in fr/en/es/pt locales - Tests: billing.test.ts (10 tests), payments.test.ts (13 tests) - Tech debt: fix empty catch blocks in ai-pedagogy.ts, exclude usage tokens from CRM API response, pass organizationId to generateChat in AIAgentHandler - Docs: add backlog and business model to docs/business/ Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>8280d7d
-
fix(api): secure storage of ai api keys by encrypting them
CognxSafeTrackcec25aa -
fix(audit): resolve technical debt + UX issues from 2026-05-11 audit
CognxSafeTrack +1: Claude Sonnet 4.6Backend: - Add missing DB indexes on Response.userId and Response.enrollmentId - Add 15s timeout with fallback on AI personalization in pedagogy.ts - Fix race condition in NavigationHandler with atomic $transaction - Fix OnboardingHandler hard reset: use FK cascades, 5-lang confirmation - Change seedDatabase to static import in CommandHandler - Add proper pagination to /enrollments admin endpoint UX/Admin: - Fix responsive grid on dashboard stats (1col mobile → 5col desktop) - Show filename + status label during file upload in ContactsPage - Add success/error toast with import summary on contact CSV/XLSX upload - Add inline form validation with field-level errors in ClientsManagementView - Wire up Copy button on AI-generated messages in ContactsPage Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>9fe2cc7 -
chore: cleanup old scripts and sync files
CognxSafeTrack4a46753 -
refactor(debt): resolve all 10 technical debt items from audit
CognxSafeTrack +1: Claude Sonnet 4.6- Redis: singleton pattern (lib/redis.ts) in api + worker, removes 16+ new Redis() calls - Meta token: systemUserTokenIssuedAt field + weekly cron monitor (warn @50d, critical @55d) - Rate limiting: @fastify/rate-limit global 200/min, login 10/min, forgot-password 3/min - Dead code: delete WhatsAppConnectButton.tsx, archive one-off scripts - Silent catches: logger.warn(err) on all backend catch blocks, console.warn on frontend - Meta API types: 5 typed interfaces replace 6 as any casts in organization.ts - Frontend logger: lib/logger.ts wraps Sentry (active when VITE_SENTRY_DSN is set) - Migration: add systemUserTokenIssuedAt + metaBusinessId to Organization schema Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>a966957
-
fix(prisma): add migration file for metaBusinessId + remove as-any casts
CognxSafeTrack +1: Claude Sonnet 4.6Creates the proper migration SQL for the metaBusinessId column that was applied directly via ALTER TABLE. Removes (org as any) casts now that the Prisma client is regenerated with the correct types. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>53e72e6 -
feat(meta-status): sync daily limit + quality rating live from Meta phone number API
CognxSafeTrack +1: Claude Sonnet 4.6fetchMetaStatus now calls GET /{phoneNumberId}?fields=messaging_limit_tier,quality_rating to get the real tier from Meta (TIER_1K, TIER_10K, TIER_100K, UNLIMITED) instead of a hardcoded value inferred from WABA status. Frontend: TIER_LABELS map for human-readable labels, QualityDot badge (green/amber/red) displayed next to the limit. dailyLimitLabel now uses the real tier from the API. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>8aa43f4 -
feat(meta-status): store metaBusinessId for reliable business verification
CognxSafeTrack +1: Claude Sonnet 4.6Add optional `metaBusinessId` field to Organization (schema + shared-types). fetchMetaStatus now queries /{businessId}?fields=verification_status directly when metaBusinessId is stored — bypasses WABA edge permission restrictions that block test WABAs and certain system user token scopes. Direct setup form gains a "Business ID Meta" input that saves via PUT /organizations/:id. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>70a5a84 -
fix(meta-status): add on_behalf_of_business_info fallback for business verification
CognxSafeTrack +1: Claude Sonnet 4.6When the `business` edge on WABA returns nothing (system user token scope or test WABA restrictions), fall back to `on_behalf_of_business_info` + direct business node query for verification_status. Covers verified businesses whose WABA doesn't expose the business edge directly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>bde43ec -
fix(meta-status): split combined Meta API call + fix business badge for test WABAs
CognxSafeTrack +1: Claude Sonnet 4.6Split `account_review_status,business{...}` into two independent calls so test WABAs (which forbid the `business` field) no longer poison WABA status. Business verification is now optional/non-fatal (wrapped in try/catch). BusinessBadge and BusinessVerificationCell now show "—" with neutral styling when `businessId` is undefined (field inaccessible) instead of misleading "Non vérifiée" amber warning + "Vérifier →" link. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>7e2166c
-
feat(whatsapp-setup): direct config mode for platform-owner accounts
CognxSafeTrack +1: Claude Sonnet 4.6Backend: - accessToken now optional in /whatsapp-setup — falls back to WHATSAPP_ACCESS_TOKEN env var (for platform-owner self-setup) Frontend: - "Reconfigurer WhatsApp" button now opens a choice modal instead of launching Embedded Signup directly - Option A (Direct): enter WABA ID + optional token — for accounts already configured on Meta (token from env var used if field left empty) - Option B (Embedded Signup): Facebook login popup — for new client accounts - Phone Number ID pre-filled from existing DB record - Auto-refreshes Meta status after successful save Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>2f67731 -
chore: force redeploy v1.1.2
CognxSafeTrack3b4544a -
feat(meta): live Meta verification status + WhatsApp setup fixes
CognxSafeTrack +1: Claude Sonnet 4.6- GET /v1/organizations/:id/meta-status — fetches WABA account_review_status and business verification_status live from Meta Graph API, cached 1h in Redis - POST /v1/organizations/:id/meta-status/refresh — force-invalidates the cache - ClientsManagementView: replaced static metaVerificationStatus field with live per-org Meta status (WABA badge, Business badge, derived daily limit) - launchEmbeddedSignup: fixed race condition — now waits for both the OAuth code (FB.login) and WABA/phone IDs (WA_EMBEDDED_SIGNUP_EVENT) before resolving - /whatsapp-setup: exchanges OAuth code for long-lived Meta token (60 days) before storing; bypasses exchange if token already starts with EAA - OnboardingWizard: removed broken WhatsApp step (3 steps: welcome, legal, AI) - OrganizationSchema: removed non-existent DB fields (contractSigned, contractSignedAt, contractSignerName, metaVerificationStatus, dailyMessageLimit) that caused prisma.organization.create() to throw Unknown arg at runtime Re…bd9eb5b -
fix(webhook): forward to localhost:8082 worker bridge, not RAILWAY_INTERNAL_URL
CognxSafeTrack +1: Claude Sonnet 4.6RAILWAY_INTERNAL_URL points to Railway's public URL (port 443 → API on 7860), not the Worker bridge on 8082. Since both HF Space and Railway run the Worker bridge locally in the same container, forward to http://localhost:8082 directly. The bridge owns org resolution, WEBHOOK mode handling, and all Redis queue ops. Remove now-unused whatsappQueue and extractWhatsAppPayload imports from API route. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>c12937b