Enable cross-subdomain htmx and purify layout to sexp

- 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

@@ -0,0 +1,30 @@
"""Backfill label + metadata for calendar relations.
Removes calendar relations with no metadata slug and no label,
which are either orphaned or were created before metadata was tracked.
Revision ID: relations_0003
Revises: relations_0002
Create Date: 2026-02-28
"""
import sqlalchemy as sa
from alembic import op
revision = "relations_0003"
down_revision = "relations_0002"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("""
DELETE FROM container_relations
WHERE child_type IN ('calendar', 'market')
AND (metadata IS NULL OR metadata->>'slug' IS NULL)
AND label IS NULL
""")
def downgrade() -> None:
pass

View File

@@ -43,6 +43,12 @@ def register():
from shared.sexp.jinja_bridge import sexp as render_sexp
from shared.sexp.relations import relations_from
from shared.services.relationships import get_children
from shared.infrastructure.urls import events_url, market_url
_SERVICE_URL = {
"calendar": events_url,
"market": market_url,
}
container_type = request.args.get("container_type", "page")
container_id = int(request.args.get("container_id", 0))
@@ -70,7 +76,17 @@ def register():
)
for child in children:
slug = (child.metadata_ or {}).get("slug", "")
href = f"/{post_slug}/{slug}/" if post_slug else f"/{slug}/"
if not slug:
continue
nav_label = defn.nav_label or ""
if post_slug and nav_label:
path = f"/{post_slug}/{nav_label}/{slug}/"
elif post_slug:
path = f"/{post_slug}/{slug}/"
else:
path = f"/{slug}/"
url_fn = _SERVICE_URL.get(defn.to_type)
href = url_fn(path) if url_fn else path
parts.append(render_sexp(
'(~relation-nav :href href :name name :icon icon :nav-class nav-class :relation-type relation-type)',
href=href,