Replace ~250 render_to_sx calls across all services with sync sx_call,
converting many async functions to sync where no other awaits remained.
Make render_to_sx/render_to_sx_with_env private (_render_to_sx).
Add (post-header-ctx) IO primitive and shared post/post-admin defmacros.
Convert built-in post/post-admin layouts from Python to register_sx_layout
with .sx defcomps. Remove dead post_admin_mobile_nav_sx.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move render functions, layouts, helpers, and utils from __init__.py
to sub-modules (renders.py, layouts.py, helpers.py, utils.py).
Update all bp route imports to point at sub-modules directly.
Each __init__.py is now ≤20 lines of setup + registration.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The service registry uses __getattr__, so .get() is interpreted
as looking up a service named "get". Use attribute access instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move remaining 19 rendering functions from the 2487-line
sx_components.py to their direct callers:
- menu_items/routes.py: menu item form, page search, nav OOB
- post/admin/routes.py: calendar view, associated entries, nav OOB
- sxc/pages/__init__.py: editor panel, post data inspector, preview,
entries browser, settings form, edit page editor
- bp/blog/routes.py: inline new post page composition
Move load_service_components() call from sx_components module-level
to setup_blog_pages() so .sx files still load at startup.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move _blog_header_sx, _settings_header_sx, _settings_nav_sx, and
_sub_settings_header_sx into the layout module as local helpers.
Eliminates 14 imports from sx_components.py for the layout system.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Home page: inline shared helpers, render_to_sx("blog-home-main")
- Post detail: new ~blog-post-detail-content defcomp with data from service
- Like toggle: call render_to_sx("market-like-toggle-button") directly
- Add post_meta_data() and post_detail_data() to BlogPageService
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
BlogPageService.index_data() assembles all data (cards, filters, actions)
and 7 new .sx defcomps handle rendering: main content, aside, filter,
actions, tag groups filter, authors filter, and sentinel.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Python no longer generates s-expression strings. All SX rendering now
goes through render_to_sx() which builds AST from native Python values
and evaluates via async_eval_to_sx() — no SX string literals in Python.
- Add render_to_sx()/render_to_html() infrastructure in shared/sx/helpers.py
- Add (abort status msg) IO primitive in shared/sx/primitives_io.py
- Convert all 9 services: ~650 sx_call() invocations replaced
- Convert shared helpers (root_header_sx, full_page_sx, etc.) to async
- Fix likes service import bug (likes.models → models)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Defpages are now declared with absolute paths in .sx files and auto-mounted
directly on the Quart app, removing ~850 lines of blueprint mount_pages calls,
before_request hooks, and g.* wrapper boilerplate. A new page = one defpage
declaration, nothing else.
Infrastructure:
- async_eval awaits coroutine results from callable dispatch
- auto_mount_pages() mounts all registered defpages on the app
- g._defpage_ctx pattern passes helper data to layout context
Migrated: sx, account, orders, federation, cart, market, events, blog
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace Python GET page handlers with declarative defpage definitions in .sx
files across all 8 apps (sx docs, orders, account, market, cart, federation,
events, blog). Each app now has sxc/pages/ with setup functions, layout
registrations, page helpers, and .sx defpage declarations.
Core infrastructure: add g I/O primitive, PageDef support for auth/layout/
data/content/filter/aside/menu slots, post_author auth level, and custom
layout registration. Remove ~1400 lines of render_*_page/render_*_oob
boilerplate. Update all endpoint references in routes, sx_components, and
templates to defpage_* naming.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Model: add sx_content column to Post. Writer: accept sx_content in
create_post, create_page, update_post. Routes: read sx_content from form
data in new post, new page, and edit routes. Read pipeline: ghost_db
includes sx_content in public dict, detail/home views prefer sx_content
over html when available, PostDTO includes sx_content.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>
- 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>
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>
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>
Migrate ~52 GET route handlers across all 7 services from Jinja
render_template() to s-expression component rendering. Each service
gets a sexp_components.py with page/oob/cards render functions.
- Add per-service sexp_components.py (account, blog, cart, events,
federation, market, orders) with full page, OOB, and pagination
card rendering
- Add shared/sexp/helpers.py with call_url, root_header_html,
full_page, oob_page utilities
- Update all GET routes to use get_template_context() + render fns
- Fix get_template_context() to inject Jinja globals (URL helpers)
- Add qs_filter to base_context for sexp filter URL building
- Mount sexp_components.py in docker-compose.dev.yml for all services
- Import sexp_components in app.py for Hypercorn --reload watching
- Fix route_prefix import (shared.utils not shared.infrastructure.urls)
- Fix federation choose-username missing actor in context
- Fix market page_markets missing post in context
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
AP blueprints (activitypub.py, ap_social.py) were querying federation
tables (ap_actor_profiles etc.) on g.s which points to the app's own DB
after the per-app split. Now uses g._ap_s backed by get_federation_session()
for non-federation apps.
Also hardens Ghost sync before_app_serving to catch/rollback on failure
instead of crashing the Hypercorn worker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Prevents PgBouncer connection pool from inheriting dirty
transaction state when the non-syncing worker returns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two Hypercorn workers both run sync_all_content_from_ghost on startup,
racing on PostAuthor/PostTag rows. Use pg_try_advisory_lock so only
one worker runs the sync.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move Ghost membership sync from blog to account service so blog no
longer queries account tables (users, ghost_labels, etc.). Account
runs membership sync at startup and exposes HTTP action/data endpoints
for webhook-triggered syncs and user lookups.
Key changes:
- account/services/ghost_membership.py: all membership sync functions
- account/bp/actions + data: ghost-sync-member, user-by-email, newsletters
- blog ghost_sync.py: stripped to content-only (posts, authors, tags)
- blog webhook member: delegates to account via call_action()
- try_publish: opens federation session when DBs differ
- oauth.py callback: uses get_account_session() for OAuthCode
- page_configs moved from db_events to db_blog in split script
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Missed cross-app call in blog/bp/blog/routes.py:127 caused
CartService not registered error on blog.rose-ash.com homepage.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Combines shared, blog, market, cart, events, federation, and account
into a single repository. Eliminates submodule sync, sibling model
copying at build time, and per-app CI orchestration.
Changes:
- Remove per-app .git, .gitmodules, .gitea, submodule shared/ dirs
- Remove stale sibling model copies from each app
- Update all 6 Dockerfiles for monorepo build context (root = .)
- Add build directives to docker-compose.yml
- Add single .gitea/workflows/ci.yml with change detection
- Add .dockerignore for monorepo build context
- Create __init__.py for federation and account (cross-app imports)