- Fix O(n²) postprocessing: compute_all_deps/io_refs/hash were called
per-file (92x for sx app). Now deferred to single finalize_components()
call after all files load.
- Add pickle cache in shared/sx/.cache/ keyed by file mtimes+sizes.
Cache stores fully-processed Component/Island/Macro objects with deps,
io_refs, and css_classes pre-computed. Closures stripped before pickle,
rebuilt from global env after restore.
- Smart finalization: cached loads skip deps/io_refs recomputation
(already in pickle), only recompute component hash.
- Fix ~sx-header → ~layouts/header ref in docs-content.sx
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Configurable page shell (~sx-page-shell kwargs + SX_SHELL app config)
so each app controls its own assets — sx docs loads only sx-browser.js
- SX-evaluated sx-on:* handlers (eval-expr instead of new Function)
with DOM primitives registered in PRIMITIVES table
- data-init boot mode for pure SX initialization scripts
- Jiggle animation on links while fetching
- Nav: 3-column grid for centered alignment, is-leaf sizing,
fix map-indexed param order (index, item), guard mod-by-zero
- Async route eval failure now falls back to server fetch
instead of silently rendering nothing
- Remove duplicate h1 title from ~doc-page
- Re-bootstrap sx-ref.js + sx-browser.js
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The sx app is stateless — no database needed. In standalone mode
(SX_STANDALONE=true), the factory now skips register_db() so the app
doesn't crash trying to connect to a non-existent PostgreSQL.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- SX_STANDALONE=true env var: no OAuth, no root header, no cross-service
fragments. Same image runs in both rose-ash cooperative and standalone.
- Factory: added no_oauth parameter to create_base_app()
- Standalone layout defcomps skip ~root-header-auto/~root-mobile-auto
- Fixed Dockerfile: was missing sx/sx/ component directory copy
- CI: deploys sx-web swarm stack on main branch when sx changes
- Stack config at ~/sx-web/ (Caddy → sx_docs, Redis)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dict values (e.g. {:X-CSRFToken csrf}) passed as component kwargs were
not being evaluated through sxEval — symbols stayed unresolved in the DOM.
Also add Cache-Control: no-cache headers for /static/ in dev mode so
browser always fetches fresh JS/CSS without needing hard refresh.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The inter-service data layer (fetch_data/call_action) was the least
structured part of the codebase — Python _handlers dicts with ad-hoc
param extraction scattered across 16 route files. This replaces them
with declarative .sx query/action definitions that make the entire
inter-service protocol self-describing and greppable.
Infrastructure:
- defquery/defaction special forms in the sx evaluator
- Query/action registry with load, lookup, and schema introspection
- Query executor using async_eval with I/O primitives
- Blueprint factories (create_data_blueprint/create_action_blueprint)
with sx-first dispatch and Python fallback
- /internal/schema endpoint on every service
- parse-datetime and split-ids primitives for type coercion
Service extractions:
- LikesService (toggle, is_liked, liked_slugs, liked_ids)
- PageConfigService (ensure, get_by_container, get_by_id, get_batch, update)
- RelationsService (wraps module-level functions)
- AccountDataService (user_by_email, newsletters)
- CartItemsService, MarketDataService (raw SQLAlchemy lookups)
50 of 54 handlers converted to sx, 4 Python fallbacks remain
(ghost-sync/push-member, clear-cart-for-order, create-order).
Net: -1,383 lines Python, +251 lines modified.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add primitives (replace, strip-tags, slice, csrf-token), convert all
social blueprint routes and federation profile to SX content builders,
delete 12 unused Jinja templates and social_lite layout.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- OOB nav updates: AJAX navigation now swaps both menu bar levels
(main nav highlighting + sub-nav with current page) using the same
oob_header_sx/oob_page_sx pattern as blog/market/events
- Enable OAuth for sx and test apps (removed from _NO_OAUTH, added sx
to ALLOWED_CLIENTS, added app_urls for sx/test/orders)
- Fetch real cross-service fragments (cart-mini, auth-menu, nav-tree)
instead of hardcoding empty values
- Add :selected param to ~menu-row-sx for white text current-page label
- Fix duplicate element IDs: use menu-row-sx child_id/child mechanism
instead of manual header_child_sx wrappers
- Fix home page copy: "Server-rendered DOM over the wire (no HTML)"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- 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>
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>
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>
- 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>
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>
- 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>
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>
- Add setup_sexp_bridge() and load_shared_components() to factory.py
so all services get s-expression support automatically
- Create shared/sexp/components.py with ~link-card component definition
(replaces 5 per-service Jinja link_card.html templates)
- Replace blog's link-card fragment handler to use sexp() instead of
render_template() — first real s-expression rendered page content
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Phase 1 - Relations service (internal): owns ContainerRelation, exposes
get-children data + attach/detach-child actions. Retargeted events, blog,
market callers from cart to relations.
Phase 2 - Likes service (internal): unified Like model replaces ProductLike
and PostLike with generic target_type/target_slug/target_id. Exposes
is-liked, liked-slugs, liked-ids data + toggle action.
Phase 3 - PageConfig → blog: moved ownership to blog with direct DB queries,
removed proxy endpoints from cart.
Phase 4 - Orders service (public): owns Order/OrderItem + SumUp checkout
flow. Cart checkout now delegates to orders via create-order action.
Webhook/return routes and reconciliation moved to orders.
Phase 5 - Infrastructure: docker-compose, deploy.sh, Dockerfiles updated
for all 3 new services. Added orders_url helper and factory model imports.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Critical: Add ownership checks to all order routes (IDOR fix).
High: Redis rate limiting on auth endpoints, HMAC-signed internal
service calls replacing header-presence-only checks, nh3 HTML
sanitization on ghost_sync and product import, internal auth on
market API endpoints, SHA-256 hashed OAuth grant/code tokens.
Medium: SECRET_KEY production guard, AP signature enforcement,
is_admin param removal, cart_sid validation, SSRF protection on
remote actor fetch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_load_user ran before _check_auth_state, so g.user was set to the wrong
user before the grant check could clear the stale session. Now grant
verification runs first, ensuring stale sessions are cleared before
the user is loaded.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two issues fixed:
- Sessions with uid but no grant_token (legacy or corrupt) were not
validated at all, allowing a user to be logged in as whoever got
their old numeric user ID after a DB rebuild
- DB errors during grant verification silently kept stale sessions
alive; now treated as invalid to fail-safe
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Each service (blog, market, cart, events, federation, account) now owns
its own database schema with independent Alembic migrations. Removes the
monolithic shared/alembic/ that ran all migrations against a single DB.
- Add per-service alembic.ini, env.py, and 0001_initial.py migrations
- Add shared/db/alembic_env.py helper with table-name filtering
- Fix cross-DB FK in blog/models/snippet.py (users lives in db_account)
- Fix cart_impl.py cross-DB queries: fetch products and market_places
via internal data endpoints instead of direct SQL joins
- Fix blog ghost_sync to fetch page_configs from cart via data endpoint
- Add products-by-ids and page-config-ensure data endpoints
- Update all entrypoint.sh to create own DB and run own migrations
- Cart now uses db_cart instead of db_market
- Add docker-compose.dev.yml, dev.sh for local development
- CI deploys both rose-ash swarm stack and rose-ash-dev compose stack
- Fix Quart namespace package crash (root_path in factory.py)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Lightweight social pages (search, follow/unfollow, followers, following,
actor timeline) auto-registered for AP-enabled apps via shared blueprint.
Federation keeps the full social hub. Followers scoped per app_domain;
post cards show "View on Hub" link instead of interaction buttons.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Redis: per-app DB index (0-5) with shared auth DB 15 for SSO keys;
flushdb replaces flushall so deploys don't wipe cross-app auth state.
Postgres: drop 13 cross-domain FK constraints (migration v2t0p8q9r0),
remove dead ORM relationships, add explicit joins for 4 live ones.
Multi-engine sessions (account + federation) ready for per-domain DBs
via DATABASE_URL_ACCOUNT / DATABASE_URL_FEDERATION env vars.
All URLs initially point to the same appdb — zero behaviour change
until split-databases.sh is run to migrate data to per-domain DBs.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
_load_user runs before _check_auth_state, so g.user was already set
when the grant was found revoked. The session was cleared but g.user
stayed populated, causing the template to render the signed-in UI
for one request after logout. Now sets g.user = None alongside the
session clear.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Delete shared/contracts/widgets.py, shared/services/widget_registry.py,
and shared/services/widgets/ (empty stubs). Remove register_all_widgets()
from factory and widgets Jinja global from jinja_setup. Zero consumers
remain — all cross-app UI composition now uses the fragment API.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Cart _cart.html: replace url_for('market.browse.product...') with
market_product_url() for links and cart_global.update_quantity for
quantity forms (market endpoints don't exist in cart app)
- Factory favicon route: use STATIC_DIR instead of relative "static"
(resolves to shared/static/ where favicon.ico actually lives)
- Cart context processor: fetch all 3 fragments (cart-mini, auth-menu,
nav-tree) concurrently, matching pattern in all other apps
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)