Enable cross-subdomain htmx and purify layout to sexp
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m15s

- Disable htmx selfRequestsOnly, add CORS headers for *.rose-ash.com
- Remove same-origin guards from ~menu-row and ~nav-link htmx attrs
- Convert ~app-layout from string-concatenated HTML to pure sexp tree
- Extract ~app-head component, replace ~app-shell with inline structure
- Convert hamburger SVG from Python HTML constant to ~hamburger sexp component
- Fix cross-domain fragment URLs (events_url, market_url)
- Fix starts-with? primitive to handle nil values
- Fix duplicate admin menu rows on OOB swaps
- Add calendar admin nav links (slots, description)
- Convert slots page from Jinja to sexp rendering
- Disable page caching in development mode
- Backfill migration to clean orphaned container_relations

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 12:09:00 +00:00
parent d2f1da4944
commit eda95ec58b
14 changed files with 251 additions and 160 deletions

View File

@@ -77,9 +77,23 @@ def register():
@bp.context_processor
async def inject_root():
from shared.infrastructure.fragments import fetch_fragment
container_nav_html = ""
post_data = getattr(g, "post_data", None)
if post_data:
post_id = post_data["post"]["id"]
post_slug = post_data["post"]["slug"]
container_nav_html = await fetch_fragment("relations", "container-nav", params={
"container_type": "page",
"container_id": str(post_id),
"post_slug": post_slug,
"exclude": "page->calendar",
})
return {
"calendar": getattr(g, "calendar", None),
"container_nav_html": container_nav_html,
}
# ---------- Pages ----------

View File

@@ -44,16 +44,14 @@ def register():
@bp.get("/")
async def get(**kwargs):
from shared.sexp.page import get_template_context
from sexp.sexp_components import render_slots_page, render_slots_oob
tctx = await get_template_context()
if not is_htmx_request():
# Normal browser request: full page with layout
html = await render_template(
"_types/slots/index.html",
)
html = await render_slots_page(tctx)
else:
html = await render_template(
"_types/slots/_oob_elements.html",
)
html = await render_slots_oob(tctx)
return await make_response(html)

View File

@@ -79,34 +79,31 @@ def _post_header_html(ctx: dict, *, oob: bool = False) -> str:
def _post_nav_html(ctx: dict) -> str:
"""Post desktop nav: calendar links + admin gear."""
from quart import url_for
"""Post desktop nav: calendar links + container nav (markets, etc.)."""
from quart import url_for, g
calendars = ctx.get("calendars") or []
rights = ctx.get("rights") or {}
is_admin = getattr(rights, "admin", False) if hasattr(rights, "admin") else rights.get("admin", False)
hx_select = ctx.get("hx_select_search", "#main-panel")
select_colours = ctx.get("select_colours", "")
post = ctx.get("post") or {}
slug = post.get("slug", "")
current_cal_slug = getattr(g, "calendar_slug", None)
parts = []
for cal in calendars:
cal_slug = getattr(cal, "slug", "") if hasattr(cal, "slug") else cal.get("slug", "")
cal_name = getattr(cal, "name", "") if hasattr(cal, "name") else cal.get("name", "")
href = url_for("calendars.calendar.get", calendar_slug=cal_slug)
is_sel = (cal_slug == current_cal_slug)
parts.append(sexp(
'(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc)',
'(~nav-link :href h :icon "fa fa-calendar" :label l :select-colours sc :is-selected sel)',
h=href,
l=cal_name,
sc=select_colours,
sel=is_sel,
))
if is_admin:
admin_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
parts.append(
f'<a href="{admin_href}" class="flex items-center gap-2 px-3 py-2 rounded">'
f'<i class="fa fa-cog" aria-hidden="true"></i></a>'
)
# Container nav fragments (markets, etc.)
container_nav = ctx.get("container_nav_html", "")
if container_nav:
parts.append(container_nav)
return "".join(parts)
@@ -114,21 +111,6 @@ def _post_nav_html(ctx: dict) -> str:
# Post admin header
# ---------------------------------------------------------------------------
def _post_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the post-admin-level header row."""
post = ctx.get("post") or {}
slug = post.get("slug", "")
link_href = call_url(ctx, "blog_url", f"/{slug}/admin/")
return sexp(
'(~menu-row :id "post-admin-row" :level 2'
' :link-href lh :link-label "admin" :icon "fa fa-cog"'
' :child-id "post-admin-header-child" :oob oob)',
lh=link_href,
oob=oob,
)
# ---------------------------------------------------------------------------
# Calendars header
# ---------------------------------------------------------------------------
@@ -347,11 +329,30 @@ def _day_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
# ---------------------------------------------------------------------------
def _calendar_admin_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build calendar admin header row."""
"""Build calendar admin header row with nav links."""
from quart import url_for
calendar = ctx.get("calendar")
cal_slug = getattr(calendar, "slug", "") if calendar else ""
select_colours = ctx.get("select_colours", "")
nav_parts = []
if cal_slug:
for endpoint, label in [
("calendars.calendar.slots.get", "slots"),
("calendars.calendar.admin.calendar_description_edit", "description"),
]:
href = url_for(endpoint, calendar_slug=cal_slug)
nav_parts.append(sexp(
'(~nav-link :href h :label l :select-colours sc)',
h=href, l=label, sc=select_colours,
))
nav_html = "".join(nav_parts)
return sexp(
'(~menu-row :id "calendar-admin-row" :level 4'
' :link-label "admin" :icon "fa fa-cog"'
' :child-id "calendar-admin-header-child" :oob oob)',
' :nav-html nh :child-id "calendar-admin-header-child" :oob oob)',
nh=nav_html,
oob=oob,
)
@@ -1659,7 +1660,7 @@ async def render_calendars_page(ctx: dict) -> str:
"""Full page: calendars listing."""
content = _calendars_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendars_header_html(ctx)
child = _post_header_html(ctx) + _calendars_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1667,8 +1668,8 @@ async def render_calendars_page(ctx: dict) -> str:
async def render_calendars_oob(ctx: dict) -> str:
"""OOB response: calendars listing."""
content = _calendars_main_panel_html(ctx)
oobs = _post_admin_header_html(ctx, oob=True)
oobs += _oob_header_html("post-admin-header-child", "calendars-header-child",
oobs = _post_header_html(ctx, oob=True)
oobs += _oob_header_html("post-header-child", "calendars-header-child",
_calendars_header_html(ctx))
return oob_page(ctx, oobs_html=oobs, content_html=content)
@@ -1681,7 +1682,7 @@ async def render_calendar_page(ctx: dict) -> str:
"""Full page: calendar month view."""
content = _calendar_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _calendar_header_html(ctx)
child = _post_header_html(ctx) + _calendar_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1703,7 +1704,7 @@ async def render_day_page(ctx: dict) -> str:
"""Full page: day detail."""
content = _day_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1726,7 +1727,7 @@ async def render_day_admin_page(ctx: dict) -> str:
"""Full page: day admin."""
content = _day_admin_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _day_admin_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
@@ -1750,7 +1751,7 @@ async def render_calendar_admin_page(ctx: dict) -> str:
"""Full page: calendar admin."""
content = _calendar_admin_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1765,6 +1766,32 @@ async def render_calendar_admin_oob(ctx: dict) -> str:
return oob_page(ctx, oobs_html=oobs, content_html=content)
# ---------------------------------------------------------------------------
# Slots
# ---------------------------------------------------------------------------
async def render_slots_page(ctx: dict) -> str:
"""Full page: slots listing."""
from quart import g
slots = ctx.get("slots") or []
calendar = ctx.get("calendar")
content = render_slots_table(slots, calendar)
hdr = root_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _calendar_admin_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_slots_oob(ctx: dict) -> str:
"""OOB response: slots listing."""
slots = ctx.get("slots") or []
calendar = ctx.get("calendar")
content = render_slots_table(slots, calendar)
oobs = _calendar_admin_header_html(ctx, oob=True)
return oob_page(ctx, oobs_html=oobs, content_html=content)
# ---------------------------------------------------------------------------
# Tickets
# ---------------------------------------------------------------------------
@@ -1820,7 +1847,7 @@ async def render_markets_page(ctx: dict) -> str:
"""Full page: markets listing."""
content = _markets_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _markets_header_html(ctx)
child = _post_header_html(ctx) + _markets_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1828,8 +1855,8 @@ async def render_markets_page(ctx: dict) -> str:
async def render_markets_oob(ctx: dict) -> str:
"""OOB response: markets listing."""
content = _markets_main_panel_html(ctx)
oobs = _post_admin_header_html(ctx, oob=True)
oobs += _oob_header_html("post-admin-header-child", "markets-header-child",
oobs = _post_header_html(ctx, oob=True)
oobs += _oob_header_html("post-header-child", "markets-header-child",
_markets_header_html(ctx))
return oob_page(ctx, oobs_html=oobs, content_html=content)
@@ -1842,7 +1869,7 @@ async def render_payments_page(ctx: dict) -> str:
"""Full page: payments admin."""
content = _payments_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _post_header_html(ctx) + _post_admin_header_html(ctx) + _payments_header_html(ctx)
child = _post_header_html(ctx) + _payments_header_html(ctx)
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
@@ -1850,8 +1877,8 @@ async def render_payments_page(ctx: dict) -> str:
async def render_payments_oob(ctx: dict) -> str:
"""OOB response: payments admin."""
content = _payments_main_panel_html(ctx)
oobs = _post_admin_header_html(ctx, oob=True)
oobs += _oob_header_html("post-admin-header-child", "payments-header-child",
oobs = _post_header_html(ctx, oob=True)
oobs += _oob_header_html("post-header-child", "payments-header-child",
_payments_header_html(ctx))
return oob_page(ctx, oobs_html=oobs, content_html=content)
@@ -2322,7 +2349,7 @@ async def render_entry_page(ctx: dict) -> str:
"""Full page: entry detail."""
content = _entry_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = (_post_header_html(ctx) + _post_admin_header_html(ctx)
child = (_post_header_html(ctx)
+ _calendar_header_html(ctx) + _day_header_html(ctx)
+ _entry_header_html(ctx))
hdr += sexp('(div :id "root-header-child" :class "w-full" (raw! h))', h=child)