GET /api/focus-groups/{id}/report/download generates a markdown report
with AI-written executive summary, key themes with quotes, and full
transcript. Frontend adds "Export Full Report" button to ThemesPanel.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All copy updated: "Session name", "What are you researching?", "Key topics to explore",
"Session length", "Thinking depth", "Response length", "Attach materials (optional)"
- llm_model, reasoning_effort, verbosity moved into collapsible "Advanced settings" accordion
- Word-count indicators now float inside textareas (absolute overlay) instead of below
- Removed duplicate amber text blocks and redundant FormDescriptions
- FocusGroupModerator header: "AI Focus Group Moderator" → "Set Up Your Research Session"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Epic 1 — OG-image & SEO base:
- Replace wrong og-image.png with branded 1200×630 Cohorta design
- index.html: full title, og:type/url/image dimensions, twitter:card, canonical
Epic 2 — Pricing from admin panel:
- Pricing.tsx: remove hardcoded DEFAULT_PACKS; add loading skeleton and error+retry state
- Features list and personas/sessions counts computed from API credits/costs
- billing.py /packs: also returns persona_cost and run_cost for frontend math
- app_settings.py: add popular:True to pro pack default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header: replace banner PNG with Logo SVG mark (44px) + Cohorta wordmark — crisp at all sizes
- ScrollToTop: moved from PublicLayout to App.tsx root — works on all pages,
avoids position:fixed breakage from framer-motion transform ancestors
- ScrollToTop: threshold 100px → appears sooner after scroll
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header: logo h-48px mobile / h-168px desktop, md:self-start overflow
- PublicLayout: main pt-80px to match new header height
- Hero: -mt-80px cancels PublicLayout pt, responsive inner pt
- AppLayout: replace PNG logo (black box) with SVG mark + text
- billing.py: add public GET /billing/packs endpoint
- api.ts + Pricing.tsx: fetch packs from API, fallback to defaults
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move cohorta-banner.png into navbar left slot (height 42px, w-auto)
- Navbar py-2/py-1.5 sized to logo — no left spacer needed
- PublicLayout main: pt-20 → pt-[58px] to match new navbar height
- Hero: mt/pt offsets updated to 58px; content starts 45px below navbar
- Hero: banner block removed (now lives in navbar)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Banner moved outside the grid to full max-w-7xl width — naturally ~240px tall at desktop
- Removed banner from left column (no longer constrained to 50% column width)
- ScrollToTop visibility threshold: 400px → 200px (appears after ~1 screen scroll)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Register form: two required checkboxes (Terms of Service / Privacy Policy + UK GDPR data processing)
- Zod schema uses z.literal(true) — form won't submit until both are checked
- Backend: validates accept_terms + accept_data_processing flags (400 if missing)
- User.save() writes created_at, consent_terms_at, consent_data_processing_at to MongoDB
- Admin UsersTab: Registered column, email verified badge, consent timestamps in edit dialog
- Fix: EU-hosted → UK hosted badge in register form
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Header nav: SVG C-mark only (no PNG box, no dark bg)
- Hero: full logo PNG banner below nav with mix-blend-mode:screen (removes dark bg)
- favicon.svg: updated C-mark colours to match rebrand
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Logo.tsx: C arc + 3 people silhouettes SVG mark matching brand design
- favicon.svg: updated to match new logo mark
- public/avatars/: 8 diverse persona SVGs (skin tones, hair styles, ages)
- Index.tsx: remove billingApi.getBalance() call on public landing page (was causing 401 console errors for anonymous visitors; pricing uses hardcoded defaults)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs caused filters to show 0 and period selector to have no effect:
1. Python < 3.11 can't parse JS toISOString() Z suffix — every request with a
period filter threw ValueError → 500 → frontend received no data. Fixed with
_parse_iso() helper that replaces Z with +00:00 before fromisoformat().
2. 'All time' sends no from/to params, but backend defaulted to _month_start()
instead of omitting the ts filter. Fixed with _period_match() helper that
returns {} (no filter) when both from and to are absent.
Also: stale _user_mtd_cost reference in get_user route replaced with
_user_period_cost(user_id, None, None); adminApi types updated with from/to.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New usePeriod hook (day/week/month/all/custom presets) with from/to ISO string outputs
- New PeriodSelector component (button group + custom date inputs)
- UsersTab, UsageTab, FocusGroupsTab all wired up with period state
- Backend /admin/users and /admin/focus-groups now accept from/to query params
- MTD Cost column header now reflects selected period label (e.g. "Cost (MTD)")
- Logout clears local state only (no account sign-out)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- @active_required + @with_user_context applied to all LLM-invoking routes
in personas.py, focus_group_ai.py, ai_personas.py
- backend/app/routes/usage.py: GET /api/usage/me (MTD summary by feature),
GET /api/usage/focus-groups/<id> (owner or admin)
- Registered usage_bp in app/__init__.py
- llm_service._record_usage now emits usage_update WS event to focus group room
Frontend:
- useMyUsage + useFocusGroupUsage hooks
- MyUsage.tsx: personal billing dashboard (cost cards + per-feature table)
- /billing route (ProtectedRoute) + Billing nav link
- FocusGroupSession: quota_warning amber banner with Progress bar,
quota_exceeded + quota_warning WS events wired via websocketServiceNew
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- token_version in JWT (bump_token_version, get_token_version on User model);
jwt_required checks tv claim → 401 on mismatch; login routes embed version
- Quota pre-flight in all 3 LLM public methods (QuotaExceededError bubbles up)
- AI runner catches QuotaExceededError → sets status paused_quota + emits WS event
- Admin routes: POST /users (create), POST /users/<id>/reset-password,
POST /pricing, GET /focus-groups with aggregated cost; PUT /users/<id>
now bumps token_version on disable or role change
- backfill_usage.py: idempotent estimated-event generator for historical data,
tiktoken for GPT models, char/3.8 for Gemini, --dry-run flag
Frontend:
- 402 interceptor dispatches quota_exceeded CustomEvent
- adminApi: createUser, resetPassword, createPricing, listFocusGroups
- UsersTab: New User dialog + Reset Password in edit dialog
- PricingTab: New Price dialog (model, provider, input/output/cached prices)
- FocusGroupsTab: focus groups table sorted by total cost
- Admin.tsx: 4th tab (Focus Groups)
- FocusGroupSession: admin-only cost badge + dismissable quota exceeded banner
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend: /api/admin/* blueprint with user CRUD (list, get, update,
disable/enable), usage summary aggregation (group by user/model/feature/
day/focus_group), usage event drill-down, and pricing list. Fixed
admin_required decorator (async-safe). Added find_all/count/update
helpers to User model.
Frontend: /admin page (AdminRoute guard, 3 tabs) — Users table with
search/filter/edit dialog, Usage tab with KPI cards + bar chart +
events table, Pricing tab showing active model rows with tier details.
Admin nav link visible only to admin role.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a task completed, the result payload (personas_created, errors_count, etc.)
was discarded instead of being saved to state. AIRecruiter always read
generationState.result as undefined → count = 0 → showed error even when
the backend had successfully created all personas.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The autonomous loop was crashing on every decision with:
TypeError: can't subtract offset-naive and offset-aware datetimes
because MongoDB stores created_at without timezone info but the code
compared it against datetime.now(timezone.utc).
- conversation_context_service: make created_at timezone-aware before
subtraction (replace tzinfo=utc when naive)
- DiscussionPanel: fix sync effect — when server reports AI mode is
inactive, always clear localAiModeActive regardless of its value,
so the "AI is generating..." spinner doesn't get stuck when the
backend fails/stops before the frontend has confirmed AI mode started
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Backend: set status to ai_mode in the route handler before submitting
to AI runner, eliminating the race condition where frontend's immediate
status poll read the old status
- Frontend: replace all raw isAiModeActive prop usages with
effectiveAiModeActive in DiscussionPanel (13 locations) so ReasoningPanel,
status text, loading indicator, and manual/AI controls all reflect the
correct state instantly on Start AI Mode click
- Frontend: add useEffect to sync localAiModeActive back to null once
the parent prop catches up, preventing permanent override after natural
session end
- These fixes also unblock the 3-second AI message polling which was
never activating due to isAiModeActive staying false
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The AI Runner runs on a dedicated background thread with its own asyncio
event loop. When it emitted WebSocket events via sio.emit(), the call
happened on the wrong loop (AI Runner's vs ASGI/Quart's), causing silent
failures — messages were saved to MongoDB but never reached the frontend.
Additionally, the frontend HTTP polling fallback was never enabled when
WebSocket appeared connected, leaving no way to discover missed messages.
- websocket_manager_async.py: store ASGI main loop reference; detect
cross-loop calls in emit_to_focus_group and use run_coroutine_threadsafe
to schedule emits on the correct loop
- __init__.py: register the ASGI event loop with the WebSocket manager
in before_serving hook
- FocusGroupSession.tsx: always poll fetchMessages every 3s during AI mode
as a reliability fallback regardless of WebSocket status
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Data isolation:
- GET /tasks/<id>: verify requesting user owns the task (403 if not)
- DELETE /tasks/<id>: same ownership check
- GET /tasks/status: add @jwt_required()
- GET /personas/<id>: add ownership check (403 if created_by != user)
- GET /focus-groups/<id>: add ownership check
- GET /focus-groups/<id>/messages: add ownership check
- POST/DELETE /focus-groups/<id>/participants: add ownership check
Fix conversation/decision 500:
- Convert POST /conversation/decision to async 202+background (was synchronous LLM → timed out / LLM errors → 500)
- Frontend polls waitForTaskResult for decision result before calling generateResponseAsync
- GET /conversation/insights: return empty insights (200) on LLM error instead of 500
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>