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 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 19:33:00 +00:00
parent 6d43404b12
commit 6c44a5f3d0
7 changed files with 56 additions and 10 deletions

View File

@@ -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)

View File

@@ -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,

View File

@@ -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

View File

@@ -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)

View File

@@ -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", ""),

View File

@@ -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:

View File

@@ -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))