diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 4d38535..1eec7f5 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -84,13 +84,25 @@ jobs: fi done - # Deploy swarm stack only on main branch + # Deploy swarm stacks only on main branch if [ '${{ github.ref_name }}' = 'main' ]; then source .env docker stack deploy -c docker-compose.yml rose-ash echo 'Waiting for swarm services to update...' sleep 10 docker stack services rose-ash + + # Deploy sx-web standalone stack (sx-web.org) + SX_REBUILT=false + if [ \"\$REBUILD_ALL\" = true ] || echo \"\$CHANGED\" | grep -q '^sx/'; then + SX_REBUILT=true + fi + if [ \"\$SX_REBUILT\" = true ]; then + echo 'Deploying sx-web stack (sx-web.org)...' + docker stack deploy -c /root/sx-web/docker-compose.yml sx-web + sleep 5 + docker stack services sx-web + fi else echo 'Skipping swarm deploy (branch: ${{ github.ref_name }})' fi diff --git a/shared/infrastructure/factory.py b/shared/infrastructure/factory.py index 601012f..ce99672 100644 --- a/shared/infrastructure/factory.py +++ b/shared/infrastructure/factory.py @@ -47,6 +47,7 @@ def create_base_app( context_fn: Callable[[], Awaitable[dict]] | None = None, before_request_fns: Sequence[Callable[[], Awaitable[None]]] | None = None, domain_services_fn: Callable[[], None] | None = None, + no_oauth: bool = False, ) -> Quart: """ Create a Quart app with shared infrastructure. @@ -156,7 +157,7 @@ def create_base_app( # Auto-register OAuth client blueprint for non-account apps # (account is the OAuth authorization server) _NO_OAUTH = {"account"} - if name not in _NO_OAUTH: + if name not in _NO_OAUTH and not no_oauth: from shared.infrastructure.oauth import create_oauth_blueprint app.register_blueprint(create_oauth_blueprint(name)) @@ -205,7 +206,7 @@ def create_base_app( # Auth state check via grant verification + silent OAuth handshake # MUST run before _load_user so stale sessions are cleared first - if name not in _NO_OAUTH: + if name not in _NO_OAUTH and not no_oauth: @app.before_request async def _check_auth_state(): from quart import session as qs diff --git a/sx/Dockerfile b/sx/Dockerfile index 166a76d..7a12ba6 100644 --- a/sx/Dockerfile +++ b/sx/Dockerfile @@ -26,6 +26,7 @@ COPY sx/ ./sx-app-tmp/ RUN cp -r sx-app-tmp/app.py sx-app-tmp/path_setup.py \ sx-app-tmp/bp sx-app-tmp/sxc sx-app-tmp/services \ sx-app-tmp/content sx-app-tmp/__init__.py ./ 2>/dev/null || true && \ + ([ -d sx-app-tmp/sx ] && cp -r sx-app-tmp/sx ./sx || true) && \ rm -rf sx-app-tmp # Sibling models for cross-domain SQLAlchemy imports diff --git a/sx/app.py b/sx/app.py index 92a0c50..5d19739 100644 --- a/sx/app.py +++ b/sx/app.py @@ -1,10 +1,12 @@ from __future__ import annotations +import os import path_setup # noqa: F401 -from shared.infrastructure.factory import create_base_app from bp import register_pages from services import register_domain_services +SX_STANDALONE = os.getenv("SX_STANDALONE") == "true" + async def sx_docs_context() -> dict: """SX docs app context processor — fetches cross-service fragments.""" @@ -39,11 +41,29 @@ async def sx_docs_context() -> dict: return ctx +async def sx_standalone_context() -> dict: + """Minimal context for standalone mode — no cross-service fragments.""" + from shared.infrastructure.context import base_context + ctx = await base_context() + ctx["menu_items"] = [] + ctx["cart_mini"] = "" + ctx["auth_menu"] = "" + ctx["nav_tree"] = "" + return ctx + + def create_app() -> "Quart": + from shared.infrastructure.factory import create_base_app + + extra_kw = {} + if SX_STANDALONE: + extra_kw["no_oauth"] = True + app = create_base_app( "sx", - context_fn=sx_docs_context, + context_fn=sx_standalone_context if SX_STANDALONE else sx_docs_context, domain_services_fn=register_domain_services, + **extra_kw, ) from sxc.pages import setup_sx_pages diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx index fd983c9..6e08b12 100644 --- a/sx/sx/layouts.sx +++ b/sx/sx/layouts.sx @@ -93,3 +93,45 @@ :label "sx" :href "/" :level 1 :colour "violet" :items (~sx-main-nav :section section)) (~root-mobile-auto))) + +;; --------------------------------------------------------------------------- +;; Standalone layouts (no root header, no auth — for sx-web.org) +;; --------------------------------------------------------------------------- + +(defcomp ~sx-standalone-layout-full (&key section) + (~sx-header-row :nav (~sx-main-nav :section section))) + +(defcomp ~sx-standalone-layout-oob (&key section) + (<> (~sx-header-row + :nav (~sx-main-nav :section section) + :oob true) + (~clear-oob-div :id "sx-header-child"))) + +(defcomp ~sx-standalone-layout-mobile (&key section) + (~mobile-menu-section + :label "sx" :href "/" :level 1 :colour "violet" + :items (~sx-main-nav :section section))) + +(defcomp ~sx-standalone-section-layout-full (&key section sub-label sub-href sub-nav selected) + (~sx-header-row + :nav (~sx-main-nav :section section) + :child (~sx-sub-row :sub-label sub-label :sub-href sub-href + :sub-nav sub-nav :selected selected))) + +(defcomp ~sx-standalone-section-layout-oob (&key section sub-label sub-href sub-nav selected) + (<> (~oob-header-sx :parent-id "sx-header-child" + :row (~sx-sub-row :sub-label sub-label :sub-href sub-href + :sub-nav sub-nav :selected selected)) + (~sx-header-row + :nav (~sx-main-nav :section section) + :oob true))) + +(defcomp ~sx-standalone-section-layout-mobile (&key section sub-label sub-href sub-nav) + (<> + (when sub-nav + (~mobile-menu-section + :label (or sub-label section) :href sub-href :level 2 :colour "violet" + :items sub-nav)) + (~mobile-menu-section + :label "sx" :href "/" :level 1 :colour "violet" + :items (~sx-main-nav :section section)))) diff --git a/sx/sxc/pages/layouts.py b/sx/sxc/pages/layouts.py index aee86a9..f04a965 100644 --- a/sx/sxc/pages/layouts.py +++ b/sx/sxc/pages/layouts.py @@ -1,11 +1,27 @@ """SX docs layout registration — all layouts delegate to .sx defcomps.""" from __future__ import annotations +import os def _register_sx_layouts() -> None: """Register the sx docs layout presets.""" from shared.sx.layouts import register_sx_layout - register_sx_layout("sx", "sx-layout-full", "sx-layout-oob", "sx-layout-mobile") - register_sx_layout("sx-section", "sx-section-layout-full", - "sx-section-layout-oob", "sx-section-layout-mobile") + if os.getenv("SX_STANDALONE") == "true": + register_sx_layout("sx", + "sx-standalone-layout-full", + "sx-standalone-layout-oob", + "sx-standalone-layout-mobile") + register_sx_layout("sx-section", + "sx-standalone-section-layout-full", + "sx-standalone-section-layout-oob", + "sx-standalone-section-layout-mobile") + else: + register_sx_layout("sx", + "sx-layout-full", + "sx-layout-oob", + "sx-layout-mobile") + register_sx_layout("sx-section", + "sx-section-layout-full", + "sx-section-layout-oob", + "sx-section-layout-mobile")