"""Shared Jinja2 environment with custom filters for the dashboard. Imported by both routers/pages.py and routers/api.py so the filters are registered exactly once.""" from __future__ import annotations import time from pathlib import Path from fastapi.templating import Jinja2Templates from markupsafe import Markup, escape from app import branding from app.config import get_settings from app.services.glossary import wrap_glossary # Cache-busting token for static assets. Computed once at import time # (i.e. process startup), so every container restart yields a fresh # value and browsers refetch CSS/JS instead of serving stale cache. # Templates append `?v={{ ASSET_VERSION }}` to every static URL. ASSET_VERSION = str(int(time.time())) TEMPLATE_DIR = Path(__file__).resolve().parent / "templates" def _fmt_price(v: float | None) -> str: """Format a price in a way that's readable in dense terminal tables. Avoids scientific notation for large round numbers (FTSE 25,962, not 2.596e+04) and keeps enough precision for FX rates like 0.8725 EUR/GBP.""" if v is None: return "—" av = abs(v) if av >= 1000: return f"{v:,.2f}" if av >= 10: return f"{v:.2f}" if av >= 1: return f"{v:.4f}" return f"{v:.4f}" def _fmt_signed(v: float | None, decimals: int = 2) -> str: if v is None: return "—" return f"{v:+,.{decimals}f}" def _fmt_money(v: float | None) -> str: if v is None: return "—" return f"{v:,.2f}" def _glossary_filter(value, tone: str | None = None): """Wrap glossary terms in NOVICE-mode AI content. Returns Markup so Jinja won't re-escape the inserted tags. Plain-text inputs are HTML-escaped first; already-Markup inputs (e.g. log.content_html) are treated as HTML and passed through wrap_glossary unchanged.""" if value is None: return Markup("") if isinstance(value, Markup): html = str(value) else: html = str(escape(value)) if (tone or "").upper() != "NOVICE": return Markup(html) return Markup(wrap_glossary(html, tone=tone)) templates = Jinja2Templates(directory=str(TEMPLATE_DIR)) templates.env.filters["price"] = _fmt_price templates.env.filters["signed"] = _fmt_signed templates.env.filters["money"] = _fmt_money templates.env.filters["glossary"] = _glossary_filter # Brand globals — every template that prints a product name should pull # from these so a future rename is a one-liner in `app/branding.py`. templates.env.globals["BRAND_NAME"] = branding.BRAND_NAME templates.env.globals["BRAND_SHORT"] = branding.BRAND_SHORT templates.env.globals["SITE_URL"] = branding.SITE_URL templates.env.globals["APP_URL"] = branding.APP_URL templates.env.globals["TAGLINE"] = branding.TAGLINE templates.env.globals["LEGAL_OPERATOR"] = branding.LEGAL_OPERATOR templates.env.globals["OPERATOR_EMAIL"] = branding.OPERATOR_EMAIL templates.env.globals["OPERATOR_JURISDICTION"] = branding.OPERATOR_JURISDICTION templates.env.globals["BETA_MODE"] = get_settings().BETA_MODE templates.env.globals["ASSET_VERSION"] = ASSET_VERSION