soulsync/core/ui_appearance.py
BoulderBadgeDad a62d2d4310 ui appearance: default worker orbs OFF on Firefox for first-time users
The blurred 60fps worker-orb canvas is the main remaining Firefox lag source after the
#935 sweep (multiple Discord lag reports). So for a FIRST-TIME user with no saved
preference, default the orbs OFF on Firefox (smooth first impression where it's needed)
and ON everywhere else (full polish where the browser handles it). An explicit saved
choice ALWAYS wins — this only picks the default when the user hasn't chosen.

Done kettui-style with a SINGLE source of truth, not the dual browser-detection I first
floated (server UA + client _isFirefox would be the same fact in two places that can
drift — exactly the server/client class #943's green-flash fix just cleaned up):

- core/ui_appearance.py (new, pure + importable): is_firefox_user_agent +
  resolve_worker_orbs_default(explicit, is_firefox) — explicit wins, unset → !firefox.
- web_server: the SERVER decides (UA via _request_is_firefox, request-context-safe) and
  injects initial_worker_orbs_enabled; config default flipped None so "unset" is
  distinguishable from an explicit False. The client just consumes the injected value
  (init.js unchanged) — no client-side re-derivation of "is Firefox".
- settings.js: the orb checkbox default now reflects the server value when unset, so
  saving Settings can't silently flip a first-time Firefox user's orbs back on.

No regression: Chrome users unchanged; users with an explicit setting unchanged (it
wins regardless of browser); /api/settings returns raw config so it can't clobber the
default for an unset value. Verified end-to-end through a real Flask request context
(Firefox→off, Chrome→on, explicit wins both ways, no crash outside a request). 8 pure
seam tests pin the contract; ruff clean.
2026-06-28 16:50:22 -07:00

40 lines
1.6 KiB
Python

"""Pure decisions for UI-appearance defaults.
Kept here (importable, no Flask/config coupling) so the rules are unit-testable in
isolation; web_server does only the request/config plumbing around them.
Worker orbs are a blurred 60fps canvas — the main remaining Firefox lag source after
the #935 sweep. So for a FIRST-TIME user (no saved preference) we default them OFF on
Firefox and ON everywhere else: a smooth first impression where it's needed, full
polish where the browser handles it. An explicit saved choice ALWAYS wins — this only
picks the default when the user hasn't chosen.
"""
from __future__ import annotations
from typing import Optional
def is_firefox_user_agent(user_agent: Optional[str]) -> bool:
"""True when a User-Agent string is Firefox.
Used ONLY to pick a performance-friendly default — never to gate functionality —
so a spoofed or unusual UA simply gets the default and the user can toggle. Chrome,
Edge, Safari, Opera, Brave do not carry 'firefox' in their UA; Firefox does
('… Gecko/… Firefox/<ver>')."""
return 'firefox' in str(user_agent or '').lower()
def resolve_worker_orbs_default(explicit: object, is_firefox: bool) -> bool:
"""Whether worker orbs should be on.
``explicit`` is the saved config value: ``True``/``False`` when the user has chosen,
``None`` when unset. An explicit choice always wins; when unset, default OFF on
Firefox (perf) and ON elsewhere.
"""
if explicit is None:
return not is_firefox
return explicit is not False
__all__ = ['is_firefox_user_agent', 'resolve_worker_orbs_default']