Commit graph

116 commits

Author SHA1 Message Date
Vadym Samoilenko
c25d2423a0 fix(enrichment): handle ObjectId/datetime in persona dict before LLM call
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 15:13:38 +01:00
Vadym Samoilenko
604fdb9458 feat(marketplace): full persona page + categories + enrichment script
- GET /api/marketplace/personas/:id — returns all fields of a published persona
- Expand _COPY_FIELDS to include all new schema fields (oceanTraits, scenarios, aiSynthesizedBio, etc.)
- New MarketplacePersonaView page (/marketplace/personas/:id) with full sidebar + tabs
- Preview button navigates to full page instead of opening a summary dialog
- Clicking card body also navigates to persona page
- scripts/enrich_old_personas.py — LLM enrichment for 12 pre-schema personas

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:38:42 +01:00
Vadym Samoilenko
a8b0a40e08 fix(personas): expand PERSONA_ALLOWED_FIELDS + add library/clone endpoints
Port fixes from Semblance:
- PERSONA_ALLOWED_FIELDS: 9 → 62 fields — oceanTraits, frustrations,
  motivations, scenarios, aiSynthesizedBio, household/lifestyle fields,
  audience_brief, research_objective, etc. were silently discarded at
  Persona.create(), causing all OCEAN traits to render as 50% on frontend
- GET /personas/library — shared library endpoint (all users, bulk user lookup)
- POST /personas/:id/clone — clone any persona to current user
- GET /personas/:id — remove 403 for non-owners (read is public to auth users)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-29 14:12:04 +01:00
Vadym Samoilenko
4c70bc8aa6 fix: PDF layout, credit idempotency, more active discussion
- PDF: meta_row redesigned to same-line two-column layout so values
  don't overflow the right edge; explicit cursor reset before each
  multi_cell body() call fixes key themes appearing to the right of
  titles; page numbers now use len(pdf.pages) + pdf.h-12 positioning
- Credits: replace fragile 4h credit_transactions lookup with atomic
  findOneAndUpdate stamp on the focus group doc itself (24h window),
  with rollback on insufficient balance — eliminates double-charging
  on crash/restart; CreditTransaction.record failure is now non-fatal
- Key themes: cap input at 80 messages + max_output_tokens=4096 to
  fix truncated JSON (Unterminated string at char 1580)
- Decision engine: require ≥4 participant responses per question before
  moving on; mandate debate/contrarian seeking after 2 agreements;
  call all participants to each question before anyone speaks 3rd time

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 21:09:14 +01:00
Vadym Samoilenko
f746dbee8e fix(ai): silent participants + call_out sent as message + contrarian fix
- _apply_silent_participant_override: new Python-level override that forces
  any participant who hasn't spoken yet into the next turn (fires before
  contrarian check, language-aware Russian/English call-out)
- _execute_participant_respond: now sends call_out as a moderator message
  before generating the participant response (was silently skipped before,
  causing 0-action loops and incoherent conversation flow); uses .get()
  instead of [] to avoid KeyError when LLM omits optional fields
- _apply_contrarian_override: language-aware call-out message (Russian if
  last moderator message contains Cyrillic)
- conversation-decision-engine.md: explicit rules that call_out MUST name
  the participant and topic_context MUST give a specific angle; silent
  participants must be called before repeat speakers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:50:50 +01:00
Vadym Samoilenko
8109fe3768 fix(backend): 4 fixes — await add_message, MODEL_ALIASES, language rule, PDF + silent participants
- focus_group_ai.py: add missing `await` on all FocusGroup.add_message() calls
  (was storing coroutine as task result → JSON serialization 500 error)
- model_pricing.py: remove broken MODEL_ALIASES import from llm_service
  (was ImportError on every LLM usage cost tracking call)
- focus-group-response.md: language detection now based on last AI Moderator
  message in conversation, not abstract topic field (fixes mixed-language responses)
- conversation_context_service.py: analytics now shows per-participant message
  counts, explicit ⚠️ SILENT PARTICIPANTS and DOMINANT SPEAKERS warnings so
  decision engine correctly engages quiet members and asks probing questions
- focus_groups.py PDF report: add full transcript section (new page), fix bar
  chart to cap width within page bounds, add page number footers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:46:39 +01:00
Vadym Samoilenko
7dacb86d00 fix(focus-group): reset moderator_position on restart/duplicate + editable participants
- Restart endpoint now $unsets moderator_position, completion_reason,
  autonomous_ended_at, autonomous_started_at so the AI doesn't
  immediately conclude on the next run (was reading stale "guide complete" position)
- Duplicate endpoint excludes same fields from the copy for the same reason
- ParticipantPanel: add isEditable mode with remove buttons and
  searchable "Add Participant" dialog (only active when status=new)
- FocusGroupSession: wire allPersonas state + add/remove handlers,
  pass isEditable={status === 'new'} to ParticipantPanel

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 20:27:50 +01:00
Vadym Samoilenko
c757cdb5ba chore(scripts): seed 7 developer personas for marketplace testing
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:55:37 +01:00
Vadym Samoilenko
e8d2483a84 feat(marketplace): persona marketplace — admin publish + user purchase
- New /api/marketplace/personas GET (list published) + POST /<id>/purchase (deduct credits, copy persona)
- Admin routes: GET /api/admin/marketplace/personas (all originals) + PUT /<id> (price/category/publish)
- Duplicate protection: can't buy same persona twice (409), can't publish purchased copies
- MarketplaceTab.tsx — admin table with inline price/category edit and publish toggle
- PersonaMarketplace.tsx — user-facing grid with search, category filter, buy dialog
- /marketplace route added to App.tsx + nav link in AppLayout
- Translations: en/ru/uk for all marketplace strings
- marketplace field added to PERSONA_ALLOWED_FIELDS

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:49:43 +01:00
Vadym Samoilenko
bbbca5bdf8 chore(billing): add backfill script for amount_usd on old purchase transactions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:14:39 +01:00
Vadym Samoilenko
783a89e825 fix(analytics): correct revenue, persona count, and gross margin
- Store amount_usd in Stripe webhook transaction (was only storing credits)
- Analytics endpoint: sum amount_usd for real USD revenue instead of credits
- Persona count: query personas collection directly (not credit transactions
  which counted transactions, not individual personas — batch creation
  of 3 at once appeared as count=1)
- Frontend: Revenue card now shows USD not credits; Gross Margin uses
  USD revenue so calculation is dimensionally correct

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:10:59 +01:00
Vadym Samoilenko
72e8dadb20 feat(personas): structural improvements for realistic focus group dialogue
- Per-persona message history: each persona now sees their own 8 previous
  responses, preventing repetition and enabling position evolution
- OCEAN archetype labels in decision engine context: instead of raw numbers,
  the decision LLM now sees "agreeableness: 72/100 [HIGH] — consensus-seeker"
- P2P interaction context: when participants interact directly, each one now
  knows who they are responding to and what that person last said
- Python-level contrarian override: when agreement ratio in recent messages
  exceeds 6% and a contrarian persona (low agreeableness or high neuroticism)
  hasn't spoken recently, Python overrides moderator/probe action to call them

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 17:00:01 +01:00
Vadym Samoilenko
e23e52f77d feat(personas): deep OCEAN semantics, missing fields, extraversion + baseline
- Add techSavviness, hasPurchasingPower, hasChildren, description to
  _format_persona_details() — previously ignored despite being in persona model
- Rewrite OCEAN block: each trait now includes its psychological definition,
  full name, and [LOW/MODERATE/HIGH] label so LLM understands the scoring scale
  (e.g. "Agreeableness (cooperation, empathy, conflict avoidance): 25/100 [LOW]")
- Add extraversion behavioral constraint to _generate_behavioral_instructions()
  (was used for response length only, never for personality guidance)
- Add LOW conscientiousness constraint (spontaneous, gut-feel)
- Add BALANCED PERSONALITY baseline for personas with all-moderate OCEAN scores —
  previously these got zero behavioral constraints, behaving generically
- Add optimist/enthusiast personality keyword mapping
- Add OCEAN archetype legend to conversation-decision-engine.md so decision
  engine understands what high/low scores mean when selecting next speaker
- Add +15% Diversity Boost modifier: prefer speakers whose archetype differs
  from the last 2 participants (reduces echo-chamber dynamics)
- Add OCEAN model explanation header to focus-group-response.md prompt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 16:46:56 +01:00
Vadym Samoilenko
64568b6164 feat(personas): improve individuality, add restart/duplicate, fix language
- Extend _format_persona_details() to include background, traits, values,
  interests, communication_style, brandLoyalty, priceConsciousness, demographics
  (race/ethnicity/culture) — previously these fields were stored but never
  sent to the LLM, causing all personas to give generic homogeneous answers
- Add _generate_behavioral_instructions() that translates OCEAN scores into
  explicit behavioral directives (low agreeableness → must push back,
  high neuroticism → voice of doubt, skeptic keyword → find the overpromise)
- Update focus-group-response.md to render BEHAVIORAL CONSTRAINTS and
  DEMOGRAPHIC CONTEXT sections so LLM uses cultural/financial context
- Add anti-groupthink rule to conversation-decision-engine: on convergence,
  prefer a contrarian persona before asking moderator probe question
- Improve ai-moderator-system.md: active @mention pull-in of silent
  participants, spontaneous follow-up probes on interesting responses,
  engagement tracking
- Add LANGUAGE RULE to all three response prompts: always match the language
  of the question (fixes mixed Russian/English responses)
- Add POST /focus-groups/<id>/restart endpoint: clears session collections,
  resets status to 'new', preserves config
- Add POST /focus-groups/<id>/duplicate endpoint: copies config to new group
- Frontend: Duplicate + Restart buttons on focus group cards with confirmation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 16:34:37 +01:00
Vadym Samoilenko
4e90741d75 fix(billing): fix metadata access for Stripe SDK v5 StripeObject
dict(session.metadata) fails with KeyError:0 — metadata is a StripeObject
in SDK v5, not a plain dict. Use getattr() for all metadata fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 14:08:06 +01:00
Vadym Samoilenko
afd44408e0 fix(billing): use Stripe SDK v5 attribute access in webhook handler
stripe.Webhook.construct_event() now returns StripeObject instances that
do not support .get()/__getitem__ — must use attribute access.
event["type"] → event.type, session.get("x") → session.x, etc.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 14:04:58 +01:00
Vadym Samoilenko
bac7e7bce3 fix(usage): hide real costs from users — show credit balance, MTD spend, transaction history
- /api/usage/me now returns credits_balance, credits_spent (MTD debits), and
  last 30 credit transactions; no dollar amounts or token counts exposed
- MyUsage page redesigned: balance + MTD spent cards, transaction history table,
  Buy Credits button
- Admin: POST /api/billing/test-checkout — $1 Stripe Checkout for any credit
  amount to validate the payment flow end-to-end
- stripe_service: cast unit_amount to int (Stripe rejects float)
- i18n updated in EN/RU/UK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:32:28 +01:00
Vadym Samoilenko
d92a099ade feat(ai-config): wire admin UI to LLM service — endpoint/key/model from DB
- _get_runtime_config(): reads active provider endpoint, api_key, main/mini
  model from app_settings (60s cache), falls back to env vars
- get_azure_client() now async, accepts cfg dict
- All generate_* methods call _get_runtime_config() per invocation so DB
  changes take effect without restart
- app_settings: _seed_from_env() backfills empty endpoint/api_key from env
  vars on first load so the admin UI shows current values immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:10:40 +01:00
Vadym Samoilenko
c98f6da6d2 fix(admin): usage report shows email instead of user ID when grouped by user
Batch-resolves user_id strings to emails after the aggregation via a
single find() on the users collection.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 13:01:43 +01:00
Vadym Samoilenko
b74bd7cf99 fix(report): PDF formatting — no empty lines, strip markdown, fix deprecated fpdf2 API
- sanitize_llm(): strips **bold**, ## headers, converts - bullets → •, collapses 3+ newlines
- body(): splits LLM text into paragraphs at \n\n, joins intra-paragraph lines into single
  line so multi_cell never creates blank gaps from double newlines
- quote_block(): collapse newlines in quote text to single space
- multi_cell(0, ...) → multi_cell(EPW, ...) for theme titles
- cell(0, 5, ..., ln=True) → cell(EPW, 5, ..., new_x=LMARGIN, new_y=NEXT)
- Participation analytics: ln=0/True → new_x/new_y API, track x_after_name correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:50:02 +01:00
Vadym Samoilenko
6d2f59fe74 feat(report): PDF export with Cyrillic, i18n, key decisions + analytics
- PDF via fpdf2 (DejaVu font from fonts-dejavu-core, Cyrillic support)
- Lang param (?lang=ru/en) passed from i18n.language in ThemesPanel
- Prompts updated: executive summary + new key-decisions prompt, both
  write entirely in the requested language
- Analytics section: participation bar chart per participant
- Key themes with quotes rendered as side-bordered blocks
- Filename: ASCII slug for Content-Disposition + UTF-8 filename* fallback
- api.ts: handles PDF blob, decodes UTF-8 filename*, 120s timeout
- AnalyticsPanel, ThemeHighlighter: light-mode colors → dark tokens

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:40:54 +01:00
Vadym Samoilenko
0a419eeee1 fix(ui+billing): dark theme for session page + no double-charge on restart
FocusGroupSession.tsx: replace bg-slate-50 with bg-background so the page
matches the rest of the dark UI; swap remaining text-slate-* for muted tokens.

focus_group_ai.py: before deducting 40 cr for an AI mode run, check if this
focus group already has a charge within the last 4 hours. Skip deduction if
found — prevents double-billing when the server restarts mid-session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-25 12:26:21 +01:00
Vadym Samoilenko
3f7f6fb621 fix(backend): get_settings fills missing DEFAULTS keys for stale config documents 2026-05-24 18:08:02 +01:00
Vadym Samoilenko
183a3cb2ff feat: full report export — LLM executive summary + transcript + themes
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>
2026-05-24 15:08:40 +01:00
Vadym Samoilenko
ef7ac32a2b fix: audit — crash fixes, SSRF, API key leak, whitelist merge, stale closure
Frontend:
- SetupTab: restore missing Input import (runtime crash on render)
- SetupTab: wrap onSubmit in event adapter — button outside <form> passed undefined
- Pricing: load() → useCallback, add to useEffect deps (stale closure)
- Pricing: credits_label hardcoded string → t() interpolation

Backend:
- admin/ai-config/test: remove endpoint/api_key from request body (SSRF fix)
  — endpoint and key now read exclusively from DB or env vars
- admin/ai-config/test: str(exc) → sanitized message, full exc logged server-side only
  (prevents API key leak via OpenAI SDK error context)
- admin/ai-config PUT: whitelist allowed fields on provider/model merge
  (prevents injection of arbitrary keys into DB document)
- admin/ai-config PUT: validate active_provider/model are non-empty strings
- admin.py: move deferred imports (time, os, AsyncOpenAI) to module level
- billing.py: guard get_json() with or {} (prevents AttributeError on empty body)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 14:51:56 +01:00
Vadym Samoilenko
d05480610d feat(admin): AI model catalog — provider config, model routing, test connection
- app_settings.py: add ai_providers[], active_provider, active_main/mini_model to DEFAULTS
- admin.py: GET/PUT /api/admin/ai-config (API key masked on read, preserved if not updated)
           POST /api/admin/ai-config/test (latency + connection check)
- AIConfigTab.tsx: provider dropdown, endpoint/key fields, models table with role+enabled toggles,
                   main/mini routing selects, "Test connection" with live latency feedback
- Admin.tsx: add "AI Config" tab

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-24 14:32:23 +01:00
Vadym Samoilenko
d679691cc3 feat: OG image, meta tags, dynamic pricing from admin
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>
2026-05-24 14:11:42 +01:00
Vadym Samoilenko
f4dc3074c0 feat: compass/crosshair SVG decorations across landing page, fix email from noreply→hello
- CompassBg: 8 compass marks scattered across full page height with brand amber
  accent on inner ring, faded crosshair lines, cardinal ticks — z-index 0 (behind content)
- Index.tsx: CompassBg rendered as absolute layer inside relative wrapper
- email_service: EMAIL_FROM default hello@ (noreply triggers spam filters)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 23:06:42 +01:00
Vadym Samoilenko
4e910704bc fix: billing idempotency atomic, logo in navbar, dark bg on app pages, Home btn, resend-verification
- billing: atomic upsert-based idempotency (fixes TOCTOU + crash-between-ops race)
- billing: payment_id uses `or` to handle explicit null payment_intent
- Header: logo h-[44px] contained within navbar frame, remove md overflow
- Header: Home button scrolls to top when already on /
- Header: useMemo limelightItems to prevent useLayoutEffect thrash on scroll
- Header: remove dead scrolled ternary (py-2 : py-2)
- Hero: remove md:pt-[176px] gap (logo no longer overflows)
- LimelightNav: clearTimeout cleanup, remove items from effect deps
- SyntheticUsers/FocusGroups: bg-slate-50 → bg-background (dark theme fix)
- api.ts + Dashboard: resendVerification passes user email (fixes 400 error)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:55:06 +01:00
Vadym Samoilenko
28bb04a2b2 fix: import datetime class in User.save() — NameError blocked all registration
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:47:13 +01:00
Vadym Samoilenko
c0df44a049 fix: responsive navbar logo, AppLayout SVG logo, landing prices from API
- 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>
2026-05-23 22:38:44 +01:00
Vadym Samoilenko
6083a1e53c fix: stripe webhook idempotency + payment_status guard
- Check payment_status == "paid" before granting credits — prevents granting
  credits for unpaid/pending checkout sessions
- Idempotency guard: query credit_transactions for existing ref.stripe_payment_id
  before processing — Stripe retries webhooks on timeout, this prevents double credits

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 22:10:25 +01:00
Vadym Samoilenko
f66d726e07 feat: required Terms + Data Processing consent on register, consent timestamps in admin
- 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>
2026-05-23 22:06:43 +01:00
Vadym Samoilenko
4c307bf00d feat: increase trial credits from 10 to 50 on signup
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:05:13 +01:00
Vadym Samoilenko
a9a5fff659 feat: full visual rebrand + landing redesign + auth page refresh + email fix
- Landing: extract 513-line monolith into 12 focused section components
  (Hero, StatsBand, FeatureGrid, HowItWorks, LivePreview, Comparison,
  UseCases, Testimonials, Pricing, FAQ, FinalCTA, TrustBar)
- Auth pages: replace flat orange panel with animated live mock
  (real persona SVGs, typewriter messages, theme bars); Login label
  fixed to "Email or username"; Register wires ?plan= badge
- Brand: new Logo SVG (C-arc + 3 figures + wordmark/tagline), expanded
  palette tokens, fluid display type scale, framer-motion shared variants
- Header: scroll progress bar, removed non-functional language pill
- Footer: fixed all dead links, legal stubs, new logo
- Legal: /about /privacy /terms /cookies /gdpr real pages added
- Email: FROM_EMAIL default fixed to noreply@ai-impress.com (verified
  apex domain), HTML template rewritten to match new brand
- Tooling: Playwright screenshot script for visual self-check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 21:02:03 +01:00
Vadym Samoilenko
9d2f1f2c7d feat: complete AIMPRESS visual rebrand — warm palette, new landing, real dashboard
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
- Replace cyan/violet design tokens with warm dark slate + orange (#E89B3C) palette
- Add Space Grotesk display font; new utilities: .outline-display, .orange-band, .corner-card, .persona-orb
- New brand components: Logo (hexagonal SVG), Header (pill nav + glass blur), Footer (4-col), PublicLayout, AppLayout, UserDropdown
- Rewrite Index.tsx as full sales funnel: Hero → Stats → Orange band → How it works → Pricing (API) → FAQ → Final CTA
- Rewrite Dashboard.tsx with real API data: credits balance, MTD spend, personas count, focus groups count, active tasks, recent transactions
- Rewrite auth pages (Login, Register, VerifyEmail, NotFound, Billing) with two-column orange-panel layout
- Replace hardcoded mock numbers in Dashboard with billingApi / personasApi / focusGroupsApi / usageApi calls
- Delete legacy components: Navigation.tsx, Hero.tsx, FeatureCard.tsx
- Add nested layout routing in App.tsx: PublicLayout for guests, AppLayout for protected routes
- Color sweep inner pages: replace all purple-500/600 with primary token
- Purge all semblance / Oliver / optical-dev references; rename semblance_app_documentation.md → cohorta_app_documentation.md; update backend scripts to cohorta_db

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 19:44:02 +01:00
Vadym Samoilenko
e01569c412 feat: commit all app changes — billing API, new auth, design overhaul
All checks were successful
Deploy to Production / deploy (push) Successful in 2m23s
Includes frontend redesign (Navigation, billingApi), backend updates
(auth routes, admin routes, LLM service refactor), MSAL removal,
and dependency updates.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 19:04:43 +01:00
Vadym Samoilenko
5491d2d73d Rebrand to Cohorta + full UI redesign + registration with email verification
Some checks failed
Deploy to Production / deploy (push) Failing after 0s
- Complete dark-theme redesign inspired by ai-impress.com (navy + cyan + violet palette)
- New Syne display font + gradient logo mark + SVG favicon
- New Navigation: glass-morphism, gradient logo, Get Started CTA
- New Hero: animated glow orbs, mock focus-group chat UI, stats row
- New landing: Features grid, How-It-Works steps, CTA banner
- New Footer: AImpress LTD branding, © AImpress LTD. All rights reserved.
- New Login page: dark card, password visibility toggle, link to Register
- New Register page: full form, benefits row, 50 free credits pitch
- New VerifyEmail page: token verification flow with auto-redirect
- Backend: email_service.py using Resend API for verification emails
- Backend: /api/auth/register, /verify-email, /resend-verification endpoints
- User model: email_verified, email_verify_token, email_verify_expires fields
- Gitea Actions CI/CD: auto-deploy to aimpress server on push to main

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-23 18:40:08 +01:00
Vadym Samoilenko
7b6a7c7347 Fix admin filters: ISO Z parsing crash + All time period returning month data
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>
2026-04-24 19:17:57 +01:00
Vadym Samoilenko
ad619d45fc Improve live token extraction: warn on missing usage_metadata, capture thinking tokens
- Add WARNING log when usage_metadata/usage is None so zero-cost events
  are visible in logs instead of silently disappearing
- Capture thoughts_token_count from Gemini thinking models into reasoning field
  (already included in candidates_token_count for billing, now also tracked separately)
- Add same warning for OpenAI missing usage object

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 19:13:39 +01:00
Vadym Samoilenko
d0ad8e67be Fix backfill: use accumulated conversation context for prompt estimation
Old logic used output text length as a proxy for prompt tokens — completely
wrong. Real Gemini calls send the full conversation history as context, so
prompt grows with every turn.

New logic:
- completion_tokens = len(response_text) / 3.8 (what was generated)
- prompt_tokens = base_template + sum(all_prior_messages_in_fg) / 3.8
  - persona_response base: 1500 tok (template + persona details + topic)
  - moderator base: 1200 tok (moderator template + fg context)
  - persona_generate base: 2500 tok (persona-detailed-generation.md template)

Also:
- Sorts messages chronologically per focus group before processing
- Accumulates context correctly so turn N includes turns 0..N-1 as context
- Idempotency via pre-fetched set instead of per-doc find_one queries
- cost_usd breakdown now has correct input/output split (not 40/60 guess)
- Dry-run prints per-focus-group cost estimates for sanity checking

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 19:11:01 +01:00
Vadym Samoilenko
57508e8e55 Add period selector to all cost-bearing admin tabs
- 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>
2026-04-24 19:03:16 +01:00
Vadym Samoilenko
d7ee22e557 Fix backfill pricing: read from model_pricing collection + --delete-existing-estimates flag 2026-04-24 18:57:25 +01:00
Vadym Samoilenko
66c8e1762e Fix backfill: handle list-type persona fields 2026-04-24 18:53:41 +01:00
Vadym Samoilenko
539c5eaaee Fix backfill script: use focus_group_messages collection + correct field names 2026-04-24 18:49:59 +01:00
Vadym Samoilenko
bc4138f332 Final pieces: decorators on LLM routes, usage self-service, billing page, WS events
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>
2026-04-24 18:43:13 +01:00
Vadym Samoilenko
915c81b8f1 Complete phases D–G: quota enforcement, token invalidation, admin writes, backfill
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>
2026-04-24 18:34:48 +01:00
Vadym Samoilenko
015e6cc5cc Add Phase D admin panel: user management + usage analytics
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>
2026-04-24 18:26:05 +01:00
Vadym Samoilenko
3e9ccafad2 Add LLM usage tracking infrastructure (Phases A-C)
- Model renames: gpt-5.2 → gpt-5.4-2026-03-05, gemini-3-pro-preview → gemini-3.1-pro-preview; retire gpt-4.1 via alias fallback
- New: llm_usage_context.py (ContextVar-based attribution), model_pricing.py (tiered pricing + 60s cache), usage_event.py (append-only telemetry), quota.py (user/FG quota enforcement with 80% warning)
- Wire _record_usage into all 3 LLM methods; set_llm_context at every service entry point
- Fix admin_required decorator (was sync, never awaited User.find_by_id); add active_required and with_user_context decorators
- Inject user_id into ContextVar from JWT on every authenticated request
- Add DB indexes for usage_events, model_pricing, users collections
- Seed script for model pricing (gpt-5.4 single-tier, gemini-3.1 two-tier 200k threshold)
- Fix parse_json_response NameError (logger undefined at module level)
- 70 passing tests: conftest.py with sys.modules stubs, test_usage_infrastructure.py (52 tests), rewrite stale test_llm_service.py (18 tests)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-24 18:08:27 +01:00
Vadym Samoilenko
2e85fc1acc Fix root cause: naive vs aware datetime crash + stuck AI mode indicator
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>
2026-03-23 19:30:04 +00:00