Replaces the static bearer-token gate with a real auth boundary. The
existing CASSANDRA_TOKEN path is retained as an admin / scripting escape
hatch — kept compatible by aliasing require_token to require_auth.
- New users table (migration 0007): email, argon2 password_hash, tier,
email_verified (declared but not enforced until phase E), settings_json
for the tone/analysis/anchor knobs we'll wire in phase D.
- app/services/auth_service.py: argon2-cffi password hashing with timing-
attack-resistant authenticate() (always runs a hash verify even on
unknown-email to deny a username-enumeration oracle).
- app/auth.py rewritten: require_auth returns a CurrentUser with either
is_admin=True (bearer path) or a User object (session path). Failing
requests get 303 → /login for HTML, 401 for API. Sessions signed with
itsdangerous against CASSANDRA_SESSION_SECRET; 14-day TTL.
- app/routers/auth.py: /login, /signup, /logout. Login form preserves the
?next=… param for redirect-after-login. Signup respects a new
CASSANDRA_SIGNUP_ENABLED flag.
- Standalone /login + /signup templates (no app chrome). base.html grows
a user chip + logout link in the header (reads request.state.current_user).
Phase A's main known limitations are documented in the plan: email
verification is declared but not enforced; session revocation is
best-effort (cookie-only, not DB-backed). Both land in phase E.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Completes Phase B. The full alternative-onboarding flow is now end-to-end:
drop a T212 pie CSV → parser → InstrumentMap resolver → PortfolioSnapshot
+ Position rows, all without ever asking the user for broker credentials.
- persist_pie() in app/services/csv_import.py: takes a ParsedPie, resolves
each Slice via InstrumentMap, writes Portfolio + Snapshot + Position
rows. Unmapped slices are still persisted using their CSV values and
surfaced in the response for the UI to warn about.
- POST /api/portfolios/upload: multipart endpoint accepting CSV file +
optional portfolio_name + currency. 2 MiB cap. Returns import summary.
- /upload page with drag-drop dropzone, file input fallback, and inline
result panel showing invested/value/result + unmapped-slice warnings.
- New "Import" link in the header nav.
Verified end-to-end against the real T212 export: all 13 positions land
with correct T212 tickers (incl. FPp_EQ for the Paris TotalEnergies
listing the heuristic resolver picks), zero unmapped slices, totals
reconcile to the penny.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Containerised macro-strategy dashboard: 4-panel web UI (indicators,
portfolio, flash news, AI strategic log), MariaDB store, hourly
ingestion jobs, OpenRouter-backed AI analysis.
Ports the four prototype scripts in the parent dir (market_pulse,
flash_news, trading212, strategic_log) into async services backed by a
persistent DB and served via FastAPI + Jinja2 + HTMX. APScheduler runs
as a separate compose service for crash-safety and easier restarts.
Portfolio composition + position names come live from Trading 212;
news per-ticker headlines reuse those names. Tone (NOVICE/INTERMEDIATE/
PRO) and analysis style (DRY/SPECULATIVE) are env-configurable and
stored on each log row so historical entries show what produced them.
Default model is deepseek/deepseek-v4-flash (overridable via env).
Light/dark theme toggle, sans-serif for prose surfaces, monospace for
data. Bearer-token auth, OpenRouter monthly cost cap, RSS feeds auto-
disabled on consecutive failures.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>