Add standalone mode for sx-web.org deployment
- 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>
This commit is contained in:
@@ -84,13 +84,25 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Deploy swarm stack only on main branch
|
# Deploy swarm stacks only on main branch
|
||||||
if [ '${{ github.ref_name }}' = 'main' ]; then
|
if [ '${{ github.ref_name }}' = 'main' ]; then
|
||||||
source .env
|
source .env
|
||||||
docker stack deploy -c docker-compose.yml rose-ash
|
docker stack deploy -c docker-compose.yml rose-ash
|
||||||
echo 'Waiting for swarm services to update...'
|
echo 'Waiting for swarm services to update...'
|
||||||
sleep 10
|
sleep 10
|
||||||
docker stack services rose-ash
|
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
|
else
|
||||||
echo 'Skipping swarm deploy (branch: ${{ github.ref_name }})'
|
echo 'Skipping swarm deploy (branch: ${{ github.ref_name }})'
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ def create_base_app(
|
|||||||
context_fn: Callable[[], Awaitable[dict]] | None = None,
|
context_fn: Callable[[], Awaitable[dict]] | None = None,
|
||||||
before_request_fns: Sequence[Callable[[], Awaitable[None]]] | None = None,
|
before_request_fns: Sequence[Callable[[], Awaitable[None]]] | None = None,
|
||||||
domain_services_fn: Callable[[], None] | None = None,
|
domain_services_fn: Callable[[], None] | None = None,
|
||||||
|
no_oauth: bool = False,
|
||||||
) -> Quart:
|
) -> Quart:
|
||||||
"""
|
"""
|
||||||
Create a Quart app with shared infrastructure.
|
Create a Quart app with shared infrastructure.
|
||||||
@@ -156,7 +157,7 @@ def create_base_app(
|
|||||||
# Auto-register OAuth client blueprint for non-account apps
|
# Auto-register OAuth client blueprint for non-account apps
|
||||||
# (account is the OAuth authorization server)
|
# (account is the OAuth authorization server)
|
||||||
_NO_OAUTH = {"account"}
|
_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
|
from shared.infrastructure.oauth import create_oauth_blueprint
|
||||||
app.register_blueprint(create_oauth_blueprint(name))
|
app.register_blueprint(create_oauth_blueprint(name))
|
||||||
|
|
||||||
@@ -205,7 +206,7 @@ def create_base_app(
|
|||||||
|
|
||||||
# Auth state check via grant verification + silent OAuth handshake
|
# Auth state check via grant verification + silent OAuth handshake
|
||||||
# MUST run before _load_user so stale sessions are cleared first
|
# 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
|
@app.before_request
|
||||||
async def _check_auth_state():
|
async def _check_auth_state():
|
||||||
from quart import session as qs
|
from quart import session as qs
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ COPY sx/ ./sx-app-tmp/
|
|||||||
RUN cp -r sx-app-tmp/app.py sx-app-tmp/path_setup.py \
|
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/bp sx-app-tmp/sxc sx-app-tmp/services \
|
||||||
sx-app-tmp/content sx-app-tmp/__init__.py ./ 2>/dev/null || true && \
|
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
|
rm -rf sx-app-tmp
|
||||||
|
|
||||||
# Sibling models for cross-domain SQLAlchemy imports
|
# Sibling models for cross-domain SQLAlchemy imports
|
||||||
|
|||||||
24
sx/app.py
24
sx/app.py
@@ -1,10 +1,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
import path_setup # noqa: F401
|
import path_setup # noqa: F401
|
||||||
from shared.infrastructure.factory import create_base_app
|
|
||||||
|
|
||||||
from bp import register_pages
|
from bp import register_pages
|
||||||
from services import register_domain_services
|
from services import register_domain_services
|
||||||
|
|
||||||
|
SX_STANDALONE = os.getenv("SX_STANDALONE") == "true"
|
||||||
|
|
||||||
|
|
||||||
async def sx_docs_context() -> dict:
|
async def sx_docs_context() -> dict:
|
||||||
"""SX docs app context processor — fetches cross-service fragments."""
|
"""SX docs app context processor — fetches cross-service fragments."""
|
||||||
@@ -39,11 +41,29 @@ async def sx_docs_context() -> dict:
|
|||||||
return ctx
|
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":
|
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(
|
app = create_base_app(
|
||||||
"sx",
|
"sx",
|
||||||
context_fn=sx_docs_context,
|
context_fn=sx_standalone_context if SX_STANDALONE else sx_docs_context,
|
||||||
domain_services_fn=register_domain_services,
|
domain_services_fn=register_domain_services,
|
||||||
|
**extra_kw,
|
||||||
)
|
)
|
||||||
|
|
||||||
from sxc.pages import setup_sx_pages
|
from sxc.pages import setup_sx_pages
|
||||||
|
|||||||
@@ -93,3 +93,45 @@
|
|||||||
:label "sx" :href "/" :level 1 :colour "violet"
|
:label "sx" :href "/" :level 1 :colour "violet"
|
||||||
:items (~sx-main-nav :section section))
|
:items (~sx-main-nav :section section))
|
||||||
(~root-mobile-auto)))
|
(~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))))
|
||||||
|
|||||||
@@ -1,11 +1,27 @@
|
|||||||
"""SX docs layout registration — all layouts delegate to .sx defcomps."""
|
"""SX docs layout registration — all layouts delegate to .sx defcomps."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
def _register_sx_layouts() -> None:
|
def _register_sx_layouts() -> None:
|
||||||
"""Register the sx docs layout presets."""
|
"""Register the sx docs layout presets."""
|
||||||
from shared.sx.layouts import register_sx_layout
|
from shared.sx.layouts import register_sx_layout
|
||||||
|
|
||||||
register_sx_layout("sx", "sx-layout-full", "sx-layout-oob", "sx-layout-mobile")
|
if os.getenv("SX_STANDALONE") == "true":
|
||||||
register_sx_layout("sx-section", "sx-section-layout-full",
|
register_sx_layout("sx",
|
||||||
"sx-section-layout-oob", "sx-section-layout-mobile")
|
"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")
|
||||||
|
|||||||
Reference in New Issue
Block a user