Merge worktree-zero-tooling-essay into macros

Resolves conflicts by keeping both:
- HEAD: cache invalidation, service worker, sw-post-message
- Worktree: optimistic mutations, offline queue, action endpoint
Plans.sx unified with combined 7c/7d documentation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 01:33:49 +00:00
8 changed files with 465 additions and 32 deletions

View File

@@ -887,6 +887,9 @@ def auto_mount_pages(app: Any, service_name: str) -> None:
# Mount service worker at root scope for offline data layer
mount_service_worker(app)
# Mount action endpoint for Phase 7c: optimistic data mutations
mount_action_endpoint(app, service_name)
def mount_pages(bp: Any, service_name: str,
names: set[str] | list[str] | None = None) -> None:
@@ -1225,3 +1228,56 @@ def mount_service_worker(app: Any) -> None:
app.add_url_rule("/sx-sw.js", endpoint="sx_service_worker", view_func=serve_sw)
logger.info("Mounted service worker at /sx-sw.js")
def mount_action_endpoint(app: Any, service_name: str) -> None:
"""Mount /sx/action/<name> endpoint for client-side data mutations.
The client can POST to trigger a named action (registered via
register_page_helpers with an 'action:' prefix). The action receives
the JSON payload, performs the mutation, and returns the new page data
as SX wire format.
This is the server counterpart to submit-mutation in orchestration.sx.
"""
from quart import make_response, request, abort as quart_abort
from .parser import serialize
from shared.browser.app.csrf import csrf_exempt
@csrf_exempt
async def action_handler(name: str) -> Any:
# Look up action helper
helpers = get_page_helpers(service_name)
action_fn = helpers.get(f"action:{name}")
if action_fn is None:
quart_abort(404)
# Parse JSON payload
import asyncio as _asyncio
data = await request.get_json(silent=True) or {}
try:
result = action_fn(**data)
if _asyncio.iscoroutine(result):
result = await result
except Exception as e:
logger.warning("Action %s failed: %s", name, e)
resp = await make_response(f'(dict "error" "{e}")', 500)
resp.content_type = "text/sx; charset=utf-8"
return resp
result_sx = serialize(result) if result is not None else "nil"
resp = await make_response(result_sx, 200)
resp.content_type = "text/sx; charset=utf-8"
return resp
action_handler.__name__ = "sx_action"
action_handler.__qualname__ = "sx_action"
app.add_url_rule(
"/sx/action/<name>",
endpoint="sx_action",
view_func=action_handler,
methods=["POST"],
)
logger.info("Mounted action endpoint for %s at /sx/action/<name>", service_name)