Aug 5 - Add new design (switchable)

This commit is contained in:
2026-08-05 14:07:38 -04:00
parent 07379bd915
commit f92d8783bf
15 changed files with 2899 additions and 568 deletions
+82
View File
@@ -27,8 +27,40 @@ limiter = Limiter(
)
# ── Design A/B test: per-request template overrides (phase48) ────────────────
# A user on the 'modern' design gets templates/modern/<name>.html in place of
# templates/<name>.html whenever that override exists; otherwise the normal
# template is used and only the layout shell + CSS differ.
#
# The rewrite happens in get_template() (not in the loader) so Jinja's template
# cache is keyed on the REWRITTEN name — a cached modern template can never be
# served to a classic user, or vice versa.
from flask.templating import Environment as _FlaskJinjaEnvironment
class ThemedEnvironment(_FlaskJinjaEnvironment):
"""Jinja environment that redirects template names to modern/<name>."""
# Populated once in create_app() by scanning templates/modern/.
jqc_modern_templates: set = set()
def get_template(self, name, parent=None, globals=None):
if (isinstance(name, str)
and self.jqc_modern_templates
and not name.startswith('modern/')):
candidate = 'modern/' + name
if candidate in self.jqc_modern_templates:
from flask import g, has_request_context
if has_request_context() and getattr(g, 'jqc_theme', 'classic') == 'modern':
name = candidate
return super().get_template(name, parent, globals)
def create_app(config_name='default'):
app = Flask(__name__)
# Must be assigned BEFORE app.jinja_env is first touched (it is a cached
# property), so the themed subclass is the one actually instantiated.
app.jinja_environment = ThemedEnvironment
app.config.from_object(config[config_name])
# ── Reverse-proxy awareness (Nginx) ──────────────────────────────────────
@@ -111,6 +143,54 @@ def create_app(config_name='default'):
from app.utils import storage as _storage
app.jinja_env.globals['media_url'] = _storage.media_url
# ── Design A/B test wiring (phase48) ──────────────────────────────────
# Index the modern/ override templates once at boot, so get_template()
# never has to touch the filesystem per request.
_modern_root = os.path.join(app.template_folder or 'templates', 'modern')
if not os.path.isabs(_modern_root):
_modern_root = os.path.join(app.root_path, _modern_root)
_modern_set = set()
if os.path.isdir(_modern_root):
for _dirpath, _dirnames, _filenames in os.walk(_modern_root):
for _fn in _filenames:
if _fn.endswith('.html'):
_rel = os.path.relpath(os.path.join(_dirpath, _fn), _modern_root)
_modern_set.add('modern/' + _rel.replace(os.sep, '/'))
ThemedEnvironment.jqc_modern_templates = _modern_set
app.logger.info('UI themes | modern overrides indexed: %s', len(_modern_set))
from flask import g
@app.before_request
def resolve_ui_theme():
"""Stash the active design on `g` for ThemedEnvironment.get_template()."""
# The mobile API renders no templates and authenticates by JWT — skip it
# so this never touches the Flask-Login session loader on API traffic.
if request.path.startswith('/api/'):
g.jqc_theme = 'classic'
return
from flask_login import current_user as _cu
theme = 'classic'
try:
if _cu.is_authenticated:
theme = _cu.ui_theme or 'classic'
except Exception: # DB column missing (migration not yet run)
theme = 'classic'
g.jqc_theme = theme if theme in ('classic', 'modern') else 'classic'
@app.context_processor
def inject_ui_theme():
"""Give base.html the shell to extend."""
from app.utils.time_utils import now_eastern
theme = getattr(g, 'jqc_theme', 'classic')
return {
'jqc_theme': theme,
'jqc_layout': 'layouts/modern.html' if theme == 'modern'
else 'layouts/classic.html',
# Long-form date shown in the modern dashboard header.
'now_display': now_eastern().strftime('%A, %B %-d, %Y'),
}
# ── Inject unread notification count into every template context ──────
# This powers the red badge on the navbar bell icon without requiring
# individual routes to pass the count manually.
@@ -183,6 +263,7 @@ def create_app(config_name='default'):
from app.routes import devices # Admin device registry
from app.routes import public # Public facility QR pages (no login)
from app.routes import scheduled_inspections # Planned/recurring inspections
from app.routes import ui # phase48 — design A/B test + new pages
app.register_blueprint(auth.bp)
app.register_blueprint(dashboard.bp)
@@ -201,6 +282,7 @@ def create_app(config_name='default'):
app.register_blueprint(devices.bp)
app.register_blueprint(public.bp)
app.register_blueprint(scheduled_inspections.bp)
app.register_blueprint(ui.bp)
# ── Mobile API (Phase 7 / Phase A / Phase B / Phase C) ───────────────────
# The /api/v1 blueprint group uses JWT Bearer tokens — no CSRF cookies needed.