This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/backend/camunda_proxy.py
Benjamin Admin 21a844cb8a fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

442 lines
15 KiB
Python

"""
Camunda BPMN Engine API Proxy
Routes API requests from /api/bpmn/* to the Camunda 7 REST API
"""
import os
import httpx
from fastapi import APIRouter, Request, HTTPException, Response, UploadFile, File, Form
from fastapi.responses import JSONResponse
from typing import Optional
# Camunda REST API URL
CAMUNDA_URL = os.getenv("CAMUNDA_URL", "http://camunda:8080/engine-rest")
ENVIRONMENT = os.getenv("ENVIRONMENT", "development")
router = APIRouter(prefix="/bpmn", tags=["bpmn"])
async def proxy_request(request: Request, path: str, timeout: float = 30.0) -> Response:
"""Forward a request to the Camunda REST API"""
url = f"{CAMUNDA_URL}{path}"
# Forward headers
headers = {}
if "authorization" in request.headers:
headers["Authorization"] = request.headers["authorization"]
if "content-type" in request.headers:
headers["Content-Type"] = request.headers["content-type"]
# Get request body for POST/PUT/PATCH/DELETE
body = None
if request.method in ("POST", "PUT", "PATCH", "DELETE"):
body = await request.body()
async with httpx.AsyncClient(timeout=timeout) as client:
try:
response = await client.request(
method=request.method,
url=url,
headers=headers,
content=body,
params=request.query_params
)
return Response(
content=response.content,
status_code=response.status_code,
headers=dict(response.headers),
media_type=response.headers.get("content-type", "application/json")
)
except httpx.ConnectError:
raise HTTPException(
status_code=503,
detail="Camunda BPMN engine unavailable"
)
except httpx.TimeoutException:
raise HTTPException(
status_code=504,
detail="Camunda BPMN engine timeout"
)
# ============================================
# Health & Engine Info
# ============================================
@router.get("/health")
async def health():
"""Health check for Camunda connection"""
async with httpx.AsyncClient(timeout=5.0) as client:
try:
response = await client.get(f"{CAMUNDA_URL}/engine")
if response.status_code == 200:
engines = response.json()
return {
"camunda": "healthy",
"connected": True,
"engines": engines
}
return {"camunda": "unhealthy", "connected": False}
except Exception as e:
return {"camunda": "unhealthy", "connected": False, "error": str(e)}
@router.get("/engine")
async def get_engines(request: Request):
"""Get list of process engines"""
return await proxy_request(request, "/engine")
# ============================================
# Process Definitions
# ============================================
@router.get("/process-definition")
async def list_process_definitions(request: Request):
"""List all deployed process definitions"""
return await proxy_request(request, "/process-definition")
@router.get("/process-definition/{id}")
async def get_process_definition(id: str, request: Request):
"""Get a specific process definition by ID"""
return await proxy_request(request, f"/process-definition/{id}")
@router.get("/process-definition/key/{key}")
async def get_process_definition_by_key(key: str, request: Request):
"""Get the latest version of a process definition by key"""
return await proxy_request(request, f"/process-definition/key/{key}")
@router.get("/process-definition/{id}/xml")
async def get_process_definition_xml(id: str, request: Request):
"""Get the BPMN 2.0 XML of a process definition"""
return await proxy_request(request, f"/process-definition/{id}/xml")
@router.get("/process-definition/key/{key}/xml")
async def get_process_definition_xml_by_key(key: str, request: Request):
"""Get the BPMN 2.0 XML of a process definition by key"""
return await proxy_request(request, f"/process-definition/key/{key}/xml")
@router.get("/process-definition/{id}/diagram")
async def get_process_definition_diagram(id: str, request: Request):
"""Get the diagram of a process definition"""
return await proxy_request(request, f"/process-definition/{id}/diagram")
# ============================================
# Process Instances
# ============================================
@router.post("/process-definition/{id}/start")
async def start_process_by_id(id: str, request: Request):
"""Start a new process instance by definition ID"""
return await proxy_request(request, f"/process-definition/{id}/start")
@router.post("/process-definition/key/{key}/start")
async def start_process_by_key(key: str, request: Request):
"""Start a new process instance by definition key"""
return await proxy_request(request, f"/process-definition/key/{key}/start")
@router.get("/process-instance")
async def list_process_instances(request: Request):
"""List all running process instances"""
return await proxy_request(request, "/process-instance")
@router.get("/process-instance/{id}")
async def get_process_instance(id: str, request: Request):
"""Get a specific process instance"""
return await proxy_request(request, f"/process-instance/{id}")
@router.delete("/process-instance/{id}")
async def delete_process_instance(id: str, request: Request):
"""Delete (cancel) a process instance"""
return await proxy_request(request, f"/process-instance/{id}")
@router.get("/process-instance/{id}/activity-instances")
async def get_activity_instances(id: str, request: Request):
"""Get activity instances for a process instance"""
return await proxy_request(request, f"/process-instance/{id}/activity-instances")
# ============================================
# Tasks (User Tasks)
# ============================================
@router.get("/task")
async def list_tasks(request: Request):
"""List all pending tasks"""
return await proxy_request(request, "/task")
@router.get("/task/{id}")
async def get_task(id: str, request: Request):
"""Get a specific task"""
return await proxy_request(request, f"/task/{id}")
@router.post("/task/{id}/claim")
async def claim_task(id: str, request: Request):
"""Claim a task for a user"""
return await proxy_request(request, f"/task/{id}/claim")
@router.post("/task/{id}/unclaim")
async def unclaim_task(id: str, request: Request):
"""Unclaim a task"""
return await proxy_request(request, f"/task/{id}/unclaim")
@router.post("/task/{id}/complete")
async def complete_task(id: str, request: Request):
"""Complete a task with variables"""
return await proxy_request(request, f"/task/{id}/complete")
@router.post("/task/{id}/delegate")
async def delegate_task(id: str, request: Request):
"""Delegate a task to another user"""
return await proxy_request(request, f"/task/{id}/delegate")
@router.get("/task/{id}/form-variables")
async def get_task_form_variables(id: str, request: Request):
"""Get form variables for a task"""
return await proxy_request(request, f"/task/{id}/form-variables")
# ============================================
# Variables
# ============================================
@router.get("/process-instance/{id}/variables")
async def get_process_variables(id: str, request: Request):
"""Get all variables of a process instance"""
return await proxy_request(request, f"/process-instance/{id}/variables")
@router.put("/process-instance/{id}/variables")
async def update_process_variables(id: str, request: Request):
"""Update variables of a process instance"""
return await proxy_request(request, f"/process-instance/{id}/variables")
@router.get("/task/{id}/variables")
async def get_task_variables(id: str, request: Request):
"""Get all variables of a task"""
return await proxy_request(request, f"/task/{id}/variables")
# ============================================
# Deployment
# ============================================
@router.get("/deployment")
async def list_deployments(request: Request):
"""List all deployments"""
return await proxy_request(request, "/deployment")
@router.get("/deployment/{id}")
async def get_deployment(id: str, request: Request):
"""Get a specific deployment"""
return await proxy_request(request, f"/deployment/{id}")
@router.delete("/deployment/{id}")
async def delete_deployment(id: str, request: Request):
"""Delete a deployment"""
return await proxy_request(request, f"/deployment/{id}")
@router.post("/deployment/create")
async def create_deployment(
request: Request,
deployment_name: str = Form(..., alias="deployment-name"),
enable_duplicate_filtering: bool = Form(False, alias="enable-duplicate-filtering"),
deploy_changed_only: bool = Form(False, alias="deploy-changed-only"),
data: UploadFile = File(...)
):
"""
Deploy a BPMN process definition.
Accepts multipart/form-data with:
- deployment-name: Name for the deployment
- data: The BPMN XML file
"""
url = f"{CAMUNDA_URL}/deployment/create"
# Read the uploaded file
file_content = await data.read()
filename = data.filename or "process.bpmn"
# Prepare multipart form data
files = {
"data": (filename, file_content, "application/octet-stream")
}
form_data = {
"deployment-name": deployment_name,
"enable-duplicate-filtering": str(enable_duplicate_filtering).lower(),
"deploy-changed-only": str(deploy_changed_only).lower()
}
async with httpx.AsyncClient(timeout=60.0) as client:
try:
response = await client.post(
url,
files=files,
data=form_data
)
return Response(
content=response.content,
status_code=response.status_code,
media_type="application/json"
)
except httpx.ConnectError:
raise HTTPException(
status_code=503,
detail="Camunda BPMN engine unavailable"
)
except httpx.TimeoutException:
raise HTTPException(
status_code=504,
detail="Deployment timeout"
)
# ============================================
# History
# ============================================
@router.get("/history/process-instance")
async def list_historic_process_instances(request: Request):
"""List historic process instances"""
return await proxy_request(request, "/history/process-instance")
@router.get("/history/process-instance/{id}")
async def get_historic_process_instance(id: str, request: Request):
"""Get a historic process instance"""
return await proxy_request(request, f"/history/process-instance/{id}")
@router.get("/history/activity-instance")
async def list_historic_activity_instances(request: Request):
"""List historic activity instances"""
return await proxy_request(request, "/history/activity-instance")
@router.get("/history/task")
async def list_historic_tasks(request: Request):
"""List historic tasks"""
return await proxy_request(request, "/history/task")
# ============================================
# Message Events
# ============================================
@router.post("/message")
async def correlate_message(request: Request):
"""Correlate a message to trigger a message event"""
return await proxy_request(request, "/message")
# ============================================
# Signal Events
# ============================================
@router.post("/signal")
async def throw_signal(request: Request):
"""Throw a signal to trigger signal events"""
return await proxy_request(request, "/signal")
# ============================================
# Convenience Endpoints (BreakPilot-specific)
# ============================================
@router.get("/processes/active")
async def get_active_processes():
"""Get all active process instances with their current state"""
async with httpx.AsyncClient(timeout=30.0) as client:
try:
# Get all running instances
instances_response = await client.get(f"{CAMUNDA_URL}/process-instance")
if instances_response.status_code != 200:
return JSONResponse(
content={"error": "Failed to fetch process instances"},
status_code=instances_response.status_code
)
instances = instances_response.json()
# Enrich with definition names
result = []
for instance in instances:
definition_id = instance.get("definitionId", "")
try:
def_response = await client.get(
f"{CAMUNDA_URL}/process-definition/{definition_id}"
)
if def_response.status_code == 200:
definition = def_response.json()
instance["processName"] = definition.get("name", definition.get("key", "Unknown"))
except Exception:
instance["processName"] = "Unknown"
result.append(instance)
return JSONResponse(content=result)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="Camunda unavailable")
@router.get("/tasks/pending")
async def get_pending_tasks(assignee: Optional[str] = None, candidateGroup: Optional[str] = None):
"""Get pending tasks, optionally filtered by assignee or candidate group"""
params = {}
if assignee:
params["assignee"] = assignee
if candidateGroup:
params["candidateGroup"] = candidateGroup
async with httpx.AsyncClient(timeout=30.0) as client:
try:
response = await client.get(f"{CAMUNDA_URL}/task", params=params)
if response.status_code != 200:
return JSONResponse(
content={"error": "Failed to fetch tasks"},
status_code=response.status_code
)
tasks = response.json()
# Enrich tasks with process information
for task in tasks:
process_instance_id = task.get("processInstanceId")
if process_instance_id:
try:
pi_response = await client.get(
f"{CAMUNDA_URL}/process-instance/{process_instance_id}"
)
if pi_response.status_code == 200:
pi = pi_response.json()
task["businessKey"] = pi.get("businessKey")
except Exception:
pass
return JSONResponse(content=tasks)
except httpx.ConnectError:
raise HTTPException(status_code=503, detail="Camunda unavailable")