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>
This commit is contained in:
Giorgio Gilestro 2026-05-29 15:32:58 +02:00
parent 13dd3a8330
commit 21835afebe
2 changed files with 19 additions and 2 deletions

View file

@ -362,10 +362,19 @@ async def analyze_portfolio(
except Exception:
raise HTTPException(status_code=400, detail="malformed JSON body")
user_lang = (
# Resolve lang. The frontend sends the live toggle state in
# payload["lang"]; that's what the user is *looking at* right now
# and is the most up-to-date value. user.lang from the DB is the
# persisted preference and is used as a fallback when the frontend
# didn't send anything (older clients, scripts, direct curl).
db_lang = (
principal.user.lang if (principal.user and principal.user.lang) else "en"
)
payload["lang"] = user_lang
incoming = (payload.get("lang") or "").strip().lower()
payload["lang"] = incoming or db_lang
log.info("analyze.lang_resolved",
payload_lang=incoming or None, db_lang=db_lang,
final=payload["lang"])
try:
req = portfolio_analysis.parse_request(payload)

View file

@ -469,6 +469,13 @@
}
}
// The language toggle's data-lang attribute is the user's LIVE
// pick — newer than user.lang in the DB if the user toggled and
// hit Generate/Regenerate before the toggle-PATCH committed.
// Backend prefers this value if provided (see universe.py).
const langPill = document.getElementById('lang-toggle');
const userLang = (langPill && langPill.dataset.lang) || 'en';
try {
const r = await fetch('/api/analyze', {
method: 'POST',
@ -478,6 +485,7 @@
positions: pie.positions,
prices: prices,
base_currency: pie.base_currency || 'GBP',
lang: userLang,
}),
});
const data = await r.json();