From 72562be961bb369ca3de9d93058d088a6ff0f760 Mon Sep 17 00:00:00 2001
From: nnnet <21066097+nnnet@users.noreply.github.com>
Date: Sun, 31 May 2026 21:36:46 +0300
Subject: [PATCH] fix(dashboard): inline critical-CSS bootstrap for user themes
to mitigate flash
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
User themes (`~/.hermes/dashboard-themes/*.yaml`) reach the SPA only
after `/api/dashboard/themes` resolves at React mount. The bundle paints
the first frame with the default Hermes Teal canvas — the
`` carries `:root{--background-base:#041c1c}`,
the bundled `presets.ts` defines the same surfaces in JS — and then
`ThemeProvider.applyTheme()` flips the inline CSS variables
on `documentElement` once the API response lands. Visible to the user
as a green canvas behind the loading SPA on every reload when the active
theme is non-default.
Built-in themes do not suffer the same effect because their full
definitions ship inside the bundle, so the SPA already has the palette
before first paint.
This patch closes the gap on the backend side: `_serve_index()` injects
a ``` escape — current values are well-known
+ # hex/font strings, but this keeps the helper safe if it is
+ # later extended to ship user-authored CSS literals.
+ def _esc(s: str) -> str:
+ return str(s).replace("", "<\\/")
+ return (
+ '"
+ )
+ return ""
+ except Exception:
+ _log.debug("theme bootstrap render failed", exc_info=True)
+ return ""
+
+
def mount_spa(application: FastAPI):
"""Mount the built SPA. Falls back to index.html for client-side routing.
@@ -16378,6 +16436,16 @@ def mount_spa(application: FastAPI):
html = html.replace('href="/fonts/', f'href="{prefix}/fonts/')
html = html.replace('href="/ds-assets/', f'href="{prefix}/ds-assets/')
html = html.replace('src="/ds-assets/', f'src="{prefix}/ds-assets/')
+ # Theme flash mitigation: when the active theme is a user theme
+ # (``HERMES_HOME/dashboard-themes/.yaml``), inject a minimal
+ # critical-CSS block so the first paint uses the target palette.
+ # Without this the SPA paints the default Hermes Teal canvas, then
+ # ``ThemeProvider`` flips the CSS variables once
+ # ``/api/dashboard/themes`` resolves. Built-in themes are already
+ # in the bundle's ``presets.ts`` so no shim is needed for them.
+ theme_bootstrap = _render_active_theme_bootstrap_css()
+ if theme_bootstrap:
+ html = html.replace("", f"{theme_bootstrap}", 1)
html = html.replace("", f"{bootstrap_script}", 1)
return HTMLResponse(
html,