From 6c44a5f3d02806472f657e52dba346c2fbcf5f95 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 28 Feb 2026 19:33:00 +0000 Subject: [PATCH] Add app label to root header and auto-reload sexp templates in dev Show current subdomain name (blog, cart, events, etc.) next to the site title in the root header row. Remove the redundant second "cart" menu row from cart overview and checkout error pages. Add dev-mode hot-reload for sexp templates: track file mtimes and re-read changed files per-request when RELOAD=true, so .sexp edits are picked up without restarting services. Co-Authored-By: Claude Opus 4.6 --- cart/sexp/sexp_components.py | 7 +----- shared/infrastructure/context.py | 1 + shared/infrastructure/factory.py | 8 +++++++ shared/sexp/components.py | 3 ++- shared/sexp/helpers.py | 1 + shared/sexp/jinja_bridge.py | 38 +++++++++++++++++++++++++++++++ shared/sexp/templates/layout.sexp | 8 ++++--- 7 files changed, 56 insertions(+), 10 deletions(-) diff --git a/cart/sexp/sexp_components.py b/cart/sexp/sexp_components.py index 7f9321e..d4b59ea 100644 --- a/cart/sexp/sexp_components.py +++ b/cart/sexp/sexp_components.py @@ -511,17 +511,13 @@ async def render_overview_page(ctx: dict, page_groups: list) -> str: """Full page: cart overview.""" main = _overview_main_panel_html(page_groups, ctx) hdr = root_header_html(ctx) - hdr += render("cart-header-child", inner_html=_cart_header_html(ctx)) return full_page(ctx, header_rows_html=hdr, content_html=main) async def render_overview_oob(ctx: dict, page_groups: list) -> str: """OOB response for cart overview.""" main = _overview_main_panel_html(page_groups, ctx) - oobs = ( - _cart_header_html(ctx, oob=True) - + root_header_html(ctx, oob=True) - ) + oobs = root_header_html(ctx, oob=True) return oob_page(ctx, oobs_html=oobs, content_html=main) @@ -717,7 +713,6 @@ def _checkout_error_content_html(error: str | None, order: Any | None) -> str: async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str: """Full page: checkout error.""" hdr = root_header_html(ctx) - hdr += render("cart-header-child", inner_html=_cart_header_html(ctx)) filt = _checkout_error_filter_html() content = _checkout_error_content_html(error, order) return full_page(ctx, header_rows_html=hdr, filter_html=filt, content_html=content) diff --git a/shared/infrastructure/context.py b/shared/infrastructure/context.py index 09f268b..cf05dc6 100644 --- a/shared/infrastructure/context.py +++ b/shared/infrastructure/context.py @@ -98,6 +98,7 @@ async def base_context() -> dict: "qs_filter": _qs_filter_fn(), "print": print, "base_url": base_url, + "app_label": current_app.name, "base_title": config()["title"], "hx_select": hx_select, "hx_select_search": hx_select_search, diff --git a/shared/infrastructure/factory.py b/shared/infrastructure/factory.py index 280122d..4248340 100644 --- a/shared/infrastructure/factory.py +++ b/shared/infrastructure/factory.py @@ -114,6 +114,14 @@ def create_base_app( setup_sexp_bridge(app) load_shared_components() load_relation_registry() + + # Dev-mode: auto-reload sexp templates when files change on disk + if os.getenv("RELOAD") == "true": + from shared.sexp.jinja_bridge import reload_if_changed + + @app.before_request + async def _sexp_hot_reload(): + reload_if_changed() errors(app) # Auto-register OAuth client blueprint for non-account apps diff --git a/shared/sexp/components.py b/shared/sexp/components.py index 8e103d2..f84e90c 100644 --- a/shared/sexp/components.py +++ b/shared/sexp/components.py @@ -9,10 +9,11 @@ from __future__ import annotations import os -from .jinja_bridge import load_sexp_dir +from .jinja_bridge import load_sexp_dir, watch_sexp_dir def load_shared_components() -> None: """Register all shared s-expression components.""" templates_dir = os.path.join(os.path.dirname(__file__), "templates") load_sexp_dir(templates_dir) + watch_sexp_dir(templates_dir) diff --git a/shared/sexp/helpers.py b/shared/sexp/helpers.py index 59f17aa..650ca96 100644 --- a/shared/sexp/helpers.py +++ b/shared/sexp/helpers.py @@ -36,6 +36,7 @@ def root_header_html(ctx: dict, *, oob: bool = False) -> str: cart_mini_html=ctx.get("cart_mini_html", ""), blog_url=call_url(ctx, "blog_url", ""), site_title=ctx.get("base_title", ""), + app_label=ctx.get("app_label", ""), nav_tree_html=ctx.get("nav_tree_html", ""), auth_menu_html=ctx.get("auth_menu_html", ""), nav_panel_html=ctx.get("nav_panel_html", ""), diff --git a/shared/sexp/jinja_bridge.py b/shared/sexp/jinja_bridge.py index 646d3a7..fd62049 100644 --- a/shared/sexp/jinja_bridge.py +++ b/shared/sexp/jinja_bridge.py @@ -53,11 +53,49 @@ def load_sexp_dir(directory: str) -> None: register_components(f.read()) +# --------------------------------------------------------------------------- +# Dev-mode auto-reload of sexp templates +# --------------------------------------------------------------------------- + +_watched_dirs: list[str] = [] +_file_mtimes: dict[str, float] = {} + + +def watch_sexp_dir(directory: str) -> None: + """Register a directory for dev-mode file watching.""" + _watched_dirs.append(directory) + # Seed mtimes + for fp in sorted( + glob.glob(os.path.join(directory, "*.sexp")) + + glob.glob(os.path.join(directory, "*.sexpr")) + ): + _file_mtimes[fp] = os.path.getmtime(fp) + + +def reload_if_changed() -> None: + """Re-read sexp files if any have changed on disk. Called per-request in dev.""" + changed = False + for directory in _watched_dirs: + for fp in sorted( + glob.glob(os.path.join(directory, "*.sexp")) + + glob.glob(os.path.join(directory, "*.sexpr")) + ): + mtime = os.path.getmtime(fp) + if fp not in _file_mtimes or _file_mtimes[fp] != mtime: + _file_mtimes[fp] = mtime + changed = True + if changed: + _COMPONENT_ENV.clear() + for directory in _watched_dirs: + load_sexp_dir(directory) + + def load_service_components(service_dir: str) -> None: """Load service-specific s-expression components from {service_dir}/sexp/.""" sexp_dir = os.path.join(service_dir, "sexp") if os.path.isdir(sexp_dir): load_sexp_dir(sexp_dir) + watch_sexp_dir(sexp_dir) def register_components(sexp_source: str) -> None: diff --git a/shared/sexp/templates/layout.sexp b/shared/sexp/templates/layout.sexp index cc34cf3..8406958 100644 --- a/shared/sexp/templates/layout.sexp +++ b/shared/sexp/templates/layout.sexp @@ -96,7 +96,7 @@ :class "w-12 h-12 rotate-180 transition-transform group-open/root:block hidden self-start" (path :d "M6 9l6 6 6-6" :fill "currentColor")))) -(defcomp ~header-row (&key cart-mini-html blog-url site-title +(defcomp ~header-row (&key cart-mini-html blog-url site-title app-label nav-tree-html auth-menu-html nav-panel-html settings-url is-admin oob) (<> @@ -106,8 +106,10 @@ (div :class "w-full flex flex-row items-top" (when cart-mini-html (raw! cart-mini-html)) (div :class "font-bold text-5xl flex-1" - (a :href (or blog-url "/") :class "flex justify-center md:justify-start" - (h1 (or site-title "")))) + (a :href (or blog-url "/") :class "flex justify-center md:justify-start items-baseline gap-2" + (h1 (or site-title "")) + (when app-label + (span :class "!text-2xl font-normal text-white" app-label)))) (nav :class "hidden md:flex gap-4 text-sm ml-2 justify-end items-center flex-0" (when nav-tree-html (raw! nav-tree-html)) (when auth-menu-html (raw! auth-menu-html))