Files
Brancheneinstufung2/connector-superoffice/webhook_app.py
Floke abdbb84938 [30e88f42] ✦ In dieser Sitzung haben wir den End-to-End-Test der SuperOffice-Schnittstelle erfolgreich von der automatisierten Simulation bis zum produktiven Live-Lauf
✦ In dieser Sitzung haben wir den End-to-End-Test der SuperOffice-Schnittstelle erfolgreich von der automatisierten Simulation bis zum produktiven Live-Lauf
  mit Echtdaten abgeschlossen.
2026-02-22 08:20:28 +00:00

156 lines
6.0 KiB
Python

from fastapi import FastAPI, Request, HTTPException, BackgroundTasks
from fastapi.responses import HTMLResponse
import logging
import os
import json
from queue_manager import JobQueue
# Logging Setup
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("connector-webhook")
app = FastAPI(title="SuperOffice Connector Webhook", version="2.0")
queue = JobQueue()
WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET", "changeme")
@app.post("/webhook")
async def receive_webhook(request: Request, background_tasks: BackgroundTasks):
"""
Endpoint for SuperOffice Webhooks.
"""
# 1. Verify Secret (Basic Security)
# SuperOffice puts signature in headers, but for custom webhook we might just use query param or header
# Let's assume for now a shared secret in header 'X-SuperOffice-Signature' or similar
# Or simply a secret in the URL: /webhook?token=...
token = request.query_params.get("token")
if token != WEBHOOK_SECRET:
logger.warning(f"Invalid webhook token attempt: {token}")
raise HTTPException(403, "Invalid Token")
try:
payload = await request.json()
logger.info(f"Received webhook payload: {payload}")
event_type = payload.get("Event", "unknown")
# Add to local Queue
queue.add_job(event_type, payload)
return {"status": "queued"}
except Exception as e:
logger.error(f"Error processing webhook: {e}", exc_info=True)
raise HTTPException(500, "Internal Server Error")
@app.get("/health")
def health():
return {"status": "ok"}
@app.get("/stats")
def stats():
return queue.get_stats()
@app.get("/api/jobs")
def get_jobs():
return queue.get_recent_jobs(limit=100)
@app.get("/dashboard", response_class=HTMLResponse)
def dashboard():
html_content = """
<!DOCTYPE html>
<html>
<head>
<title>Connector Dashboard</title>
<meta http-equiv="refresh" content="5">
<style>
body { font-family: sans-serif; padding: 20px; background: #f0f2f5; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
h1 { color: #333; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #ddd; font-size: 14px; }
th { background-color: #f8f9fa; color: #666; font-weight: 600; }
tr:hover { background-color: #f8f9fa; }
.status { padding: 4px 8px; border-radius: 4px; font-size: 12px; font-weight: bold; text-transform: uppercase; }
.status-PENDING { background: #e2e8f0; color: #475569; }
.status-PROCESSING { background: #dbeafe; color: #1e40af; }
.status-COMPLETED { background: #dcfce7; color: #166534; }
.status-FAILED { background: #fee2e2; color: #991b1b; }
.status-RETRY { background: #fef9c3; color: #854d0e; }
.meta { color: #888; font-size: 12px; }
pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #444; font-family: monospace; font-size: 11px; max-height: 60px; overflow-y: auto; }
</style>
</head>
<body>
<div class="container">
<div style="display: flex; justify-content: space-between; align-items: center;">
<h1>🔌 SuperOffice Connector Dashboard</h1>
<div id="stats"></div>
</div>
<table>
<thead>
<tr>
<th width="50">ID</th>
<th width="120">Status</th>
<th width="150">Updated</th>
<th width="150">Event</th>
<th>Payload / Error</th>
</tr>
</thead>
<tbody id="job-table">
<tr><td colspan="5" style="text-align:center;">Loading...</td></tr>
</tbody>
</table>
</div>
<script>
async function loadData() {
try {
// Use relative path to work behind Nginx /connector/ prefix
const response = await fetch('api/jobs');
const jobs = await response.json();
const tbody = document.getElementById('job-table');
tbody.innerHTML = '';
if (jobs.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" style="text-align:center;">No jobs found</td></tr>';
return;
}
jobs.forEach(job => {
const tr = document.createElement('tr');
let details = JSON.stringify(job.payload, null, 2);
if (job.error_msg) {
details += "\\n\\n🔴 ERROR: " + job.error_msg;
}
tr.innerHTML = `
<td>#${job.id}</td>
<td><span class="status status-${job.status}">${job.status}</span></td>
<td>${new Date(job.updated_at + "Z").toLocaleTimeString()}</td>
<td>${job.event_type}</td>
<td><pre>${details}</pre></td>
`;
tbody.appendChild(tr);
});
} catch (e) {
console.error("Failed to load jobs", e);
}
}
loadData();
// Also handled by meta refresh, but JS refresh is smoother if we want to remove meta refresh
</script>
</body>
</html>
"""
return HTMLResponse(content=html_content, status_code=200)
if __name__ == "__main__":
import uvicorn
uvicorn.run("webhook_app:app", host="0.0.0.0", port=8000, reload=True)