Public Quart microservice that runs pytest against shared/tests/ and
shared/sexp/tests/, serving an HTMX-powered sexp-rendered dashboard
with pass/fail/running status, auto-refresh polling, and re-run button.
No database — results stored in memory.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
base_context() doesn't include blog_url/cart_url/etc — those live in
Jinja globals. Without them call_url(ctx, "blog_url", ...) falls back
to a relative path instead of https://blog.rose-ash.com/...
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build a minimal context directly instead of relying on
get_template_context() which runs the full context processor chain
including cross-service fragment fetches. Each step (base_context,
fragments, post hydration) is independently try/excepted so the page
renders with whatever is available.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The relations container-nav fragment was inserting nav_label (e.g.
"calendars", "markets") as a URL path segment, generating wrong links
like /the-village-hall/markets/suma/ instead of /the-village-hall/suma/.
The nav_label is for display only, not URL construction.
Also adds a rich 404 handler that shows site headers and post breadcrumb
when a slug can be resolved from the URL path. Falls back gracefully to
the minimal error page if context building fails.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Payments config (SumUp credentials per page) is a cart concern since all
checkouts go through the cart service. Moves it from events.rose-ash.com
to cart.rose-ash.com/<page_slug>/admin/payments/ and adds a cart admin
overview page at /<page_slug>/admin/.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The calendar blueprint was nested under calendars (admin), making URLs
/{slug}/admin/{calendar_slug}/ instead of /{slug}/{calendar_slug}/.
Register calendar blueprint directly on the app and update all endpoint
references from calendars.calendar.* to calendar.* (37 in Python,
~50 in templates).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Cross-subdomain hx-get breaks due to OAuth redirects. When external=true,
menu-row renders a plain <a href> without HTMX attributes, allowing
normal browser navigation.
Applied to post header links on events and market services which link
back to blog.rose-ash.com.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Markets use the same admin pattern as calendars but on the market
subdomain, not the events subdomain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Change calendars blueprint prefix from /calendars to /admin
- Simplify routes from /calendars/ to / within blueprint
- Reserve admin, markets, payments, entries as calendar slugs
- Update blog admin nav link to /{slug}/admin/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Six 2-week sprints, each shipping one or two complete deliverables.
Not 20 weeks crammed into 2 — the right amount of work for the time.
Each sprint is valuable on its own. Stop after any and you've shipped.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Ghost killed by day 5, sexp protocol running internally by day 8,
sexpr.js on every page by day 10. Cut Rust client, IPFS mesh, and
browser extension to later. Everything users touch runs on sexp.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stop concatenating post title into base_title in route context.
Build proper "Post Title — Site Title" format in meta component instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Schedules all existing plans into coherent 20-week roadmap with parallel
tracks: platform stability, decoupling, entities/relations, Ghost removal,
sexp pages, internal protocol, client-side runtime, native client, and
scalability. Critical path identified through Ghost removal as linchpin.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace all 676 inline sexp() string calls across 7 services with
render(component_name, **kwargs) calls backed by 46 external .sexpr
component definition files (587 defcomps total).
- Add render() function to shared/sexp/jinja_bridge.py
- Add load_service_components() helper and update load_sexp_dir() for *.sexpr
- Update parser keyword regex to support HTMX hx-on::event syntax
- Convert remaining inline HTML in route files to render() calls
- Add shared/sexp/templates/misc.sexp for cross-service utility components
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Build and battle-test the protocol on the internal microservice mesh
before exposing it publicly. Current fetch_data/call_action/fetch_fragment
map directly to sexp verbs. Same protocol serves internal and public clients.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Strongest near-term application: replace lossy dicts and opaque HTML
fragments with structured trees that are both inspectable and renderable.
Includes incremental migration path from current fetch_data/fetch_fragment.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Covers: APIs as separate concept disappearing, front-end framework
collapse, AI as first-class citizen, browser monopoly breaking,
content portability, client-server blur, computational governance,
and the Unix pipes analogy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Verbs are no longer limited to HTTP's fixed seven methods — any symbol
is a valid verb. Domain-specific actions (reserve, publish, vote, bid)
read as natural language. Verb behaviour declared via schema endpoint.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Members' Rust clients become full peer nodes — AP instances, IPFS nodes,
and artdag GPU workers. The relay server becomes a lightweight matchmaker
(message queue, pinning, peer directory) while all compute, rendering,
and content serving is distributed across members' own hardware. Back
to the original vision of the web: everyone has a server.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merges sexpr-activitypub-extension.md and sexpr-protocol-and-tiered-clients.md
into sexpr-unified-protocol.md — recognising that browsing, federation, and
real-time updates are all the same thing: peers exchanging s-expressions on
a bidirectional stream. One format, one connection, one parser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defines three client tiers (browser HTML, browser extension with
sexpr.js, Rust native client) served from the same route handlers
via content negotiation. Includes native sexp:// protocol design
over QUIC, content-addressed caching, bidirectional streaming,
self-describing schema, and implementation plan from Phase 1
(Quart content negotiation) through Phase 7 (fallback gateway).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sexp parser doesn't handle "(" and ")" as string literals
inside expressions. Use raw! with pre-formatted strings instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates all f-string HTML from the remaining three services,
completing the migration of all sexp_components.py files to the
s-expression rendering system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Market: blog links now use market_url('/{slug}/') instead of
events_url('/{slug}/markets/'), matching the market service's
actual route structure /<page_slug>/<market_slug>/.
Calendar: flatten route from /<slug>/calendars/<calendar_slug>/
to /<slug>/<calendar_slug>/ by changing the events app blueprint
prefix and moving listing routes to explicit /calendars/ paths.
Update all hardcoded calendar URL paths across blog and events
services (Python + Jinja templates).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defines a backwards-compatible AP extension using s-expressions as
the wire format: content negotiation, component discovery protocol,
WebSocket streaming, and a path to publishing as a FEP. Includes
bidirectional JSON-LD bridging for Mastodon/Pleroma compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates every HTML tag from the market service's sexp component layer,
replacing f-string HTML with composable sexp() calls throughout ~30 functions
including product cards, filter panels, nav panels, product detail, meta tags,
cart controls, like buttons, and sentinel infinite-scroll elements.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Documents why s-expressions on the wire are a natural fit for
LLM agents: fewer tokens, no closing-tag errors, components as
tool calls, mutations as agent actions, content-addressed caching.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Separate JS content from HTML tag — pass JS body into
(script (raw! js)) so zero raw HTML tags remain.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
~39 functions converted from f-string HTML to sexp() calls.
Only remaining HTML is the intentional <script> block in
render_editor_panel (complex JS init for WYSIWYG editor).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two planning documents for the next major architectural steps:
- sexpr-js-runtime-plan: isomorphic JS s-expression runtime for
client-side rendering, content-addressed component caching,
and native hypermedia mutations
- ghost-removal-plan: full Ghost CMS replacement covering content
(Lexical→sexp), membership, newsletters, Stripe subscriptions,
and media uploads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Eliminates every raw HTML string from the events service component file.
Converted ~30 functions including ticket admin, entry cards, ticket widgets,
view toggles, entry detail, options, buy forms, slots/ticket-type tables,
calendar description forms, nav OOB panels, and cart icon.
Zero HTML tags remain in events/sexp/sexp_components.py.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
Switch all cross-service relation calls to the new registry-aware
relate/unrelate/can-relate actions, and consolidate per-service
container-nav fragment fetches into the generic relations handler.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move 24 defcomp definitions from Python string constants in components.py
to 7 grouped .sexp files under shared/sexp/templates/. Add load_sexp_dir()
to jinja_bridge.py for file-based loading. Migrate events and market
link-card fragment handlers from render_template to sexp. Delete 9
superseded Jinja HTML fragment templates.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fixes EvalError: Undefined symbol: path when rendering ~mobile-filter
component which uses an SVG <path> element.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Declarative relation registry via defrelation s-expressions with
cardinality enforcement (one-to-one, one-to-many, many-to-many),
registry-aware relate/unrelate/can-relate API endpoints, generic
container-nav fragment, and relation-driven UI components.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>