"""
BreakPilot Studio - Workflow/BPMN Module
BPMN 2.0 Prozess-Editor mit bpmn-js Integration.
Ermoeglicht das Modellieren, Speichern und Deployen von Geschaeftsprozessen.
"""
class WorkflowModule:
"""BPMN Workflow Editor Modul fuer BreakPilot Studio."""
@staticmethod
def get_css() -> str:
"""CSS fuer das Workflow/BPMN Panel."""
return """
/* ==========================================
WORKFLOW/BPMN MODULE
========================================== */
#panel-workflow {
display: none;
flex-direction: column;
padding: 24px;
min-height: calc(100vh - 104px);
height: calc(100vh - 104px);
}
#panel-workflow.active {
display: flex;
}
/* Header */
.workflow-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 20px;
}
.workflow-title {
font-size: 24px;
font-weight: 700;
color: var(--bp-text);
margin-bottom: 4px;
}
.workflow-subtitle {
font-size: 14px;
color: var(--bp-text-muted);
}
/* Toolbar */
.workflow-toolbar {
display: flex;
gap: 12px;
margin-bottom: 16px;
padding: 12px 16px;
background: var(--bp-surface-elevated);
border-radius: 8px;
border: 1px solid var(--bp-border);
flex-wrap: wrap;
}
.workflow-toolbar-group {
display: flex;
gap: 8px;
align-items: center;
}
.workflow-toolbar-separator {
width: 1px;
height: 24px;
background: var(--bp-border);
margin: 0 8px;
}
.workflow-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
border: 1px solid var(--bp-border);
background: var(--bp-surface);
color: var(--bp-text);
}
.workflow-btn:hover {
background: var(--bp-surface-elevated);
border-color: var(--bp-primary);
}
.workflow-btn.primary {
background: var(--bp-primary);
border-color: var(--bp-primary);
color: white;
}
.workflow-btn.primary:hover {
background: var(--bp-primary-hover);
}
.workflow-btn.success {
background: var(--bp-success);
border-color: var(--bp-success);
color: white;
}
.workflow-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Canvas Container */
.workflow-canvas-container {
flex: 1;
display: flex;
flex-direction: column;
border: 1px solid var(--bp-border);
border-radius: 12px;
overflow: hidden;
background: white;
min-height: 400px;
}
.workflow-canvas {
flex: 1;
width: 100%;
height: 100%;
}
/* bpmn-js Overrides for Dark Theme Support */
.bjs-powered-by {
display: none !important;
}
.djs-palette {
background: var(--bp-surface) !important;
border-color: var(--bp-border) !important;
}
.djs-palette-entries .entry {
color: var(--bp-text) !important;
}
/* Status Bar */
.workflow-status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 16px;
background: var(--bp-surface);
border-top: 1px solid var(--bp-border);
font-size: 12px;
color: var(--bp-text-muted);
}
.workflow-status-item {
display: flex;
align-items: center;
gap: 6px;
}
.workflow-status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--bp-text-muted);
}
.workflow-status-dot.connected {
background: var(--bp-success);
}
.workflow-status-dot.disconnected {
background: var(--bp-danger);
}
/* Process List Panel */
.workflow-processes-panel {
position: fixed;
top: 56px;
right: 0;
bottom: 0;
width: 320px;
background: var(--bp-surface);
border-left: 1px solid var(--bp-border);
transform: translateX(100%);
transition: transform 0.3s ease;
z-index: 60;
display: flex;
flex-direction: column;
}
.workflow-processes-panel.open {
transform: translateX(0);
}
.workflow-processes-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid var(--bp-border);
}
.workflow-processes-title {
font-size: 16px;
font-weight: 600;
color: var(--bp-text);
}
.workflow-processes-close {
background: none;
border: none;
font-size: 20px;
color: var(--bp-text-muted);
cursor: pointer;
}
.workflow-processes-list {
flex: 1;
overflow-y: auto;
padding: 16px;
}
.workflow-process-item {
padding: 12px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.2s;
}
.workflow-process-item:hover {
border-color: var(--bp-primary);
}
.workflow-process-name {
font-size: 14px;
font-weight: 600;
color: var(--bp-text);
margin-bottom: 4px;
}
.workflow-process-meta {
font-size: 12px;
color: var(--bp-text-muted);
}
/* Task Inbox Panel */
.workflow-tasks-panel {
margin-top: 16px;
background: var(--bp-surface-elevated);
border: 1px solid var(--bp-border);
border-radius: 12px;
max-height: 300px;
overflow: hidden;
display: none;
}
.workflow-tasks-panel.visible {
display: block;
}
.workflow-tasks-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--bp-border);
}
.workflow-tasks-title {
font-size: 14px;
font-weight: 600;
color: var(--bp-text);
}
.workflow-tasks-count {
padding: 2px 8px;
background: var(--bp-primary);
color: white;
border-radius: 999px;
font-size: 11px;
font-weight: 600;
}
.workflow-tasks-list {
max-height: 240px;
overflow-y: auto;
}
.workflow-task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--bp-border-subtle);
}
.workflow-task-item:last-child {
border-bottom: none;
}
.workflow-task-info {
flex: 1;
}
.workflow-task-name {
font-size: 13px;
font-weight: 500;
color: var(--bp-text);
margin-bottom: 2px;
}
.workflow-task-process {
font-size: 11px;
color: var(--bp-text-muted);
}
.workflow-task-action {
padding: 6px 12px;
background: var(--bp-primary);
color: white;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
}
/* Loading Overlay */
.workflow-loading {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
}
.workflow-loading-spinner {
width: 40px;
height: 40px;
border: 3px solid var(--bp-border);
border-top-color: var(--bp-primary);
border-radius: 50%;
animation: workflow-spin 1s linear infinite;
}
@keyframes workflow-spin {
to { transform: rotate(360deg); }
}
/* Toast Notifications */
.workflow-toast {
position: fixed;
bottom: 24px;
right: 24px;
padding: 12px 20px;
background: var(--bp-surface);
border: 1px solid var(--bp-border);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
z-index: 200;
animation: workflow-toast-in 0.3s ease;
}
.workflow-toast.success {
border-color: var(--bp-success);
}
.workflow-toast.error {
border-color: var(--bp-danger);
}
@keyframes workflow-toast-in {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
"""
@staticmethod
def get_html() -> str:
"""HTML fuer das Workflow/BPMN Panel."""
return """
Camunda: Pruefe...
Elemente: 0
"""
@staticmethod
def get_js() -> str:
"""JavaScript fuer das Workflow/BPMN Panel."""
return """
// ==========================================
// WORKFLOW/BPMN MODULE
// ==========================================
console.log('Workflow Module loaded');
let workflowModeler = null;
let workflowCamundaConnected = false;
// Default empty BPMN diagram
const WORKFLOW_EMPTY_BPMN = `
`;
// Initialize BPMN Modeler
async function initWorkflowModule() {
console.log('Initializing Workflow Module...');
const container = document.getElementById('workflow-canvas');
if (!container) {
console.error('Workflow canvas container not found');
return;
}
// Check if BpmnJS is loaded
if (typeof BpmnJS === 'undefined') {
console.error('bpmn-js not loaded');
workflowShowToast('bpmn-js konnte nicht geladen werden', 'error');
return;
}
try {
// Create modeler instance
workflowModeler = new BpmnJS({
container: container,
keyboard: {
bindTo: document
}
});
// Load empty diagram
await workflowModeler.importXML(WORKFLOW_EMPTY_BPMN);
// Center the diagram
const canvas = workflowModeler.get('canvas');
canvas.zoom('fit-viewport');
// Update element count on changes
workflowModeler.on('elements.changed', workflowUpdateElementCount);
workflowUpdateElementCount();
console.log('Workflow Modeler initialized');
// Check Camunda connection
workflowCheckCamundaStatus();
} catch (err) {
console.error('Error initializing workflow modeler:', err);
workflowShowToast('Fehler beim Initialisieren des Editors', 'error');
}
}
// Check Camunda connection status
async function workflowCheckCamundaStatus() {
const statusDot = document.getElementById('workflow-camunda-status');
const statusText = document.getElementById('workflow-camunda-status-text');
try {
const response = await fetch('/api/bpmn/health');
const data = await response.json();
if (data.connected) {
statusDot.classList.add('connected');
statusDot.classList.remove('disconnected');
statusText.textContent = 'Camunda: Verbunden';
workflowCamundaConnected = true;
} else {
throw new Error('Not connected');
}
} catch (err) {
statusDot.classList.add('disconnected');
statusDot.classList.remove('connected');
statusText.textContent = 'Camunda: Nicht verbunden';
workflowCamundaConnected = false;
}
}
// Create new diagram
async function workflowNewDiagram() {
if (!workflowModeler) return;
try {
await workflowModeler.importXML(WORKFLOW_EMPTY_BPMN);
workflowModeler.get('canvas').zoom('fit-viewport');
workflowUpdateElementCount();
workflowShowToast('Neues Diagramm erstellt', 'success');
} catch (err) {
console.error('Error creating new diagram:', err);
workflowShowToast('Fehler beim Erstellen', 'error');
}
}
// Open file dialog
function workflowOpenFile() {
document.getElementById('workflow-file-input').click();
}
// Load file from input
async function workflowLoadFile(event) {
const file = event.target.files[0];
if (!file) return;
try {
const xml = await file.text();
await workflowModeler.importXML(xml);
workflowModeler.get('canvas').zoom('fit-viewport');
workflowUpdateElementCount();
workflowShowToast('Datei geladen: ' + file.name, 'success');
} catch (err) {
console.error('Error loading file:', err);
workflowShowToast('Fehler beim Laden der Datei', 'error');
}
// Reset input
event.target.value = '';
}
// Save as XML
async function workflowSaveXML() {
if (!workflowModeler) return;
try {
const { xml } = await workflowModeler.saveXML({ format: true });
workflowDownload(xml, 'process.bpmn', 'application/xml');
workflowShowToast('XML exportiert', 'success');
} catch (err) {
console.error('Error saving XML:', err);
workflowShowToast('Fehler beim Speichern', 'error');
}
}
// Save as SVG
async function workflowSaveSVG() {
if (!workflowModeler) return;
try {
const { svg } = await workflowModeler.saveSVG();
workflowDownload(svg, 'process.svg', 'image/svg+xml');
workflowShowToast('SVG exportiert', 'success');
} catch (err) {
console.error('Error saving SVG:', err);
workflowShowToast('Fehler beim Speichern', 'error');
}
}
// Deploy to Camunda
async function workflowDeploy() {
if (!workflowModeler) return;
if (!workflowCamundaConnected) {
workflowShowToast('Camunda nicht verbunden', 'error');
return;
}
try {
const { xml } = await workflowModeler.saveXML({ format: true });
// Create form data
const formData = new FormData();
formData.append('deployment-name', 'BreakPilot-Process-' + Date.now());
formData.append('data', new Blob([xml], { type: 'application/octet-stream' }), 'process.bpmn');
const response = await fetch('/api/bpmn/deployment/create', {
method: 'POST',
body: formData
});
if (response.ok) {
const result = await response.json();
workflowShowToast('Deployment erfolgreich: ' + result.name, 'success');
console.log('Deployment result:', result);
} else {
const error = await response.text();
throw new Error(error);
}
} catch (err) {
console.error('Error deploying:', err);
workflowShowToast('Deployment fehlgeschlagen', 'error');
}
}
// Show deployed processes panel
async function workflowShowProcesses() {
const panel = document.getElementById('workflow-processes-panel');
const list = document.getElementById('workflow-processes-list');
panel.classList.add('open');
try {
const response = await fetch('/api/bpmn/process-definition');
const processes = await response.json();
if (processes.length === 0) {
list.innerHTML = 'Keine Prozesse deployed
';
return;
}
list.innerHTML = processes.map(p => `
${p.name || p.key}
Version ${p.version} | ${p.key}
`).join('');
} catch (err) {
console.error('Error loading processes:', err);
list.innerHTML = 'Fehler beim Laden
';
}
}
// Hide processes panel
function workflowHideProcesses() {
document.getElementById('workflow-processes-panel').classList.remove('open');
}
// Load process definition XML
async function workflowLoadProcess(definitionId) {
try {
const response = await fetch('/api/bpmn/process-definition/' + definitionId + '/xml');
const data = await response.json();
if (data.bpmn20Xml) {
await workflowModeler.importXML(data.bpmn20Xml);
workflowModeler.get('canvas').zoom('fit-viewport');
workflowUpdateElementCount();
workflowHideProcesses();
workflowShowToast('Prozess geladen', 'success');
}
} catch (err) {
console.error('Error loading process:', err);
workflowShowToast('Fehler beim Laden des Prozesses', 'error');
}
}
// Toggle tasks panel
function workflowToggleTasks() {
const panel = document.getElementById('workflow-tasks-panel');
panel.classList.toggle('visible');
if (panel.classList.contains('visible')) {
workflowLoadTasks();
}
}
// Load pending tasks
async function workflowLoadTasks() {
const list = document.getElementById('workflow-tasks-list');
const count = document.getElementById('workflow-tasks-count');
try {
const response = await fetch('/api/bpmn/tasks/pending');
const tasks = await response.json();
count.textContent = tasks.length;
if (tasks.length === 0) {
list.innerHTML = 'Keine offenen Tasks
';
return;
}
list.innerHTML = tasks.map(t => `
${t.name}
${t.processDefinitionId || 'Prozess'}
`).join('');
} catch (err) {
console.error('Error loading tasks:', err);
list.innerHTML = 'Fehler beim Laden
';
}
}
// Complete a task
async function workflowCompleteTask(taskId) {
try {
const response = await fetch('/api/bpmn/task/' + taskId + '/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
if (response.ok) {
workflowShowToast('Task abgeschlossen', 'success');
workflowLoadTasks();
} else {
throw new Error('Failed to complete task');
}
} catch (err) {
console.error('Error completing task:', err);
workflowShowToast('Fehler beim Abschliessen', 'error');
}
}
// Zoom controls
function workflowZoomIn() {
if (!workflowModeler) return;
const canvas = workflowModeler.get('canvas');
canvas.zoom(canvas.zoom() * 1.2);
}
function workflowZoomOut() {
if (!workflowModeler) return;
const canvas = workflowModeler.get('canvas');
canvas.zoom(canvas.zoom() / 1.2);
}
function workflowZoomFit() {
if (!workflowModeler) return;
workflowModeler.get('canvas').zoom('fit-viewport');
}
// Update element count
function workflowUpdateElementCount() {
if (!workflowModeler) return;
const elementRegistry = workflowModeler.get('elementRegistry');
const count = elementRegistry.getAll().length;
document.getElementById('workflow-element-count').textContent = 'Elemente: ' + count;
}
// Download helper
function workflowDownload(content, filename, contentType) {
const blob = new Blob([content], { type: contentType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = filename;
a.click();
URL.revokeObjectURL(url);
}
// Toast notification
function workflowShowToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = 'workflow-toast ' + type;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => {
toast.remove();
}, 3000);
}
// Module loader function
function loadWorkflowModule() {
console.log('Loading Workflow Module...');
// Small delay to ensure DOM is ready
setTimeout(initWorkflowModule, 100);
}
// Show panel function for module loader
function showWorkflowPanel() {
loadWorkflowModule();
}
// Expose globally
window.loadWorkflowModule = loadWorkflowModule;
window.workflowNewDiagram = workflowNewDiagram;
window.workflowOpenFile = workflowOpenFile;
window.workflowLoadFile = workflowLoadFile;
window.workflowSaveXML = workflowSaveXML;
window.workflowSaveSVG = workflowSaveSVG;
window.workflowDeploy = workflowDeploy;
window.workflowShowProcesses = workflowShowProcesses;
window.workflowHideProcesses = workflowHideProcesses;
window.workflowLoadProcess = workflowLoadProcess;
window.workflowToggleTasks = workflowToggleTasks;
window.workflowCompleteTask = workflowCompleteTask;
window.workflowZoomIn = workflowZoomIn;
window.workflowZoomOut = workflowZoomOut;
window.workflowZoomFit = workflowZoomFit;
"""