Commit graph

162 commits

Author SHA1 Message Date
f4025e3cbb settings: PATCH /api/settings/language with ACTIVE_LANGUAGES gate
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:16:17 +02:00
1ea71bc160 log: serve translated content when available; English fallback
Adds module-level _resolve_log_content(session, log_id, lang) helper
to app/routers/pages.py: looks up StrategicLogTranslation by (log_id,
lang) when lang != 'en'; falls back silently to the English original
when no translation row exists yet (the expected case for the first
hour after a new language activates, or when translation fails for a
specific log).

log_page / log_page_day pull cu.user.lang and thread it through
_log_page_context so the template renders the right variant.

Two tests cover both branches.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:13:57 +02:00
924f37548b digest: translate variants once per active non-en language
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 17:07:18 +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
e4982cdc04 ai-log-job: translate strategic log for active non-en languages
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:57:06 +02:00
e190d0e35b alembic: add 0022 localization (users.lang + strategic_log_translations) 2026-05-27 16:53:27 +02:00
9423fa81b7 models: add User.lang + StrategicLogTranslation
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:52:04 +02:00
7683f82820 i18n: add translate() helper backed by call_llm
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:48:32 +02:00
5730aad73c i18n: add LANGUAGES, ACTIVE_LANGUAGES, respond_in_clause helper
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:46:32 +02:00
2ecf250d53 localization: digest is shared, not per-user (corrected design)
The user pointed out that the only genuinely per-user AI surface is
portfolio analysis. The strategic log AND the email digest are both
shared cycles — generated once per cycle, consumed by many users.

For the digest, this means:
- _generate_variants still produces one English variant per tone (as
  today, unchanged)
- A new helper translates each variant once per active non-en lang in
  parallel via asyncio.gather, producing a {(tone, lang): content}
  table for the duration of the job run
- The per-user send loop selects (user.digest_tone, user.lang),
  falling back to the English variant of the same tone on miss

Translation count per run = tones × non-en active langs = 3 today.
100 Italian users no longer mean 100 translation calls.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:22:41 +02:00
8af1da12dd docs: implementation plan for Italian localization
11 TDD-style tasks: i18n service, translation helper, model + migration,
ai_log_job translation fan-out, per-user surfaces (analyse, digest),
localized /log endpoint, PATCH /api/settings/language, dropdown UI, and
final regression + manual smoke.

Per-user surfaces append "Respond in Italian." to the system prompt
(one extra line, no extra LLM call). The strategic log is generated in
English, then fanned out to translate() per active non-en language in
parallel via asyncio.gather. The /log endpoint serves the matching
translation row when present, English fallback otherwise.

Translation uses the default call_llm provider chain — no separate
cheap-model carve-out needed at DeepSeek's $0.28/M output pricing.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:13:29 +02:00
e6308260a5 docs: localization spec — explicit no-tier-gating decision
Translate for any user with lang='it' regardless of paid/free status.
Italian + UK are the first markets, so IT availability is part of the
public-facing experience — a free-tier visitor needs to see the AI in
Italian to convert. At ~$0.005/day total cost the gating isn't worth
the savings.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 16:08:28 +02:00
76f81648e5 docs: spec for Italian localization (ES/FR/DE as WIP)
Hybrid model: per-user surfaces (analyse, digest, chat) generated
directly in the target language via a "Respond in Italian" clause
appended to the system prompt. Shared content (strategic log)
generated in English as today, then post-translated and cached per
language in a new strategic_log_translations table. Translation calls
fan out in parallel with asyncio.gather so total job latency stays
bounded by max(single call).

No separate translation-model setting — DeepSeek-4-flash at $0.28/M
output is cheap enough that the routine cost is noise (~$0.005/day
with Italian only at 24 logs/day).

Users.lang VARCHAR(8) DEFAULT 'en'. Settings dropdown lists all four
options but ES/FR/DE are disabled UI-side and rejected server-side
against an ACTIVE_LANGUAGES allowlist — flipping them on later is a
one-line constant change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:50:10 +02:00
1ecc527118 cleanup: drop dead upload.html + soften broker-only marketing copy
- Delete app/templates/upload.html. The /upload route redirected to
  /settings#import (302) and never rendered this template; the file
  was carrying stale Trading-212-only copy.
- Landing + pricing pages: replace "Trading 212 today, more brokers
  planned" with "Trading 212 natively, other formats auto-detected"
  to reflect the LLM-fallback parser that's been live for a few days.

The /upload redirect route in app/routers/pages.py stays — it remains
a useful bookmark-forwarder for users with old links.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:39:03 +02:00
11662c0ea8 portfolio-edit: add a quiet how-to hint inside the composer
A small italic muted line beneath the form explaining the controls:
"Type a symbol, then quantity and cost — or use the calendar to fill
cost from a buy date — then [+] to add. [×] next to an existing row
removes it."

Only renders while the composer itself is visible (i.e. in edit mode),
so it doesn't clutter the dashboard at rest.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:27:44 +02:00
6377c929b8 portfolio-edit: form is edit-mode only; submit becomes a + glyph
Two related polishes:

- The add form was auto-shown by the empty-state path so brand-new
  users would see something to act on. That conflicts with the user's
  preference for "Edit always toggles the form, no other path." The
  empty state now shows guidance copy ("click edit to add one")
  instead. exitEditMode always hides the form too.
- The submit "add" word-button is replaced by a square accent-bordered
  + glyph (26×26). Matches the visual weight of the calendar ghost
  next to it but stays in the accent colour so it reads as primary.
  Adds a tiny active-state scale tick for tactile feedback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:22:13 +02:00
2ffd228976 portfolio-edit: indent add form 12px to match panel-header
Composer had zero horizontal padding so the leading `$` prompt was
flush with the panel border. Match the panel-header's 12px horizontal
inset so the form sits inside the panel's content gutter.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:20:20 +02:00
477a47be2c dashboard: drop "held locally · prices via /api/universe" meta line
Subtitle was technical noise that didn't earn its space in the header.
Title alone reads cleaner. Kept the scoped panel-header layout override
in cassandra.css since it's harmless and future-proof against re-adding
header children.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:19:52 +02:00
f997a8adde portfolio-edit: fix unescaped apostrophe in fallback string
A single-quoted string literal "couldn't validate" was breaking the
parse because the apostrophe wasn't escaped. The page logged a syntax
error and none of the edit-mode JS ran. Backslash-escape it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:15:46 +02:00
3de43f55a6 portfolio-edit: fix rogue Edit/Done/Add visibility
Two interlocking bugs surfaced after the design pass:

1. CSS `display: inline-flex` on .pf-edit-btn/.pf-done-btn overrode the
   UA's `[hidden] { display: none }`, so the JS toggling `editBtn.hidden`
   had no visual effect — both buttons rendered side by side.
2. portfolio.js's empty-state path sets `form.hidden = false` but the
   populated-portfolio render path only removed the `pf-empty` class; it
   never reset `form.hidden = true`. So once a user went through the
   empty state, the add form stuck around — leaving the Add button
   visible on a populated dashboard.

Fixes are surgical: add an explicit `[hidden]` rule for the two
header pills, and re-hide the form in `renderPanel` unless edit mode
is currently active (so we don't yank the form out from under an
edit-in-progress).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:09:34 +02:00
bb41ee38f7 portfolio-edit: design pass — terminal-aesthetic inline composer
The previous CSS used invented variable names (--neu-dim, --err, --ok)
that don't exist in the project's design system; the form fell back to
hardcoded hex values and looked disconnected from the rest of the site.

Rebuilt against the real tokens (--border, --dim, --muted, --positive,
--negative, --warning, --accent) and the mono-first 'geopolitical-
terminal aesthetic' the rest of the dashboard uses:

  $ ticker  ✓ 172.40 USD  │  qty  @  cost  USD  📅              add
                                                              ────

- No boxed-form chrome. A dashed bottom rule separates the composer
  from the table below.
- Inputs lose their card-style boxes; they're underline-only with a
  faint accent wash on focus — feels like editing a command line.
- '$' prompt marker, '│' divider, '@' between qty and cost give the
  row a terminal grammar without being twee.
- Submit is a ghost pill in the accent colour; lights to solid only
  when enabled.
- All controls now respond correctly to the light/dark theme toggle.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:06:32 +02:00
a9b7d4d8bb portfolio-edit: rebuild form as compact inline strip
Replace the multi-row wizard-style form (Ticker / Qty on row 1, mode
radios on row 2, Date+Cost on row 3) with a single horizontal strip
that sits unobtrusively above the portfolio table. The radio toggle is
gone; a small calendar icon next to the Cost input pops out a date
picker that auto-fills cost on selection and then hides itself.

Same input IDs, so the existing validate/Add/× handlers work unchanged.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 15:02:03 +02:00
70da4cdf84 css: portfolio edit-mode + add-position form styles 2026-05-27 14:56:43 +02:00
9a46a0daec portfolio: render hidden × per row; empty state shows add form
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:55:29 +02:00
84934827b8 portfolio-edit: bought-on-date mode + historical lookup
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:52:52 +02:00
58576a86fc portfolio-edit: add button writes position to localStorage
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:51:45 +02:00
ee6966399c portfolio-edit: ticker validate on blur + duplicate warning
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:50:48 +02:00
f1b242720d portfolio-edit: edit-mode toggle scaffold 2026-05-27 14:49:43 +02:00
9ed78f2758 dashboard: scaffold portfolio edit-mode markup 2026-05-27 14:49:01 +02:00
30e565909f ticker-validate: mount router at /api/ticker/*
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:48:00 +02:00
b7d6235fcb ticker-validate: add /api/ticker/historical with weekend-walkback
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:45:52 +02:00
ca953e5ea2 ticker-validate: cover failure + side-effect paths
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:43:15 +02:00
3bb62763ea ticker-validate: add /api/ticker/validate endpoint
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:41:33 +02:00
ae3f104fa7 docs: implementation plan for manual portfolio composition
12 TDD-style tasks: two backend endpoints (validate + historical),
router registration, dashboard markup, and five JS slices building the
edit-mode behaviour (toggle → ticker validate → Add → date-mode →
delete via delegation). CSS pass and final manual smoke close it out.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:36:32 +02:00
4c92d8a3e7 docs: spec for manual portfolio composition
Dashboard-native edit mode: EDIT button toggles in-place editing; the
add-position form has on-blur ticker validation against a new paid
endpoint, qty input, and an avg-cost / bought-on-date toggle. Only
avg_cost + qty are persisted to localStorage (no acquisition date,
no server-side holdings). Empty state replaces "Import a CSV" with
the inline form so brand-new users can act without leaving the page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 14:23:23 +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
b8ebba9503 ui: drop remaining T212-only framing from dashboard + import lede
- portfolio.js empty-state CTA: "Import a T212 CSV" → "Import a portfolio CSV"
- settings.html lede: lead with broker-agnostic copy; relegate the T212
  export path to a smaller secondary line.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:41:05 +02:00
b352601228 settings: soften import copy to be broker-agnostic
Section heading drops "Trading 212"; drop-zone label and hint mention
the auto-detect path; the help-paragraph opens conditionally with
"If you use Trading 212" so non-T212 users don't feel like outsiders.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:32:35 +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
59b28506df csv-parser: add public parse_with_llm with cache hit/miss orchestration
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:24:38 +02:00
c77b3564f3 csv-parser: add _extract_mapping_via_llm with provider-failure wrapping
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:21:19 +02:00
b99f46d2fc csv-parser: add _apply_mapping helper
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:18:31 +02:00
f44b77df6f csv-parser: add _validate_mapping helper
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:16:26 +02:00
8dcf662945 csv-parser: add _detect_dialect helper
Heuristic refined from the plan draft: candidate header rows must be
followed by a row containing at least one numeric token. Without this,
IBKR-style multi-line preambles (all-text rows before the real header)
would be mistaken for the header at preamble=0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:14:11 +02:00
f8a0ed3923 csv-parser: add _fingerprint helper
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:08:34 +02:00
f3fd769b3b tests: add fabricated IBKR fixture for LLM parser 2026-05-27 12:06:47 +02:00
4bcc20b0bb alembic: use sa.text() for integer server_defaults in 0021 2026-05-27 12:06:05 +02:00
ac82d5854e alembic: add 0021 csv_format_templates
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:03:17 +02:00
fb3498efb0 csv-parser: tighten CsvFormatTemplate generics + last_used_at note
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 12:01:06 +02:00
3f1d2a1034 csv-parser: add CsvFormatTemplate model
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 11:51:01 +02:00