Commit Graph

216 Commits

Author SHA1 Message Date
3bc5de126d Add cache busting instruction for sx.js to CLAUDE.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 23:03:51 +00:00
1447122a0c Add on-demand CSS: registry, pre-computed component classes, header compression
- Parse tw.css into per-class lookup registry at startup
- Pre-scan component CSS classes at registration time (avoid per-request regex)
- Compress SX-Css header: 8-char hash replaces full class list (LRU cache)
- Add ;@css comment annotation for dynamically constructed class names
- Safelist bg-sky-{100..400} in Tailwind config for menu-row-sx dynamic shades
- Client sends/receives hash, falls back gracefully on cache miss

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 21:39:57 +00:00
ab45e21c7c Cache-bust sx.js and disable static file caching
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:46:26 +00:00
c0d369eb8e Refactor SX templates: shared components, Python migration, cleanup
- Extract shared components (empty-state, delete-btn, sentinel, crud-*,
  view-toggle, img-or-placeholder, avatar, sumup-settings-form, auth
  forms, order tables/detail/checkout)
- Migrate all Python sx_call() callers to use shared components directly
- Remove 55+ thin wrapper defcomps from domain .sx files
- Remove trivial passthrough wrappers (blog-header-label, market-card-text, etc)
- Unify duplicate auth flows (account + federation) into shared/sx/templates/auth.sx
- Unify duplicate order views (cart + orders) into shared/sx/templates/orders.sx
- Disable static file caching in dev (SEND_FILE_MAX_AGE_DEFAULT=0)
- Add SX response validation and debug headers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:34:34 +00:00
755313bd29 Add market admin CRUD: list, create, and delete marketplaces
Replaces placeholder "Market admin" text with a functional admin panel
that lists marketplaces for a page and supports create/delete via sx,
mirroring the events calendar admin pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:16:39 +00:00
01a67029f0 Replace Tailwind CDN with pre-built CSS via standalone CLI
- Add shared/static/styles/tailwind.css as Tailwind v4 input with
  explicit @source paths for all service templates and safelisted
  dynamic classes (bg-{colour}-{shade}, text-{size})
- Build to shared/static/styles/tw.css (93KB minified)
- Replace <script src="cdn.tailwindcss.com"> with <link> to tw.css
  in sx page shell, Jinja _head.html, and ~base-shell component
- Add build-tw.sh convenience script

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:23:20 +00:00
b54f7b4b56 Fix SX history, OOB header swaps, cross-service nav components
- Always re-fetch on popstate (drop LRU cache) for fresh content on back/forward
- Save/restore scroll position via pushState
- Add id="root-header-child" to ~app-body so OOB swaps can target it
- Fix OOB renderers: nest root-row inside root-header-child swap instead of
  separate OOB that clobbers it
- Fix 3+ header rows dropped: wrap all headers in single fragment instead of
  concatenating outside (<> ...)
- Strip <script data-components> from text/sx responses before renderToString
- Fall back to location.assign for cross-origin pushState (SecurityError)
- Move blog/sx/nav.sx to shared/sx/templates/ so all services have nav components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 17:17:39 +00:00
5ede32e21c Activate regular script tags after sx swap operations
Scripts inserted via innerHTML/insertAdjacentHTML don't execute.
Add _activateScripts() to _swapContent that recreates script tags
(without type or type=text/javascript) as live elements. This fixes
editor.js not loading when navigating to edit pages via sx-get.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:39:07 +00:00
7aea1f1be9 Activate script tags in raw! DOM output
Scripts inserted via innerHTML (template.content) don't execute.
When raw! renders HTML containing <script> tags, recreate them as
live elements so the browser fetches and executes them. Fixes
editor.js not loading on HTMX navigation to edit pages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:25:52 +00:00
0ef4a93a92 Wrap raw Jinja HTML in (raw! "...") for sx source embedding
Post edit, data, entries, and settings pages pass raw Jinja HTML
as content to full_page_sx/oob_page_sx, which wraps it in SxExpr().
This injects unescaped HTML directly into sx source, breaking the
parser. Fix by serializing the HTML into a (raw! "...") expression
that the sx evaluator renders unescaped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 15:16:42 +00:00
48696498ef Wrap multi-expression sx returns in fragments to prevent kwarg truncation
When multiple sx expressions are concatenated and passed as a kwarg
value via SxExpr(), the parser only sees the first as the value — the
rest become extra args silently dropped by the component. Wrap in (<>)
fragments in render_editor_panel() and _page_cards_sx().

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:56:05 +00:00
b7d95a8b4e Fix sx.js component kwarg evaluation: distinguish expressions from data
Three issues with the eager kwarg evaluation in renderComponentDOM and
renderStrComponent:

1. Data arrays (e.g. tags list of dicts) were being passed to sxEval
   which tried to call a dict as a function — causing blank pages.
   Fix: only evaluate arrays with a Symbol head (actual expressions);
   pass data arrays through as-is.

2. Expression arrays like (get t "src") inside map lambdas lost their
   scope when deferred — causing "get,t,src" URLs. Fix: eagerly evaluate
   these Symbol-headed expressions in the caller's env.

3. Bare symbol `t` used as boolean in editor.sx threw "Undefined symbol".
   Fix: use `true` literal instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:51:07 +00:00
e7d5c6734b Fix renderDOM swallowing pre-rendered DOM nodes as empty dicts
renderComponentDOM now eagerly renders kwarg values that are render
expressions (HTML tags, <>, ~components) into DOM nodes. But renderDOM
treated any non-array object as a dict and returned an empty fragment,
silently discarding pre-rendered content. Add a nodeType check to pass
DOM nodes through unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 14:41:51 +00:00
e4a6d2dfc8 Fix renderStrComponent with same eager-eval pattern as renderComponentDOM
The string renderer's component call had the same deferred-evaluation
bug — and this is the path actually used for blog card rendering via
renderToString. Apply the same _isRenderExpr check to route render-only
forms through renderStr while data expressions go through sxEval.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:50:42 +00:00
0a5562243b Fix renderComponentDOM: route render-only forms through renderDOM
The previous fix eagerly evaluated all kwarg expressions via sxEval,
which broke render-only forms (<>, raw!, HTML tags, ~components) that
only exist in the render pipeline. Now detect render expressions by
checking if the head symbol is an HTML/SVG tag, <>, raw!, or ~component,
and route those through renderDOM while data expressions still go
through sxEval for correct scope resolution.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:45:43 +00:00
2b41aaa6ce Fix renderComponentDOM evaluating kwarg expressions in wrong scope
renderComponentDOM was deferring evaluation of complex expressions
(arrays) passed as component kwargs, storing raw AST instead.  When the
component body later used these values as attributes, the caller's env
(with lambda params like t, a) was no longer available, producing
stringified arrays like "get,t,src" as attribute values — which browsers
interpreted as relative URLs.

Evaluate all non-literal kwarg values eagerly in the caller's env,
matching the behavior of callComponent and the Python-side renderer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:40:50 +00:00
cfe66e5342 Fix back_populates typo in Post.authors relationship
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:36:18 +00:00
382d1b7c7a Decouple blog models and BlogService from shared layer
Move Post/Author/Tag/PostAuthor/PostTag/PostUser models from
shared/models/ghost_content.py to blog/models/content.py so blog-domain
models no longer live in the shared layer. Replace the shared
SqlBlogService + BlogService protocol with a blog-local singleton
(blog_service), and switch entry_associations.py from direct DB access
to HTTP fetch_data("blog", "post-by-id") to respect the inter-service
boundary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 13:28:11 +00:00
a580a53328 Fix alembic revision IDs to match existing naming convention
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:38:43 +00:00
0f9af31ffe Phase 0+1: native post writes, Ghost no longer write-primary
- Final sync script with HTML verification + author→user migration
- Make ghost_id nullable on posts/authors/tags, add UUID/timestamp defaults
- Add user profile fields (bio, slug, profile_image, etc.) to User model
- New PostUser M2M table (replaces post_authors for new posts)
- PostWriter service: direct DB CRUD with Lexical rendering, optimistic
  locking, AP federation, tag upsert
- Rewrite create/edit/settings routes to use PostWriter (no Ghost API calls)
- Neuter Ghost webhooks (post/page/author/tag → 204 no-op)
- Disable Ghost startup sync

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 12:33:37 +00:00
e8bc228c7f Rebrand sexp → sx across web platform (173 files)
Rename all sexp directories, files, identifiers, and references to sx.
artdag/ excluded (separate media processing DSL).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 11:06:57 +00:00
17cebe07e7 Add sx-get to cross-domain cart and auth-menu fragment links
Cart mini and auth-menu components were rendering plain <a href>
links for cross-domain navigation. Add sx-get with OOB swap
attributes so these use the SX fetch path instead of full reloads.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:47:24 +00:00
82b411f25a Add cross-domain SX navigation with OOB swap
Enable instant cross-subdomain navigation (blog → market, etc.) via
sx-get instead of full page reloads. The server prepends missing
component definitions to OOB responses so the client can render
components from other domains.

- sexp.js: send SX-Components header, add credentials for cross-origin
  fetches to .rose-ash.com/.localhost, process sexp scripts in response
  before OOB swap
- helpers.py: add components_for_request() to diff client/server
  component sets, update sexp_response() to prepend missing defs
- factory.py: add SX-Components to CORS allowed headers, add
  Access-Control-Allow-Methods
- fragments/routes.py: switch nav items from ~blog-nav-item-plain to
  ~blog-nav-item-link (sx-get enabled)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:33:12 +00:00
a643b3532d Phase 5 cleanup: remove legacy HTML components, fix nav-tree fragment
- Remove old raw! layout components (~app-head, ~app-layout, ~oob-response,
  ~header-row, ~menu-row, ~oob-header, ~header-child) from layout.sexp
- Convert nav-tree fragment from Jinja HTML to sexp source, fixing the
  "Unexpected character: ." parse error caused by HTML leaking into sexp
- Add _as_sexp() helper to safely coerce HTML fragments to ~rich-text
- Fix federation/sexp/search.sexpr extra closing paren
- Remove dead _html() wrappers from blog and account sexp_components
- Remove stale render import from cart sexp_components
- Add dev_watcher.py to auto-reload on .sexp/.sexpr/.js/.css changes
- Add test_parse_all.py to parse-check all 59 sexpr/sexp files
- Fix test assertions for sx- attribute prefix (was hx-)
- Add sexp.js version logging for cache debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 10:12:03 +00:00
22802bd36b Send all responses as sexp wire format with client-side rendering
- Server sends sexp source text, client (sexp.js) renders everything
- SexpExpr marker class for nested sexp composition in serialize()
- sexp_page() HTML shell with data-mount="body" for full page loads
- sexp_response() returns text/sexp for OOB/partial responses
- ~app-body layout component replaces ~app-layout (no raw!)
- ~rich-text is the only component using raw! (for CMS HTML content)
- Fragment endpoints return text/sexp, auto-wrapped in SexpExpr
- All _*_html() helpers converted to _*_sexp() returning sexp source
- Head auto-hoist: sexp.js moves meta/title/link/script[ld+json]
  from rendered body to document.head automatically
- Unknown components render warning box instead of crashing page
- Component kwargs preserve AST for lazy rendering (fixes <> in kwargs)
- Fix unterminated paren in events/sexp/tickets.sexpr

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:45:07 +00:00
0d48fd22ee Add test service to CI build loop
The test service was missing from the CI app list, so its Docker
image was never rebuilt on push (no Node.js for sexp.js parity tests).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:54:40 +00:00
b92e7a763e Use lazy import for quart.Response in sexp_response helper
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:46:58 +00:00
fec5ecdfb1 Add s-expression wire format support and test detail view
- HTMX beforeSwap hook intercepts text/sexp responses and renders
  them client-side via sexp.js before HTMX swaps the result in
- sexp_response() helper for returning text/sexp from route handlers
- Test detail page (/test/<nodeid>) with clickable test names
- HTMX navigation to detail returns sexp wire format (4x smaller
  than pre-rendered HTML), full page loads render server-side
- ~test-detail component with back link, outcome badge, and
  error traceback display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:45:28 +00:00
269bcc02be Send test dashboard component definitions to client via sexp.js
Uses client_components_tag() to emit all component definitions as
<script type="text/sexp" data-components> before </body>, making them
available for client-side rendering by sexp.js.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:42:42 +00:00
9f2f0dacaf Add update/hydrate methods and browser auto-init to sexp.js
Adds Sexp.update() for re-rendering data-sexp elements with new data,
Sexp.hydrate() for finding and rendering all [data-sexp] elements,
and auto-init on DOMContentLoaded + htmx:afterSwap integration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:40:14 +00:00
39e013a75e Wire sexp.js into page template with auto-init and HTMX integration
- Load sexp.js in ~app-layout before body.js
- Auto-process <script type="text/sexp"> tags on DOMContentLoaded
- Re-process after htmx:afterSwap for dynamic content
- Sexp.mount(target, expr, env) for rendering into DOM elements
- Sexp.processScripts() picks up data-components and data-mount tags
- client_components_tag() Python helper serializes Component objects
  back to sexp source for client-side consumption
- 37 parity tests all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:36:49 +00:00
2df1014ee3 Add Node.js to test containers for sexp.js parity tests
Node 20 from Debian packages — needed to run test_sexp_js.py which
verifies JS renderer output matches Python renderer output.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:30:17 +00:00
e8a991834b Add sexp.js: client-side s-expression parser, evaluator, and DOM renderer
Vanilla JS (no build tools) counterpart to shared/sexp/ Python modules.
Parses s-expression text, evaluates special forms, and renders to DOM
nodes or HTML strings. Full component system with defcomp/~name.

Includes:
- Parser: tokenizer + parse/parseAll matching Python parser exactly
- Evaluator: all special forms (if, when, cond, let, and, or, lambda,
  defcomp, define, ->, set!), higher-order forms (map, filter, reduce)
- DOM renderer: createElement for HTML tags, SVG namespace support,
  component invocation, raw! for pre-rendered HTML, <> fragments
- String renderer: matches Python html.render output for SSR parity
- ~50 built-in primitives (arithmetic, string, collection, predicates)
- 35 parity tests verifying JS output matches Python output via Node.js

Also fixes Python raw! handler to properly unwrap _RawHTML objects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:28:21 +00:00
bc7a4a5128 Add cross-service URL functions and rights to base_context
blog_url, market_url, cart_url, events_url and g.rights were only
available as Jinja globals, not in the ctx dict passed to sexp
helper functions. This caused all cross-service links in the header
system (post title, cart badge, admin cog, admin nav items) to
produce relative URLs resolving to the current service domain.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:19:42 +00:00
8e4c2c139e Fix duplicate menu rows on HTMX navigation between depth levels
When navigating from a deeper page (e.g. day) to a shallower one
(e.g. calendar) via HTMX, orphaned header rows from the deeper page
persisted in the DOM because OOB swaps only replaced specific child
divs, not siblings. Fix by sending empty OOB swaps to clear all
header row IDs not present at the current depth.

Applied to events (calendars/calendar/day/entry/admin/slots) and
market (market_home/browse/product/admin). Also restore app_label
in root header.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:09:15 +00:00
db3f48ec75 Remove app_label text from root header, keep settings cog
The word "settings" (app_label) was showing next to "Rose Ash 2.0"
in the top bar. Removed that label while restoring the settings cog
icon on the right side of the menu bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 23:03:46 +00:00
b40f3d124c Remove settings cog from root header bar
The settings page is accessible via its own route; no need for a
persistent cog icon next to Rose Ash 2.0.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:59:33 +00:00
3809affcab Test dashboard: full menu system, all-service tests, filtering
- Run tests for all 10 services via per-service pytest subprocesses
- Group results by service with section headers
- Clickable summary cards filter by outcome (passed/failed/errors/skipped)
- Service filter nav using ~nav-link buttons in menu bar
- Full menu integration: ~header-row + ~header-child + ~menu-row
- Show logo image via cart-mini rendering
- Mount full service directories in docker-compose for test access
- Add 24 unit test files across 9 services

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:54:25 +00:00
81e51ae7bc Fix settings cog URL: /settings/ not /admin/
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:50:16 +00:00
b6119b7f04 Show settings cog on root header for admin users
Pass settings_url and is_admin to header-row component so the blog
settings cog appears on the root header row for admin users across
all services. Links to blog /admin/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:47:32 +00:00
75cb5d43b9 Apply generic admin header pattern to all events admin pages
Events admin pages (calendars, calendar admin, day admin, entry admin,
slots, slot detail) now use shared post_admin_header_html with
selected="calendars". Container nav is fetched via fragments so post
header row matches other services.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:46:00 +00:00
f628b35fc3 Make post header row generic: admin cog + container_nav in shared helper
Move admin cog generation and container_nav border wrapping from
blog-specific wrapper into shared post_header_html so all services
render identical post header rows. Blog, events, cart all delegate
to the shared helper now. Cart admin pages fetch container_nav_html
via fragments. Village Hall always links to blog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:37:24 +00:00
2e4fbd5777 Remove extra cart header row from admin pages, use shared post header
Cart admin pages (admin overview, payments) now use the same header
pattern as blog/market/events: root_header → post_header → admin_header.
The domain name appears via app_label on the root header instead of a
separate level-1 "cart" row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:17:36 +00:00
b47ad6224b Unify post admin nav across all services
Move post admin header into shared/sexp/helpers.py so blog, cart,
events, and market all render the same admin row with identical nav:
calendars | markets | payments | entries | data | edit | settings.

All links are external (cross-service). The selected item shows
highlighted on the right and as white text next to "admin" on the left.

- blog: delegates to shared helper, removes blog-specific nav builder
- cart: delegates to shared helper for payments admin
- events: adds shared admin row (selected=calendars) to calendar admin
- market: adds /<slug>/admin/ route + page_admin blueprint, delegates
  to shared helper (selected=markets). Fixes 404 on page-level admin.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 22:01:56 +00:00
2d08d6f787 Eliminate payments sub-admin row in cart, show selection on admin label
Same pattern as blog: remove the level-3 payments header row, instead
show "payments" in white text next to "admin" on the admin row.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:35:02 +00:00
beebe559cd Show selected sub-page name in white next to admin label
Appends e.g. "settings" in white text next to the admin shield icon
on the left side of the admin row, in addition to the highlighted
nav button on the right.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:28:27 +00:00
b63aa72efb Fix admin nav selection: use !important to override text-black
The direct bg-stone-500 text-white classes were losing to text-black
in Tailwind specificity. Use !bg-stone-500 !text-white to ensure
selected admin nav items display correctly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 21:27:02 +00:00
8cfa12de6b Eliminate post sub-admin rows, highlight active nav on admin row
Remove the separate sub-admin header rows (data, entries, edit, settings)
that caused duplicate/stale rows on HTMX navigation and font styling breaks.
Instead, pass selected= to the admin row to highlight the active nav item
via aria-selected styling. External nav items (calendars, markets, payments)
also gain is-selected and select-colours support.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:40:03 +00:00
3dd62bd9bf Bigger text in test dashboard + add deliberate failing test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:34:19 +00:00
c926e5221d Fix test dashboard: use raw! for pre-rendered table rows
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:32:40 +00:00