From bbf05089cd0fa64b8a55c9cc5a6e841e6dedfd49 Mon Sep 17 00:00:00 2001 From: Jaco Bezuidenhout Date: Wed, 27 May 2026 12:53:54 +0000 Subject: [PATCH] fix: approve/reject endpoints return HTML pages instead of raw JSON Mobile-friendly dark-theme page with icon, heading, and description. Green for success, yellow for cancelled/not-found, red for errors. --- app/main.py | 128 +++++++++++++++++++++++++++++++++++----------------- 1 file changed, 86 insertions(+), 42 deletions(-) diff --git a/app/main.py b/app/main.py index 7b2aa88..2fd2968 100644 --- a/app/main.py +++ b/app/main.py @@ -10,7 +10,7 @@ from typing import AsyncGenerator from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import JSONResponse, StreamingResponse +from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse from pydantic import BaseModel from .approval import cleanup_expired, generate_token, pop_action, queue_action, verify_token @@ -184,6 +184,43 @@ async def list_models(): } +def _html_page(title: str, icon: str, heading: str, body: str, color: str = "#3fb950") -> HTMLResponse: + """Render a simple mobile-friendly result page.""" + html = f""" + + + + + {title} — Jon Snow + + + +
+
{icon}
+

{heading}

+

{body}

+
jon-snow · nxm.co.za
+ +
+ +""" + return HTMLResponse(html) + + @app.get("/approve/{token}") async def approve_action(token: str): """Public endpoint — called when user clicks Approve in Discord. Executes the queued action.""" @@ -191,38 +228,47 @@ async def approve_action(token: str): try: action_id, purpose = verify_token(token) except ValueError as e: - return JSONResponse( - {"error": str(e), "message": "Approval failed — token may be expired or already used."}, - status_code=400, - ) + return _html_page("Error", "⚠️", "Token Invalid", + f"{e}. The link may have expired (15 min) or already been used.", + color="#f85149") if purpose != "approve": - return JSONResponse({"error": "wrong token purpose"}, status_code=400) + return _html_page("Error", "⚠️", "Wrong Link", + "This doesn't look like an approval link.", + color="#f85149") action = pop_action(action_id) if not action: - return JSONResponse( - {"error": "action not found", "message": "Action not found — it may have already been executed or expired."}, - status_code=404, - ) + return _html_page("Not Found", "🔍", "Action Not Found", + "This action has already been executed, rejected, or expired.", + color="#d29922") logger.info(f"approved: {action.action_type} — {action.description}") try: if action.action_type == "docker_rebuild": result = await call_qyburn_rebuild(action.params["stack_name"]) - return JSONResponse({ - "ok": True, - "message": f"✅ Rebuilding {action.params['stack_name']}. Check container logs for progress.", - "result": result, - }) + ok = result.get("ok", False) + if ok: + return _html_page("Approved", "✅", "Rebuilding", + f"{action.params['stack_name']} is rebuilding. " + f"It will be back online in ~30 seconds.") + else: + return _html_page("Failed", "❌", "Rebuild Failed", + f"Could not rebuild {action.params['stack_name']}: " + f"{result.get('stderr','unknown error')[:200]}", + color="#f85149") elif action.action_type == "docker_restart": result = await call_qyburn_restart(action.params["stack_name"]) - return JSONResponse({ - "ok": True, - "message": f"✅ Restarting {action.params['stack_name']}.", - "result": result, - }) + ok = result.get("ok", False) + if ok: + return _html_page("Approved", "✅", "Restarted", + f"{action.params['stack_name']} has been restarted successfully.") + else: + return _html_page("Failed", "❌", "Restart Failed", + f"Could not restart {action.params['stack_name']}: " + f"{result.get('stderr','unknown error')[:200]}", + color="#f85149") elif action.action_type == "file_write": path = action.params.get("path", "") content = action.params.get("content", "") @@ -230,18 +276,17 @@ async def approve_action(token: str): target = SITES_DIR / safe_path target.parent.mkdir(parents=True, exist_ok=True) target.write_text(content) - return JSONResponse({ - "ok": True, - "message": f"✅ File written: /opt/sites/{safe_path}", - }) + return _html_page("Approved", "✅", "File Written", + f"/opt/sites/{safe_path} has been updated.") else: - return JSONResponse( - {"error": f"Unknown action type: {action.action_type}"}, - status_code=400, - ) + return _html_page("Error", "⚠️", "Unknown Action", + f"Action type '{action.action_type}' is not supported.", + color="#f85149") except Exception as e: logger.error(f"action execution failed: {e}") - return JSONResponse({"error": f"Execution failed: {e}"}, status_code=500) + return _html_page("Error", "❌", "Execution Failed", + f"Something went wrong: {str(e)[:200]}", + color="#f85149") @app.get("/reject/{token}") @@ -251,26 +296,25 @@ async def reject_action(token: str): try: action_id, purpose = verify_token(token) except ValueError as e: - return JSONResponse( - {"error": str(e), "message": "Rejection failed — token may be expired or already used."}, - status_code=400, - ) + return _html_page("Error", "⚠️", "Token Invalid", + f"{e}. The link may have expired (15 min) or already been used.", + color="#f85149") if purpose != "reject": - return JSONResponse({"error": "wrong token purpose"}, status_code=400) + return _html_page("Error", "⚠️", "Wrong Link", + "This doesn't look like a rejection link.", + color="#f85149") action = pop_action(action_id) if not action: - return JSONResponse( - {"error": "action not found", "message": "Action not found — it may have already been handled."}, - status_code=404, - ) + return _html_page("Not Found", "🔍", "Already Handled", + "This action has already been executed, rejected, or expired.", + color="#d29922") logger.info(f"rejected: {action.action_type} — {action.description}") - return JSONResponse({ - "ok": True, - "message": f"❌ Action rejected: {action.description}", - }) + return _html_page("Rejected", "❌", "Action Cancelled", + f"{action.description} was rejected and will not be executed.", + color="#d29922") @app.post("/internal/queue-action")