diff --git a/account/app.py b/account/app.py index a838bdc..a6348ce 100644 --- a/account/app.py +++ b/account/app.py @@ -82,18 +82,22 @@ def create_app() -> "Quart": from bp.data.routes import register as register_data app.register_blueprint(register_data()) - # --- Ghost membership sync at startup --- + # --- Ghost membership sync at startup (background) --- + # Runs as a background task to avoid blocking Hypercorn's startup timeout. @app.before_serving - async def _sync_ghost_membership(): - from services.ghost_membership import sync_all_membership_from_ghost - from shared.db.session import get_session - try: - async with get_session() as s: - await sync_all_membership_from_ghost(s) - await s.commit() - print("[account] Ghost membership sync complete") - except Exception as e: - print(f"[account] Ghost membership sync failed (non-fatal): {e}") + async def _schedule_ghost_membership_sync(): + import asyncio + async def _sync(): + from services.ghost_membership import sync_all_membership_from_ghost + from shared.db.session import get_session + try: + async with get_session() as s: + await sync_all_membership_from_ghost(s) + await s.commit() + print("[account] Ghost membership sync complete") + except Exception as e: + print(f"[account] Ghost membership sync failed (non-fatal): {e}") + asyncio.get_event_loop().create_task(_sync()) return app diff --git a/shared/browser/app/errors.py b/shared/browser/app/errors.py index 0da2550..ce638fc 100644 --- a/shared/browser/app/errors.py +++ b/shared/browser/app/errors.py @@ -33,6 +33,30 @@ class AppError(ValueError): self.status_code = status_code +def _error_page(message: str) -> str: + """Self-contained error page HTML. Bypasses Jinja/context processors.""" + return ( + "" + "" + "Error" + "" + "
" + f"

{message}

" + "" + "

Reload

" + "
" + ) + + def errors(app): def _info(e): return { @@ -119,11 +143,11 @@ def errors(app): f"

Service {escape(service)} is unavailable.

", 503, ) - html = await render_template( - "_types/root/exceptions/error.html", - service_name=service, - ) - return await make_response(html, 503) + # Raw HTML — cannot use render_template here because the context + # processor would try to fetch fragments again → infinite loop. + return await make_response(_error_page( + f"The {escape(service)} service is currently unavailable. It may be restarting." + ), 503) @app.errorhandler(Exception) async def error(e): @@ -134,11 +158,12 @@ def errors(app): status = e.code or 500 if request.headers.get("HX-Request") == "true": - # Generic message for unexpected/untrusted errors return await make_response( "Something went wrong. Please try again.", status, ) - html = await render_template("_types/root/exceptions/error.html") - return await make_response(html, status) + # Raw HTML — avoids context processor / fragment loop on errors. + return await make_response(_error_page( + "WELL THIS IS EMBARRASSING…" + ), status)