Implements docs/read-markets-compliance-changes.md as flag-gated changes
(no deletions) so paused features stay in the tree for future re-enable.
All four flags default False so a fresh deploy is compliance-safe.
- New env flags: PORTFOLIO_AI_ENABLED, PORTFOLIO_SYNC_ENABLED,
TICKER_UNIVERSE_AGGREGATE_ENABLED, SUBSCRIPTIONS_ENABLED.
- Gates: /api/analyze, /api/portfolio/sync*, /api/stripe/*, /pricing,
ticker_universe writes, portfolio_analysis.analyse(). is_paid_active()
returns True for any auth'd user when subscriptions are paused.
- Prompts (PROMPT_VERSION 10): universal _COMPLIANCE_RIDER prepended to
every system prompt; watch list removed; price-target / close-above-below
/ trigger / forward-state-as-description rules added; SPECULATIVE
pivoted to regime-only scenarios; daily + weekly digests tightened.
- Reviewer: deterministic regex/lexicon pre-check fail-closed under the
Haiku call; portfolio rider gated by PORTFOLIO_AI_ENABLED; base prompt
sharpened for forward-state and MAR forward-opinion patterns;
ReviewerVerdict audit table; generate_with_review retry helper.
- Migration 0026: purge portfolio_sync + ticker_universe; create
reviewer_verdicts.
- Copy: MAR cite fixed to Art 3(1)(35) + Art 20 + Del Reg 2016/958;
portfolio reframed as browser-only viewer in disclaimer / privacy /
terms / about / pricing / landing (en + it). TODO(legal) marker for
lawyer sign-off on disclaimer.
- Tests: 13 lexicon + 6 reviewer compliance regressions; conftest enables
all flags so existing 402 tests still cover their code paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Public landing page is now served in English and Italian via path-
prefixed URLs (/en/ and /it/), with the bare / detecting the
visitor's language and 302-ing to the right one.
Routing
-------
* GET / : if authed → dashboard (unchanged). Otherwise the visitor's
language is resolved from (in priority order) the rtm.lang cookie,
the Accept-Language header, the cf-ipcountry geolocation header,
and finally DEFAULT_LANG, then a 302 redirects to /<lang>/.
* GET /en/ + /it/ : render the localised landing template, set the
rtm.lang cookie (1-year, SameSite=Lax) so the next /-visit goes
straight to the same translation. Logged-in users with user.lang
set bypass detection for / (same priority chain — user.lang wins
on every public surface they touch).
Storage
-------
* app/locales/<lang>.yaml — flat-ish nested copy files. YAML chosen
so a future translator can edit without touching Python. Strings
containing inline HTML (<strong>, <em>, <a>) are rendered with the
Jinja `safe` filter in the template.
* app/services/locales.py — loads at startup, exposes get_locale()
and detect_public_lang(). Wraps each YAML tree in a small _Dotted
view so templates can write {{ t.hero.subhead }} (deliberately NOT
a dict subclass — dict's built-in method names would shadow YAML
keys like `items`).
Template + chrome
-----------------
* landing.html ported in full to {{ t.<key> }} references.
* public_base.html gets <html lang="…"> + hreflang link tags (en/it/
x-default) when a route opts in via lang_switch=true. A tiny
EN | IT link group lands in the public header, only visible on
surfaces that opt in.
* public.css picks up the small lang-switch widget styles.
Scope (intentionally narrow)
----------------------------
* Only the landing page is localised. Pricing, terms, privacy,
disclaimer, login, verify all stay English-only for now; their
header chrome stays English too because localising labels there
while the linked content is still EN would be a worse mismatch.
* When other public pages get translated, lang_switch=true on those
routes will surface the same widget there with no template changes.
Tests
-----
* tests/test_locales.py covers YAML load parity (every active
language has a file), dotted access through the tree, the
detection precedence chain, and the unknown-locale fallback.
Deps
----
* pyyaml was already in requirements.lock as a transitive but not
declared. Added to pyproject so it stays pinned as an explicit
direct dependency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User reported phone still showing old behaviour (Qty/Avg portfolio
columns visible) even though the server-side JS had been updated.
Root cause: every <link>/<script> URL was a plain
/static/css/foo.css with no query string, so mobile Chrome served
the file from its HTTP cache rather than refetching it.
Adds a process-startup timestamp to the Jinja environment as
ASSET_VERSION (computed once when templates_env is imported). Every
<link>/<script> reference now appends `?v={{ ASSET_VERSION }}` so a
container restart bumps the URL and the browser refetches. 38 URLs
across 8 templates updated via sed; tests still pass.
Side benefit: future CSS/JS edits no longer require users to hard-
refresh.
Splits the 2571-line cassandra.css into ten focused stylesheets:
tokens (palette + fonts), layout (chrome), panels, dashboard,
portfolio, log-chat, auth, settings, news, public. base.html and
public_base.html load only what they need; auth pages (login,
verify, unsubscribe confirm) load tokens + layout + auth.
Brand drift-detection test repointed at tokens.css (where the
palette now lives). 291 tests still pass.
Adds the unauthenticated surface that's needed to invite outsiders:
- Landing (/) — dual-purpose root: dashboard for logged-in users,
landing for everyone else. New maybe_current_user soft-auth helper
in app/auth.py supports it without disturbing the per-route
require_token deps on /news, /log, /upload, /settings.
- About, Pricing, Disclaimer, Terms, Privacy — own router
(app/routers/public.py), no auth dep, shared public_base layout
(brand link, thin nav, footer with legal links + ICO ref + date).
- Editorial positioning: news aggregator with a macro brain; tagline
"Understand markets. Don't gamble on them."; anti-trading-as-gambling
stance carried through About and Landing.
Legal pass following an independent lawyer-style review:
- Privacy: explicit UK-GDPR Art. 6 lawful-basis section; Art. 22
automated-decision line; explicit consent for sessionStorage sync
key (PECR); 30-day IP-log retention; Art. 21 objection right;
Children clause; Art. 33/34 breach-notification clause;
international-transfer mechanism (IDTA + UK Addendum). ICO
registration ZC098928 surfaced at the top.
- Pricing: paid-card AI-portfolio-analysis bullet rewritten to remove
advice-shaped wording ("what would invalidate the posture" gone);
added italic carve-out citing FSMA / FCA COBS.
- Disclaimer: separate EU/EEA carve-out + MAR 596/2014 Art. 3(1)(34)
commentator safe-harbour; "qualifies the Terms" line; hallucination
wording fixed.
- Terms: cl.4 explicit AI-training prohibition + harassment line;
cl.5 CCR 2013 14-day cancellation; cl.7 softened AI copyright
claim under CDPA s.9(3) ambiguity; cl.8 proportionate suspension +
pro-rata refund for paid users; cl.10 CRA 2015 Pt 1 statutory-rights
carve-out from the liability cap; cl.11 right to close account on
material change; cl.12 non-exclusive jurisdiction + UK consumer
local courts.
Code-side enforcement of the Privacy claim:
- openrouter.py: outbound OpenRouter calls now carry
X-OR-Allow-Training: false. DeepSeek doesn't expose a per-request
flag; the Privacy page discloses this caveat verbatim.
Apex domain prep:
- branding.APP_URL flipped to https://read.markets (was app.). DNS for
the apex already resolves; pending operator NPM step is a cert that
covers the bare apex + a 301 from app.read.markets. No hard-coded
subdomain references remain in code (verified with grep).
Nav + chrome:
- app dropdown gains Pricing / Terms / Privacy / Disclaimer links.
- login.html gains a small legal-links footer for the
highest-leverage moment to surface them.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>