feat: frontend error capture — diagnostic instantané pour Hermes
FlowDeck CI / test (push) Failing after 5s
FlowDeck CI / docker (push) Has been skipped

- app.js: intercepte window.onerror + unhandledrejection
  Envoie automatiquement au backend via POST /api/frontend-error
  Déduplication, throttling 1/sec, max 50 erreurs buffer local
  window.__flowdeck_errors accessible en console debug

- api.py: 2 nouvelles routes
  POST /api/frontend-error — reçoit erreurs JS, déduplique, logge
  GET /api/frontend-errors?clear=true — Hermes lit les erreurs

- csrf.py: exemption /api/frontend-error du CSRF

Usage Hermes après chaque déploiement:
  curl -s http://localhost:8080/api/frontend-errors | jq .
  → Voir TOUTES les erreurs JS en temps réel
This commit is contained in:
2026-07-13 07:52:52 -04:00
parent 3718ad488c
commit ae3f8b473a
3 changed files with 106 additions and 1 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ class CSRFMiddleware(BaseHTTPMiddleware):
"""
SAFE_METHODS = {"GET", "HEAD", "OPTIONS"}
EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/auth/register", "/auth/local-login", "/api/user", "/board/api/pages", "/board/api/favorites", "/api/workspace", "/api/local-workspace", "/api/settings", "/db/", "/workspace"}
EXCLUDED_PATHS = {"/api/webhook", "/api/v1", "/auth/callback", "/auth/register", "/auth/local-login", "/api/user", "/board/api/pages", "/board/api/favorites", "/api/workspace", "/api/local-workspace", "/api/settings", "/db/", "/workspace", "/api/frontend-error"}
async def dispatch(self, request: Request, call_next):
# Webhook receiver, OAuth callback, and internal API are exempt
+52
View File
@@ -623,3 +623,55 @@ async def get_collaborators(owner: str, repo: str):
return {"collaborators": collaborators}
except Exception as e:
return {"collaborators": [], "error": str(e)}
# ── Frontend Error Capture ───────────────────────────────────
# Hermes diagnostique le frontend en appelant GET /api/frontend-errors
_frontend_errors: list[dict] = []
_MAX_STORED_ERRORS = 100
@router.post("/frontend-error")
async def capture_frontend_error(request: Request):
"""Reçoit les erreurs JS du navigateur. Appelé automatiquement par app.js."""
try:
body = await request.json()
except Exception:
return {"status": "ignored", "reason": "invalid json"}
msg = body.get("message", "")
err_type = body.get("type", "error")
source = body.get("source", "")
line = body.get("line", 0)
# Dédupliquer les erreurs identiques consécutives
if _frontend_errors and _frontend_errors[-1].get("message") == msg:
_frontend_errors[-1]["count"] = _frontend_errors[-1].get("count", 1) + 1
_frontend_errors[-1]["time"] = body.get("time", "")
else:
body["count"] = 1
_frontend_errors.append(body)
while len(_frontend_errors) > _MAX_STORED_ERRORS:
_frontend_errors.pop(0)
if err_type == "error":
logger.warning("Frontend JS error: %s (%s:%s)", msg, source, line)
else:
logger.warning("Frontend unhandled rejection: %s", msg)
return {"status": "ok"}
@router.get("/frontend-errors")
async def get_frontend_errors(request: Request, clear: bool = True):
"""Retourne les erreurs frontend récentes. Hermes appelle cette route."""
errors = list(_frontend_errors)
if clear:
_frontend_errors.clear()
return {
"errors": errors,
"count": len(errors),
"cleared": clear,
}