Commit graph

8 commits

Author SHA1 Message Date
21835afebe analyze: send the live toggle lang from the frontend, log resolution
The /api/analyze flow previously read principal.user.lang from the
DB on every request and ignored anything the client might send. That
races the language toggle's PATCH: a user can flip the toggle and
click Generate/Regenerate before the PATCH /api/settings/language
hits the DB, so the analysis is sent with the OLD persisted lang
while the toggle visually reads as the new one. From the user's POV
the analysis comes back in the wrong language.

Frontend portfolio.js now reads the live #lang-toggle data-lang
attribute (the same source the UI itself uses) and includes it in
the /api/analyze body. The dataset attribute is updated optimistically
by cassandraSetLang() before the PATCH fires, so it always reflects
what the user is looking at.

Backend universe.py prefers payload["lang"] when present and falls
back to user.lang otherwise — older clients (scripts, direct curl)
that don't send anything still get the DB-stored preference. The
resolution path is logged so we can confirm in prod which lang
actually drove a given request.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-29 15:32:58 +02:00
a2bcb2c053 cleanup: drop stale tombstones and dead config fields
Stale comments referencing completed migrations:
- universe.py "remain live until step 10 of Phase G" — endpoints gone
- api.py "Portfolio endpoints moved to universe.py" — empty block
- csv_import.py "persist_pie removed in Phase G" — historical context

Dead Settings fields (all confirmed unreferenced by app code):
- CASSANDRA_PORT — port is hardcoded in docker-compose / uvicorn cmd
- POLAR_API_KEY — Polar was replaced by Stripe
- CASSANDRA_MOCK — env var still set by tests as a sentinel; the
  Settings field itself was never read
- CASSANDRA_BASE_CURRENCY — "GBP" hardcoded inline elsewhere

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:25:33 +02:00
d318039ad5 analyse: thread user.lang into the system prompt
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:01:00 +02:00
bc55ab7d26 csv-parser: keep LLM-mapped tickers; don't pass them through T212 mapping
The route's resolve-slice loop is T212-specific — it looks tickers up
against the InstrumentMap, which only has T212's universe. For the LLM
path the ticker is already Yahoo-ready (e.g. VOD.L, ASML.AS), so
sending it through resolve_slice produced spurious "could not be
resolved" warnings and dropped the positions.

Fix: ParsedPie gains a ``tickers_resolved`` flag (default False for
T212 backward-compat); _apply_mapping in the LLM path sets it True
and also extracts currency from the LLM-mapped currency_col into a
new ``ParsedPosition.currency`` field. The route branches on the flag:
LLM-path positions are kept verbatim with a best-effort InstrumentMap
lookup for nicer name/currency overrides, never dropped.

Integration test tightened to assert all 5 IBKR fixture positions
round-trip with the right currencies (USD / GBP / EUR).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:48:27 +02:00
8bc9dccd40 universe: paid-gate + LLM fallback on /portfolio/parse
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:31:07 +02:00
1be0c5a436 docs: drop Phase D.x markers now that the referral loop is closed
The "Phase D.1/D.2/D.3" comment scaffolding and the "Paddle webhook
will fill this in" references became actively misleading after D.3
landed — anyone reading the code would think referral conversion was
still pending. Also corrects a stale "Paddle" reference to "Stripe"
(we never shipped Paddle; ended up on Stripe after the Paddle → Polar
→ Stripe MoR onboarding pivot).

Pure docstring sweep, no behaviour change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 23:09:39 +02:00
9759080134 phase D milestones 1+2: referral system + paid-access gate
Lays the billing-prep spine before Paddle lands in D.3.

D.1 — referrals
- users.referral_code: unique 8-char URL-safe code (alphabet excludes the
  ambiguous 0/O/1/I/L). Generated lazily on first /settings hit so existing
  accounts pick one up without a backfill migration.
- users.referred_by_user_id + new referrals audit table (referrer,
  referred, created_at, converted_at, credited_at). converted_at /
  credited_at stay null until D.3 fills them via the Paddle webhook.
- POST /login accepts ?ref=<code>; the code rides on the signed
  pending-verify cookie so it survives the GET → POST → /verify hop.
- /settings page: email, tier badge, referral code chip + invite link
  with one-click copy, pending/converted/active-credits stats grid.
  Settings nav link added to the top bar.

Reward shape: when the referred user makes their first paid Paddle
subscription, both they and the referrer get 50% off for 3 months.
(D.3 wires the actual credit application via the Paddle webhook.)

D.2 — paid-access gate
- users.credit_until: timestamp until which a free-tier account has
  paid-tier access. Null = no credit. Populated by admin CLI now and the
  D.3 webhook later.
- app.services.access exposes paid_status(user) → PaidStatus dataclass
  (active / source / expires_at / days_remaining), is_paid_active() with
  admin-bearer-token bypass, and a require_paid FastAPI dependency that
  raises 402 Payment Required for free-tier callers.
- POST /api/analyze (portfolio AI commentary) gated behind require_paid.
- Settings page surfaces credit window when active ("free · credit · N
  day(s) remaining (expires YYYY-MM-DD)") and the upgrade hint when not.
- Admin CLI: python -m app.cli {grant-credit,revoke-credit,show-status}.
  grant-credit is idempotent — extends from max(now, current expiry) so
  re-running the command never erodes an existing grant.

Migrations 0013 (referrals) and 0014 (credit_until). Tests cover the
paid-status truth table, code generation + normalisation, CLI argument
parsing, and the pending-cookie ref roundtrip (29 new tests).
2026-05-21 23:25:35 +01:00
6e7f57c6b2 phase G: data minimisation + passwordless auth + DeepSeek-first LLM
Server no longer holds portfolios. Holdings live in the browser
(localStorage); the server publishes an anonymous ticker_universe and a
gzipped /api/universe payload identical for every authenticated user, so
access patterns can't betray which tickers a user holds. AI commentary
is generated ephemerally from the browser-supplied pie and the cost
ledger row records no positions. Migrations 0009-0011 added the
universe table and dropped positions / portfolio_snapshots /
portfolios.

Authentication is now e-mail OTP only. Migration 0010 dropped
password_hash and email_verified (every active session is by
construction proof of email control). The /signup endpoint is gone;
signup and login share a single email-entry page. Email rendering is
HTML+plain-text multipart with a shared brand palette (app/branding.py)
asserted in sync with the CSS by a drift-detection test.

LLM provider defaults to DeepSeek-direct (cheaper, api.deepseek.com)
with OpenRouter as automatic fallback if DeepSeek fails. ai_log_job and
indicator_summary_job now iterate the two tones (NOVICE, INTERMEDIATE)
per cycle so the dashboard's tone toggle is instant; PROMPT_VERSION
bumped to 6 with an educational anti-TA / anti-gambling stance baked
into _CORE. NOVICE mode renders a curated glossary inline (CBOE VIX,
yield curve, HY OAS, etc.) with JS-positioned tooltips that survive
viewport edges and sticky bars. Model name and tokens hidden from the
user UI; still recorded in StrategicLog.model and AICall for admin.

Layout adds a sticky top nav, a sticky bottom markets bar (one chip per
exchange with status LED + headline index + 1d change), and
Phase H feedback reporting is queued in tasks/todo.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 14:16:57 +01:00