(null);
+
+ const _handleStop = useCallback(async (row: SystemWorkflow) => {
+ if (!row.activeRunId) return;
+ setStoppingId(row.id);
+ try {
+ await api.post(`/api/system/workflow-runs/${row.activeRunId}/stop`);
+ showSuccess(t('Stop-Signal gesendet'));
+ await _load();
+ } catch (e: any) {
+ showError(t('Fehler: {msg}', { msg: e?.message || t('Stoppen fehlgeschlagen') }));
+ } finally {
+ setStoppingId(null);
+ }
+ }, [showSuccess, showError, _load, t]);
+
+ const _hasManualTrigger = useCallback((row: SystemWorkflow): boolean => {
+ const invs = row.invocations || [];
+ return invs.some((i) => i.enabled && (i.kind === 'manual' || i.kind === 'api'));
+ }, []);
+
+ const _rawColumns: ColumnConfig[] = useMemo(() => [
+ { key: 'label', label: t('Workflow'), width: 200, sortable: true, filterable: true },
+ {
+ key: 'mandateId',
+ label: t('Mandant'),
+ width: 140,
+ sortable: true,
+ filterable: true,
+ displayField: 'mandateLabel',
+ },
+ {
+ key: 'featureInstanceId',
+ label: t('Instanz'),
+ width: 140,
+ sortable: true,
+ filterable: true,
+ displayField: 'instanceLabel',
+ },
+ {
+ key: 'ownerId',
+ label: t('Benutzer'),
+ width: 140,
+ sortable: true,
+ filterable: true,
+ displayField: 'ownerLabel',
+ },
+ {
+ key: 'active',
+ label: t('Aktiv'),
+ width: 80,
+ sortable: true,
+ filterable: true,
+ },
+ {
+ key: 'isRunning',
+ label: t('Läuft'),
+ width: 80,
+ sortable: true,
+ filterable: true,
+ },
+ {
+ key: 'sysCreatedAt',
+ label: t('Erstellt'),
+ width: 140,
+ sortable: true,
+ filterable: true,
+ formatter: (v: number) => _formatTs(v),
+ },
+ {
+ key: 'lastStartedAt',
+ label: t('Zuletzt gestartet'),
+ width: 160,
+ sortable: true,
+ filterable: true,
+ formatter: (v: number) => _formatTs(v),
+ },
+ {
+ key: 'runCount',
+ label: t('Läufe'),
+ width: 80,
+ sortable: true,
+ filterable: true,
+ formatter: (v: number) => (v != null ? String(v) : '0'),
+ },
+ ], [t]);
+
+ const _columns = useMemo(
+ () => resolveColumnTypes(_rawColumns, backendAttributes),
+ [_rawColumns, backendAttributes],
+ );
+
+ const _hookData = useMemo(() => ({
+ refetch: _load,
+ handleDelete: (id: string) => _handleDelete(id),
+ pagination: paginationMeta,
+ }), [_load, _handleDelete, paginationMeta]);
+
+ return (
+ <>
+
+
+
+ {t('Alle Workflows über alle Features und Mandanten')}
+
+
+
+
+ {(['all', 'active', 'inactive'] as const).map((f) => (
+
+ ))}
+
+
+
+
+
+
+
+ data={workflows}
+ columns={_columns}
+ loading={loading}
+ pagination={true}
+ pageSize={25}
+ searchable={true}
+ filterable={true}
+ sortable={true}
+ selectable={true}
+ initialSort={[{ key: 'sysCreatedAt', direction: 'desc' }]}
+ apiEndpoint="/api/system/workflow-runs/workflows"
+ actionButtons={[
+ {
+ type: 'edit',
+ title: t('bearbeiten'),
+ onAction: _handleEdit,
+ visible: (row: SystemWorkflow) => row.canEdit === true,
+ },
+ {
+ type: 'delete',
+ title: t('löschen'),
+ visible: (row: SystemWorkflow) => row.canDelete === true,
+ },
+ ]}
+ customActions={[
+ {
+ id: 'view',
+ icon: ,
+ title: t('anzeigen'),
+ onClick: (row) => _handleEdit(row),
+ visible: (row) => row.canEdit !== true,
+ },
+ {
+ id: 'rename',
+ icon: ,
+ title: t('umbenennen'),
+ onClick: (row) => _handleRename(row),
+ visible: (row) => row.canEdit === true,
+ },
+ {
+ id: 'activate',
+ icon: ,
+ title: t('aktivieren'),
+ onClick: (row) => _handleToggleActive(row),
+ loading: (row) => togglingId === row.id,
+ visible: (row) => row.canEdit === true && row.active === false,
+ },
+ {
+ id: 'deactivate',
+ icon: ,
+ title: t('deaktivieren'),
+ onClick: (row) => _handleToggleActive(row),
+ loading: (row) => togglingId === row.id,
+ visible: (row) => row.canEdit === true && row.active !== false,
+ },
+ {
+ id: 'execute',
+ icon: ,
+ title: t('ausführen'),
+ onClick: (row) => _handleExecute(row),
+ loading: (row) => executingId === row.id,
+ visible: (row) => row.canExecute === true && _hasManualTrigger(row) && !row.isRunning,
+ },
+ {
+ id: 'stop',
+ icon: ,
+ title: t('stoppen'),
+ onClick: (row) => _handleStop(row),
+ loading: (row) => stoppingId === row.id,
+ visible: (row) => row.isRunning === true && !!row.activeRunId,
+ },
+ ]}
+ onDelete={(row) => _handleDelete(row.id)}
+ onRowClick={(row) => onWorkflowClick?.(row.id)}
+ hookData={_hookData}
+ emptyMessage={t('Keine Workflows gefunden.')}
+ />
+
+
+ >
+ );
+};
+
+// ===========================================================================
+// Workspace Tab (run detail only — no table)
+// ===========================================================================
+
+const _FILE_REF_KEYS = new Set(['fileId', 'documentId', 'fileIds', 'documents']);
+
+function _isPlainObject(v: unknown): v is Record {
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
+}
+
+function _stripFileRefKeys(value: unknown): unknown {
+ if (_isPlainObject(value)) {
+ const out: Record = {};
+ for (const [k, v] of Object.entries(value)) {
+ if (_FILE_REF_KEYS.has(k)) continue;
+ const stripped = _stripFileRefKeys(v);
+ if (stripped !== undefined) out[k] = stripped;
+ }
+ return Object.keys(out).length > 0 ? out : undefined;
+ }
+ if (Array.isArray(value)) {
+ const out = value.map((v) => _stripFileRefKeys(v)).filter((v) => v !== undefined);
+ return out.length > 0 ? out : undefined;
+ }
+ return value;
+}
+
+function _formatScalar(v: unknown): string {
+ if (v === null || v === undefined) return '—';
+ if (typeof v === 'string') return v;
+ if (typeof v === 'number' || typeof v === 'boolean') return String(v);
+ return JSON.stringify(v);
+}
+
+const _DataBlock: React.FC<{ data: unknown; emptyHint?: string }> = ({ data, emptyHint }) => {
+ if (data === undefined || data === null) {
+ return emptyHint ? {emptyHint}
: null;
+ }
+
+ if (_isPlainObject(data)) {
+ const entries = Object.entries(data);
+ if (entries.length === 0) {
+ return emptyHint ? {emptyHint}
: null;
+ }
+ return (
+
+ {entries.map(([k, v]) => {
+ const isComplex = _isPlainObject(v) || Array.isArray(v);
+ if (isComplex) {
+ return (
+
+
+ {k}
+
+
+ {JSON.stringify(v, null, 2)}
+
+
+ );
+ }
+ return (
+
+ {k}
+ {_formatScalar(v)}
+
+ );
+ })}
+
+ );
+ }
+
+ return (
+
+ {JSON.stringify(data, null, 2)}
+
+ );
+};
+
+const _FileLinkList: React.FC<{ files: Array<{ id: string; fileName?: string }> }> = ({ files }) => {
+ if (!files.length) return null;
+ const baseUrl = api.defaults.baseURL || '';
+ return (
+
+ );
+};
+
+const _INTERNAL_EXTRACT_FILENAME_SUBSTR = 'extracted_content_transient';
+
+function _isHiddenWorkflowArtifactFile(f: { fileName?: string }): boolean {
+ return (f.fileName ?? '').toLowerCase().includes(_INTERNAL_EXTRACT_FILENAME_SUBSTR);
+}
+
+const _ProducedFilesSection: React.FC<{
+ steps: Array<{ outputFiles?: Array<{ id: string; fileName?: string }> }>;
+ unassignedFiles?: Array<{ id: string; fileName?: string }>;
+}> = ({ steps, unassignedFiles }) => {
+ const { t } = useLanguage();
+ const seen = new Set();
+ const allFiles: Array<{ id: string; fileName?: string }> = [];
+ for (const step of steps) {
+ for (const f of step.outputFiles ?? []) {
+ if (_isHiddenWorkflowArtifactFile(f)) continue;
+ if (!seen.has(f.id)) { seen.add(f.id); allFiles.push(f); }
+ }
+ }
+ for (const f of unassignedFiles ?? []) {
+ if (_isHiddenWorkflowArtifactFile(f)) continue;
+ if (!seen.has(f.id)) { seen.add(f.id); allFiles.push(f); }
+ }
+ if (!allFiles.length) return null;
+ const baseUrl = api.defaults.baseURL || '';
+ return (
+
+
+
+ {t('Ergebnisse')} ({allFiles.length})
+
+
+
+ );
+};
+
+function _downloadJson(data: unknown, fileName: string) {
+ const json = JSON.stringify(data, null, 2);
+ const blob = new Blob([json], { type: 'application/json' });
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = fileName;
+ a.click();
+ URL.revokeObjectURL(url);
+}
+
+interface _WorkspaceTabProps {
+ runId: string | null;
+ onBack: () => void;
+}
+
+const _TERMINAL_STATUSES = new Set(['completed', 'failed', 'cancelled', 'error', 'stopped']);
+const _POLL_INTERVAL_MS = 3000;
+
+const _WorkspaceTab: React.FC<_WorkspaceTabProps> = ({ runId, onBack }) => {
+ const { t } = useLanguage();
+ const { request } = useApiRequest();
+ const [runDetail, setRunDetail] = useState> | null>(null);
+ const [detailLoading, setDetailLoading] = useState(false);
+
+ const _loadDetail = useCallback(async (id: string) => {
+ setDetailLoading(true);
+ try {
+ const detail = await fetchWorkspaceRunDetail(request, id);
+ setRunDetail(detail);
+ } catch (e) {
+ console.error('Workspace run detail failed', e);
+ } finally {
+ setDetailLoading(false);
+ }
+ }, [request]);
+
+ useEffect(() => {
+ if (runId) _loadDetail(runId);
+ else setRunDetail(null);
+ }, [runId, _loadDetail]);
+
+ useEffect(() => {
+ if (!runId || !runDetail) return;
+ const status = runDetail.run?.status;
+ if (status && _TERMINAL_STATUSES.has(status)) return;
+ const timer = setInterval(() => {
+ fetchWorkspaceRunDetail(request, runId)
+ .then(detail => setRunDetail(detail))
+ .catch(() => {});
+ }, _POLL_INTERVAL_MS);
+ return () => clearInterval(timer);
+ }, [runId, runDetail, request]);
+
+ if (!runId) {
+ return (
+
+
{t('Wähle einen Run im Dashboard aus, um die Details anzuzeigen.')}
+
+ );
+ }
+
+ if (detailLoading || !runDetail) {
+ return ;
+ }
+
+ const { run, steps, workflow, unassignedFiles } = runDetail;
+
+ return (
+
+
+
{run.workflowLabel || run.workflowId}
+
+ {t('Status')}: {run.status}
+ {run.startedAt && {t('Start')}: {_formatTs(run.startedAt)}}
+ {run.completedAt && {t('Ende')}: {_formatTs(run.completedAt)}}
+ {workflow?.targetFeatureInstanceId && {t('Ziel-Instanz')}: {run.targetInstanceLabel || workflow.targetFeatureInstanceId}}
+ {(run.costTokens ?? 0) > 0 && Tokens: {run.costTokens}}
+
+ {run.error && (
+
+ {run.error}
+
+ )}
+ <_ProducedFilesSection steps={steps} unassignedFiles={unassignedFiles} />
+
{t('Schritte')}
+ {steps.length === 0 ? (
+
{t('Keine Schritte protokolliert.')}
+ ) : (
+
+ {steps.map((step) => {
+ const inputData = _stripFileRefKeys(step.inputSnapshot ?? {});
+ const outputData = _stripFileRefKeys(step.output ?? {});
+ const inputFiles = (step.inputFiles ?? []).filter((f) => !_isHiddenWorkflowArtifactFile(f));
+ const outputFiles = (step.outputFiles ?? []).filter((f) => !_isHiddenWorkflowArtifactFile(f));
+ const hasInput = inputData !== undefined || inputFiles.length > 0;
+ const hasOutput = outputData !== undefined || outputFiles.length > 0;
+ return (
+
+
+
+ {step.status}
+
+ {step.nodeType} ({step.nodeId})
+ {step.durationMs != null && {step.durationMs}ms}
+ {(step.tokensUsed ?? 0) > 0 && {step.tokensUsed} tokens}
+
+
+ {hasInput && (
+
+
+ {t('Input')}
+ {inputData !== undefined && inputData !== null && (
+
+ )}
+
+ <_DataBlock data={inputData} />
+ <_FileLinkList files={inputFiles} />
+
+ )}
+ {hasOutput && (
+
+
+ {t('Output')}
+ {outputData !== undefined && outputData !== null && (
+
+ )}
+
+ <_DataBlock data={outputData} />
+ <_FileLinkList files={outputFiles} />
+
+ )}
+ {step.error && (
+
+
+ {t('Fehler')}
+
+ {step.error}
+
+ )}
+
+ {step.startedAt && {t('Start')}: {_formatTs(step.startedAt)}}
+ {step.completedAt && {t('Ende')}: {_formatTs(step.completedAt)}}
+ {(step.retryCount ?? 0) > 0 && {t('Wiederholungen')}: {step.retryCount}}
+
+
+
+ );
+ })}
+
+ )}
+ {(() => {
+ const visibleUnassigned = (unassignedFiles ?? []).filter((f) => !_isHiddenWorkflowArtifactFile(f));
+ if (!visibleUnassigned.length) return null;
+ return (
+ <>
+
{t('Sonstige Dokumente')}
+ <_FileLinkList files={visibleUnassigned} />
+ >
+ );
+ })()}
+
+ );
+};
+
+// ===========================================================================
+// Editor Tab — wraps GraphicalEditorPage with auto-resolved instanceId
+// ===========================================================================
+
+const _EditorTab: React.FC = () => {
+ const { t } = useLanguage();
+ const { dynamicBlock } = useNavigation();
+
+ const editorInstance = useMemo(
+ () => _findAnyEditorInstance(dynamicBlock),
+ [dynamicBlock],
+ );
+
+ if (!editorInstance) {
+ return (
+
+
{t('Kein Editor verfügbar. Bitte erstelle zuerst eine Feature-Instanz mit Editor (Workspace oder Graphical Editor).')}
+
+ );
+ }
+
+ return (
+
+
+
+ );
+};
+
+// ===========================================================================
+// Templates Tab — wraps GraphicalEditorTemplatesPage with auto-resolved instanceId
+// ===========================================================================
+
+const _TemplatesTab: React.FC = () => {
+ const { t } = useLanguage();
+ const { dynamicBlock } = useNavigation();
+
+ const editorInstance = useMemo(
+ () => _findAnyEditorInstance(dynamicBlock),
+ [dynamicBlock],
+ );
+
+ if (!editorInstance) {
+ return (
+
+
{t('Kein Editor verfügbar. Bitte erstelle zuerst eine Feature-Instanz mit Editor.')}
+
+ );
+ }
+
+ return (
+
+ );
+};
+
+// ===========================================================================
+// Main page with Tabs
+// ===========================================================================
+
+const _TAB_ALIASES: Record = {
+ dashboard: 'runs',
+ workspace: 'detail',
+};
+
+export const WorkflowAutomationPage: React.FC = () => {
+ const { t } = useLanguage();
+ const [searchParams] = useSearchParams();
+
+ const rawTab = searchParams.get('tab') || 'workflows';
+ const initialTab = _TAB_ALIASES[rawTab] || rawTab;
+ const initialRunId = searchParams.get('runId') || null;
+
+ const [activeTab, setActiveTab] = useState(initialRunId ? 'detail' : initialTab);
+ const [selectedRunId, setSelectedRunId] = useState(initialRunId);
+ const [workflowFilter, setWorkflowFilter] = useState(null);
+
+ const _handleWorkflowClick = useCallback((workflowId: string) => {
+ setWorkflowFilter(workflowId);
+ setActiveTab('runs');
+ }, []);
+
+ useEffect(() => {
+ if (workflowFilter) setWorkflowFilter(null);
+ }, [workflowFilter]);
+
+ const _handleRunClick = useCallback((runId: string) => {
+ setSelectedRunId(runId);
+ setActiveTab('detail');
+ }, []);
+
+ const _handleBackFromWorkspace = useCallback(() => {
+ setSelectedRunId(null);
+ setActiveTab('runs');
+ }, []);
+
+ const tabs = useMemo(() => [
+ {
+ id: 'workflows',
+ label: t('Workflows'),
+ content: <_WorkflowsTab onWorkflowClick={_handleWorkflowClick} />,
+ },
+ {
+ id: 'editor',
+ label: t('Editor'),
+ content: <_EditorTab />,
+ },
+ {
+ id: 'templates',
+ label: t('Vorlagen'),
+ content: <_TemplatesTab />,
+ },
+ {
+ id: 'runs',
+ label: t('Workflow-Durchläufe'),
+ content: <_DashboardTab workflowFilter={workflowFilter} onRunClick={_handleRunClick} />,
+ },
+ {
+ id: 'detail',
+ label: t('Durchlauf-Details'),
+ content: <_WorkspaceTab runId={selectedRunId} onBack={_handleBackFromWorkspace} />,
+ },
+ ], [t, _handleWorkflowClick, workflowFilter, _handleRunClick, selectedRunId, _handleBackFromWorkspace]);
+
+ return (
+
+
{t('Workflow-Automation')}
+
+
+ );
+};
+
+export default WorkflowAutomationPage;
diff --git a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
index 8cb5615..6faf39c 100644
--- a/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
+++ b/src/pages/views/graphicalEditor/GraphicalEditorTemplatesPage.tsx
@@ -44,7 +44,15 @@ function _formatTs(ts?: number): string {
return time;
}
-export const GraphicalEditorTemplatesPage: React.FC = () => {
+interface GraphicalEditorTemplatesPageProps {
+ persistentInstanceId?: string;
+ persistentMandateId?: string;
+}
+
+export const GraphicalEditorTemplatesPage: React.FC = ({
+ persistentInstanceId,
+ persistentMandateId,
+}) => {
const { t } = useLanguage();
const scopeLabels = useMemo(
@@ -57,8 +65,10 @@ export const GraphicalEditorTemplatesPage: React.FC = () => {
[t],
);
- const instanceId = useInstanceId();
- const { mandateId } = useParams<{ mandateId: string }>();
+ const urlInstanceId = useInstanceId();
+ const { mandateId: urlMandateId } = useParams<{ mandateId: string }>();
+ const instanceId = persistentInstanceId || urlInstanceId;
+ const mandateId = persistentMandateId || urlMandateId;
const { request } = useApiRequest();
const navigate = useNavigate();
const { showSuccess, showError } = useToast();
diff --git a/src/types/mandate.ts b/src/types/mandate.ts
index 3ba7f92..f112c36 100644
--- a/src/types/mandate.ts
+++ b/src/types/mandate.ts
@@ -182,113 +182,9 @@ export interface FeatureConfig {
deprecated?: boolean;
}
-// =============================================================================
-// FEATURE REGISTRY (DEPRECATED)
-// =============================================================================
-
-/**
- * @deprecated Since Navigation-API-Konzept implementation.
- *
- * Navigation is now provided by the backend via GET /api/navigation.
- * The backend is the Single Source of Truth for navigation structure.
- *
- * Icon mapping is now handled by src/config/pageRegistry.ts using uiComponent codes.
- *
- * This registry is kept for backward compatibility with existing code that may
- * still reference it. It will be removed in a future version.
- *
- * TODO: Remove after all references are migrated to use backend navigation.
- */
-export const FEATURE_REGISTRY: Record = {
- trustee: {
- code: 'trustee',
- label: 'Treuhand',
- icon: 'briefcase',
- views: [
- { code: 'dashboard', label: 'Übersicht', path: 'dashboard' },
- { code: 'data-tables', label: 'Daten-Tabellen', path: 'data-tables' },
- { code: 'position-documents', label: 'Zuordnungen', path: 'position-documents' },
- { code: 'import-process', label: 'Import & Verarbeitung', path: 'import-process' },
- { code: 'instance-roles', label: 'Rollen & Rechte', path: 'instance-roles', adminOnly: true },
- { code: 'settings', label: 'Buchhaltungseinstellungen', path: 'settings' },
- ]
- },
- chatworkflow: {
- code: 'chatworkflow',
- label: 'Workflow',
- icon: 'play_circle',
- views: [
- { code: 'dashboard', label: 'Übersicht', path: 'dashboard' },
- { code: 'runs', label: 'Runs', path: 'runs' },
- { code: 'files', label: 'Dateien', path: 'files' },
- ]
- },
- realestate: {
- code: 'realestate',
- label: 'Immobilien',
- icon: 'home',
- views: [
- { code: 'dashboard', label: 'Karte', path: 'dashboard' },
- { code: 'instance-roles', label: 'Rollen & Rechte', path: 'instance-roles', adminOnly: true },
- ]
- },
- teamsbot: {
- code: 'teamsbot',
- label: 'Teams Bot',
- icon: 'headset_mic',
- views: [
- { code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
- { code: 'assistant', label: 'Assistent', path: 'assistant' },
- { code: 'modules', label: 'Module', path: 'modules' },
- { code: 'sessions', label: 'Live-Session', path: 'sessions' },
- { code: 'settings', label: 'Einstellungen', path: 'settings' },
- ]
- },
- graphicalEditor: {
- code: 'graphicalEditor',
- label: 'Grafischer Editor',
- icon: 'sitemap',
- views: [
- { code: 'editor', label: 'Editor', path: 'editor' },
- { code: 'templates', label: 'Vorlagen', path: 'templates' },
- { code: 'workflows-tasks', label: 'Tasks', path: 'workflows-tasks' },
- { code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
- ]
- },
- neutralization: {
- code: 'neutralization',
- label: 'Neutralisierung',
- icon: 'shield_check',
- views: [
- { code: 'dashboard', label: 'Neutralisierung testen', path: 'playground' },
- { code: 'playground', label: 'Neutralisierung testen', path: 'playground' },
- { code: 'config', label: 'Einstellungen', path: 'config' },
- { code: 'attributes', label: 'Attribute', path: 'attributes' },
- ]
- },
- commcoach: {
- code: 'commcoach',
- label: 'Kommunikations-Coach',
- icon: 'account_voice',
- views: [
- { code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
- { code: 'assistant', label: 'Assistent', path: 'assistant' },
- { code: 'modules', label: 'Module', path: 'modules' },
- { code: 'session', label: 'Session', path: 'session' },
- { code: 'settings', label: 'Einstellungen', path: 'settings' },
- ]
- },
- workspace: {
- code: 'workspace',
- label: 'AI Workspace',
- icon: 'psychology',
- views: [
- { code: 'dashboard', label: 'Dashboard', path: 'dashboard' },
- { code: 'editor', label: 'Editor', path: 'editor' },
- { code: 'settings', label: 'Einstellungen', path: 'settings' },
- ]
- },
-};
+// FEATURE_REGISTRY removed (2026-06-07).
+// Navigation is provided by the backend via GET /api/navigation.
+// Icon mapping is handled by src/config/pageRegistry.ts using uiComponent codes.
// =============================================================================
// HELPERS
From cd14babb2e37b1424aafa3d06145edc4bf085f41 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Mon, 8 Jun 2026 10:31:24 +0200
Subject: [PATCH 3/7] refactory workflowAutomation completed as system
component reolacing automation2 and graphEditor
---
src/App.tsx | 9 +-
src/api/workflowApi.ts | 1204 ++-----------
src/api/workflowAutomationApi.ts | 1193 +++++++++++++
.../editor/Automation2FlowEditor.tsx | 88 +-
.../FlowEditor/editor/EditorChatPanel.tsx | 10 +-
.../editor/EditorWorkflowChatList.tsx | 8 +-
.../FlowEditor/editor/RunTracingPanel.tsx | 4 +-
.../FlowEditor/editor/TemplatePicker.tsx | 2 +-
.../frontendTypeRenderers/CaseListEditor.tsx | 2 +-
.../frontendTypeRenderers/ConditionEditor.tsx | 2 +-
.../FeatureInstancePicker.tsx | 4 +-
.../nodes/frontendTypeRenderers/index.tsx | 4 +-
.../FlowEditor/nodes/shared/DataPicker.tsx | 2 +-
src/components/UnifiedDataBar/FilesTab.tsx | 44 +-
.../UnifiedDataBar/UnifiedDataBar.tsx | 17 +-
.../workflowAutomation/FlowEditor/index.ts | 8 +
src/config/keepAliveRoutes.tsx | 6 +
src/config/pageRegistry.tsx | 3 +-
src/hooks/useIntegrationsOverview.ts | 12 +-
src/hooks/useWorkflows.ts | 44 +-
src/pages/AutomationsDashboardPage.tsx | 22 -
src/pages/WorkflowAutomationPage.tsx | 1565 -----------------
.../Automation2WorkflowsTasks.module.css | 513 ------
.../GraphicalEditorWorkflowsTasksPage.tsx | 943 ----------
.../views/trustee/TrusteeAbschlussView.tsx | 11 +-
.../views/trustee/TrusteeAnalyseView.tsx | 11 +-
.../trustee/TrusteeExpenseImportView.tsx | 21 +-
.../views/trustee/TrusteeScanUploadView.tsx | 12 +-
.../views/trustee/trusteePipelineGraph.ts | 6 +-
.../WorkflowEditorPage.tsx} | 29 +-
.../WorkflowTemplatesPage.tsx} | 29 +-
.../WorkflowAutomationHubPage.tsx | 138 ++
.../workflowAutomation/tabs/EditorTab.tsx | 46 +
.../workflowAutomation/tabs/RunDetailTab.tsx | 336 ++++
src/pages/workflowAutomation/tabs/RunsTab.tsx | 564 ++++++
.../workflowAutomation/tabs/TasksTab.tsx | 221 +++
.../workflowAutomation/tabs/TemplatesTab.tsx | 43 +
.../workflowAutomation/tabs/WorkflowsTab.tsx | 400 +++++
src/pages/workflowAutomation/types.ts | 184 ++
src/styles/themes/light.css | 4 +-
40 files changed, 3422 insertions(+), 4342 deletions(-)
create mode 100644 src/api/workflowAutomationApi.ts
create mode 100644 src/components/workflowAutomation/FlowEditor/index.ts
delete mode 100644 src/pages/AutomationsDashboardPage.tsx
delete mode 100644 src/pages/WorkflowAutomationPage.tsx
delete mode 100644 src/pages/views/graphicalEditor/Automation2WorkflowsTasks.module.css
delete mode 100644 src/pages/views/graphicalEditor/GraphicalEditorWorkflowsTasksPage.tsx
rename src/pages/views/{graphicalEditor/GraphicalEditorPage.tsx => workflowAutomation/WorkflowEditorPage.tsx} (86%)
rename src/pages/views/{graphicalEditor/GraphicalEditorTemplatesPage.tsx => workflowAutomation/WorkflowTemplatesPage.tsx} (92%)
create mode 100644 src/pages/workflowAutomation/WorkflowAutomationHubPage.tsx
create mode 100644 src/pages/workflowAutomation/tabs/EditorTab.tsx
create mode 100644 src/pages/workflowAutomation/tabs/RunDetailTab.tsx
create mode 100644 src/pages/workflowAutomation/tabs/RunsTab.tsx
create mode 100644 src/pages/workflowAutomation/tabs/TasksTab.tsx
create mode 100644 src/pages/workflowAutomation/tabs/TemplatesTab.tsx
create mode 100644 src/pages/workflowAutomation/tabs/WorkflowsTab.tsx
create mode 100644 src/pages/workflowAutomation/types.ts
diff --git a/src/App.tsx b/src/App.tsx
index 15019ad..5c4d66b 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -43,8 +43,7 @@ import { AccessManagementHub, AdminMandatesPage, AdminUsersPage, AdminUserMandat
import { AdminMandateWizardPage, AdminInvitationWizardPage } from './pages/admin/wizards';
import { PromptsPage, FilesPage, ConnectionsPage } from './pages/basedata';
import { BillingDataView, BillingAdmin, BillingMandateView, AdminSubscriptionsPage } from './pages/billing';
-import { AutomationsDashboardPage } from './pages/AutomationsDashboardPage';
-import { WorkflowAutomationPage } from './pages/WorkflowAutomationPage';
+import { WorkflowAutomationPage } from './pages/workflowAutomation/WorkflowAutomationHubPage';
import { RagInventoryPage } from './pages/RagInventoryPage';
import { ComplianceAuditPage } from './pages/ComplianceAuditPage';
function App() {
@@ -124,11 +123,6 @@ function App() {
} />
- {/* ============================================== */}
- {/* AUTOMATIONS DASHBOARD */}
- {/* ============================================== */}
- } />
-
{/* ============================================== */}
{/* WORKFLOW AUTOMATION (System-Komponente) */}
{/* ============================================== */}
@@ -143,6 +137,7 @@ function App() {
} />
} />
+
{/* ============================================== */}
{/* FEATURE-INSTANZ ROUTES */}
{/* /mandates/:mandateId/:featureCode/:instanceId */}
diff --git a/src/api/workflowApi.ts b/src/api/workflowApi.ts
index 8954c44..ba1cc89 100644
--- a/src/api/workflowApi.ts
+++ b/src/api/workflowApi.ts
@@ -1,989 +1,135 @@
/**
- * Workflow API (GraphicalEditor)
- * Node types and graph execution for n8n-style flows.
- */
-
-import type { ApiRequestOptions } from '../hooks/useApi';
-
-const LOG = '[Workflow]';
-
-// ============================================================================
-// TYPES
-// ============================================================================
-
-export interface NodeTypeParameter {
- name: string;
- type: string;
- required?: boolean;
- description?: string;
- default?: unknown;
- frontendType?: string;
- frontendOptions?: Record;
- options?: unknown[];
- validation?: Record;
-}
-
-export interface PortField {
- name: string;
- type: string;
- /** Plain string or per-language map from the API catalog. */
- description: string | Record;
- required: boolean;
- enumValues?: string[] | null;
- /** When true, surface at the top of the DataPicker as the most common/recommended pick. */
- recommended?: boolean;
- /** Human label from portTypeCatalog (backend). Preferred over technical path in DataPicker. */
- pickerLabel?: string | null;
- /** Backend: segment for one list element (between List field and nested field). */
- pickerItemLabel?: string | null;
-}
-
-export interface PortSchema {
- name: string;
- fields: PortField[];
-}
-
-/** One pickable binding — defined on ``outputPorts[n].dataPickOptions`` (authoritative list from gateway). */
-export interface DataPickOption {
- path: (string | number)[];
- pickerLabel: string;
- detail?: string;
- recommended?: boolean;
- iterable?: boolean;
- /** For display and optional strict compatibility (e.g. str, Any). */
- type?: string;
-}
-
-/** @deprecated Prefer ``outputPorts[].dataPickOptions``; kept for older payloads. */
-export type OutputPickHint = DataPickOption;
-
-export interface InputPortDef {
- accepts: string[];
-}
-
-/** Graph-defined output schema (e.g. form fields from node parameters). */
-export interface GraphDefinedSchemaRef {
- kind: 'fromGraph';
- parameter: string;
-}
-
-export interface OutputPortDef {
- schema: string | GraphDefinedSchemaRef;
- dynamic?: boolean;
- deriveFrom?: string;
- /**
- * When set, DataPicker uses **only** this list for that port (no portTypeCatalog expansion).
- * Authoritative, like `parameters` for node configuration.
- */
- dataPickOptions?: DataPickOption[];
-}
-
-export interface NodeType {
- id: string;
- category: string;
- label: string;
- description: string;
- parameters: NodeTypeParameter[];
- inputs: number;
- outputs: number;
- outputLabels?: string[];
- executor: string;
- inputPorts?: Record;
- outputPorts?: Record;
- meta?: {
- icon?: string;
- color?: string;
- /** True if this node performs an LLM / AI call (credits). */
- usesAi?: boolean;
- method?: string;
- action?: string;
- };
-}
-export interface NodeTypeCategory {
- id: string;
- label: Record | string;
-}
-
-export interface SystemVariable {
- type: string;
- description: string;
-}
-
-/** Single form field type with its canonical port primitive. Delivered by GET /node-types. */
-export interface FormFieldType {
- id: string;
- label: string;
- portType: string;
-}
-
-export interface ConditionOperatorDef {
- id: string;
- label: string;
- labelKey?: string;
- needsValue: boolean;
- valueInput?: { kind: string; options?: string[] };
-}
-
-export interface NodeTypesResponse {
- nodeTypes: NodeType[];
- categories: NodeTypeCategory[];
- portTypeCatalog?: Record;
- conditionOperatorCatalog?: Record;
- systemVariables?: Record;
- formFieldTypes?: FormFieldType[];
-}
-
-export interface Automation2GraphNode {
- id: string;
- type: string;
- parameters?: Record;
- inputPorts?: Array<{ name: string; schema: string; accepts?: string[] }>;
- outputPorts?: Array<{ name: string; schema: string | GraphDefinedSchemaRef }>;
-}
-
-export interface Automation2Connection {
- source: string;
- target: string;
- sourceOutput?: number;
- targetInput?: number;
-}
-
-export interface Automation2Graph {
- nodes: Automation2GraphNode[];
- connections: Automation2Connection[];
-}
-
-export interface ExecuteGraphResponse {
- success: boolean;
- nodeOutputs?: Record;
- error?: string;
- /** Soft, non-blocking message displayed alongside a successful response.
- * Used e.g. by the Save flow to surface "Gespeichert mit X Pflicht-Fehlern"
- * without flipping `success` to `false`. */
- warning?: string;
- stopped?: boolean;
- failedNode?: string;
- paused?: boolean;
- taskId?: string;
- runId?: string;
- nodeId?: string;
-}
-
-/** Entry point / start configured outside the canvas (manual, form, schedule, …) */
-export interface WorkflowEntryPoint {
- id: string;
- kind: string;
- category: 'on_demand' | 'always_on';
- enabled: boolean;
- title: Record | string;
- description?: Record;
- config: Record;
-}
-
-export interface Automation2Workflow {
- id: string;
- label: string;
- graph: Automation2Graph;
- active?: boolean;
- /** Target feature instance for execution data scope (NULL for templates) */
- targetFeatureInstanceId?: string | null;
- /** Entry points (Starts) — how this workflow may be invoked */
- invocations?: WorkflowEntryPoint[];
- /** Enriched: run count */
- runCount?: number;
- /** Enriched: has active (running/paused) run */
- isRunning?: boolean;
- /** Enriched: status of active run */
- runStatus?: string;
- /** Enriched: nodeId where workflow is stuck (paused) */
- stuckAtNodeId?: string;
- /** Enriched: human-readable label for stuck node */
- stuckAtNodeLabel?: string;
- /** From PowerOnModel base — record creation timestamp (seconds) */
- sysCreatedAt?: number;
- /** Enriched: last run started timestamp (seconds) */
- lastStartedAt?: number;
-}
-
-// ============================================================================
-// AUTO-PREFIX TYPES (Greenfield)
-// ============================================================================
-
-export type AutoWorkflowStatus = 'draft' | 'published' | 'archived';
-export type AutoRunStatus = 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
-export type AutoStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
-export type AutoTaskStatus = 'pending' | 'completed' | 'cancelled' | 'expired';
-export type AutoTemplateScope = 'user' | 'instance' | 'mandate' | 'system';
-
-export interface AutoVersion {
- id: string;
- workflowId: string;
- versionNumber: number;
- status: AutoWorkflowStatus;
- graph: Automation2Graph;
- invocations?: WorkflowEntryPoint[];
- publishedAt?: number;
- publishedBy?: string;
-}
-
-export interface AutoRun {
- id: string;
- workflowId: string;
- versionId?: string;
- status: AutoRunStatus;
- trigger?: Record;
- startedAt?: number;
- completedAt?: number;
- nodeOutputs?: Record;
- currentNodeId?: string;
- resumeContext?: Record;
- error?: string;
- costTokens?: number;
- costCredits?: number;
-}
-
-export interface AutoWorkflow {
- id: string;
- mandateId: string;
- featureInstanceId: string;
- label: string;
- description?: string;
- tags?: string[];
- isTemplate: boolean;
- templateSourceId?: string;
- templateScope?: AutoTemplateScope;
- sharedReadOnly?: boolean;
- currentVersionId?: string;
- active: boolean;
- eventId?: string;
- notifyOnFailure?: boolean;
- graph: Automation2Graph;
- invocations?: WorkflowEntryPoint[];
- sysCreatedBy?: string;
- sysCreatedAt?: number;
- sysModifiedBy?: string;
- sysModifiedAt?: number;
-}
-
-export interface AutoTask {
- id: string;
- runId: string;
- workflowId: string;
- nodeId: string;
- nodeType: string;
- config: Record;
- assigneeId?: string;
- status: AutoTaskStatus;
- result?: Record;
- expiresAt?: number;
- sysCreatedAt?: number;
-}
-
-export interface AutoStepLog {
- id: string;
- runId: string;
- nodeId: string;
- nodeType: string;
- status: AutoStepStatus;
- inputSnapshot?: Record;
- output?: Record;
- error?: string;
- startedAt?: number;
- completedAt?: number;
- durationMs?: number;
- tokensUsed?: number;
- retryCount?: number;
-}
-
-// ============================================================================
-// API FUNCTIONS
-// ============================================================================
-
-export type ApiRequestFunction = (options: ApiRequestOptions) => Promise;
-
-/**
- * Fetch node types for the flow builder (backend-driven).
- * GET /api/workflows/{instanceId}/node-types?language=de
- */
-export async function fetchNodeTypes(
- request: ApiRequestFunction,
- instanceId: string,
- language = 'de'
-): Promise {
- console.log(`${LOG} fetchNodeTypes: instanceId=${instanceId} language=${language}`);
- const data = await request({
- url: `/api/workflows/${instanceId}/node-types`,
- method: 'get',
- params: { language },
- });
- const nodeTypes = data?.nodeTypes ?? [];
- const categories = data?.categories ?? [];
- const portTypeCatalog = data?.portTypeCatalog ?? undefined;
- const conditionOperatorCatalog = data?.conditionOperatorCatalog ?? undefined;
- const systemVariables = data?.systemVariables ?? undefined;
- const formFieldTypes = data?.formFieldTypes ?? undefined;
- console.log(
- `${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
- `${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
- `${conditionOperatorCatalog ? Object.keys(conditionOperatorCatalog).length : 0} conditionKinds, ` +
- `${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` +
- `${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes`
- );
- return { nodeTypes, categories, portTypeCatalog, conditionOperatorCatalog, systemVariables, formFieldTypes };
-}
-
-export interface UpstreamPathEntry {
- producerNodeId: string;
- producerLabel?: string;
- path: (string | number)[];
- type: string;
- label: string;
- scopeOrigin: 'data' | 'loop' | 'system';
- valueKind?: string;
-}
-
-export interface ConditionMetaResponse {
- valueKind: string;
- operators: ConditionOperatorDef[];
-}
-
-export interface ConditionMetaRequest {
- graph: Automation2Graph;
- nodeId?: string;
- ref: { type: 'ref'; nodeId: string; path: (string | number)[] };
-}
-
-/**
- * POST /api/workflows/{instanceId}/condition-meta — operators for a DataRef (If/Else).
- */
-export async function fetchConditionMeta(
- request: ApiRequestFunction,
- instanceId: string,
- body: ConditionMetaRequest,
- language = 'de'
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/condition-meta`,
- method: 'post',
- params: { language },
- data: body,
- });
- return {
- valueKind: String(data?.valueKind ?? 'unknown'),
- operators: (data?.operators ?? []) as ConditionOperatorDef[],
- };
-}
-
-/**
- * POST /api/workflows/{instanceId}/upstream-paths — pickable upstream paths for DataPicker / AI.
- */
-export async function postUpstreamPaths(
- request: ApiRequestFunction,
- instanceId: string,
- graph: Automation2Graph,
- nodeId: string
-): Promise<{ paths: UpstreamPathEntry[] }> {
- const data = await request({
- url: `/api/workflows/${instanceId}/upstream-paths`,
- method: 'post',
- data: { graph, nodeId },
- });
- return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
-}
-
-/** Scope-aware data sources for the DataPicker — all loop-scope logic lives on the backend. */
-export interface GraphDataSources {
- /** Ancestor node IDs that are valid sources (loop body nodes excluded when on Done branch). */
- availableSourceIds: string[];
- /** Maps nodeId → output port index to use instead of 0 (e.g. loop node on Done branch → 1). */
- portIndexOverrides: Record;
- /** IDs of flow.loop nodes whose body the current node is inside (show currentItem etc.). */
- loopBodyContextIds: string[];
-}
-
-/**
- * POST /api/workflows/{instanceId}/graph-data-sources
+ * Workflow API (WorkflowAutomation) — LEGACY RE-EXPORT LAYER
*
- * Returns scope-aware source list so the DataPicker needs zero graph-traversal logic.
- * The graph connections must use { source, target, sourceOutput?, targetInput? } format.
- */
-export async function fetchGraphDataSources(
- request: ApiRequestFunction,
- instanceId: string,
- nodeId: string,
- nodes: Array<{ id: string; type?: string }>,
- connections: Array<{ source: string; target: string; sourceOutput?: number; targetInput?: number }>,
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/graph-data-sources`,
- method: 'post',
- data: { nodeId, graph: { nodes, connections } },
- });
- return {
- availableSourceIds: data?.availableSourceIds ?? [],
- portIndexOverrides: data?.portIndexOverrides ?? {},
- loopBodyContextIds: data?.loopBodyContextIds ?? [],
- };
-}
-
-/** GET saved workflow graph variant of upstream-paths (requires workflowId). */
-export async function getUpstreamPathsSaved(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string,
- nodeId: string
-): Promise<{ paths: UpstreamPathEntry[] }> {
- const data = await request({
- url: `/api/workflows/${instanceId}/upstream-paths/${encodeURIComponent(nodeId)}`,
- method: 'get',
- params: { workflowId },
- });
- return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
-}
-
-/**
- * Execute an automation2 graph.
- * POST /api/workflows/{instanceId}/execute
- */
-export interface ExecuteGraphOptions {
- /** Use a configured start on the saved workflow */
- entryPointId?: string;
- /** Full run envelope (overrides entry point mapping) */
- runEnvelope?: Record;
- /** Merged into envelope.payload */
- payload?: Record;
-}
-
-export async function executeGraph(
- request: ApiRequestFunction,
- instanceId: string,
- graph: Automation2Graph,
- workflowId?: string,
- options?: ExecuteGraphOptions
-): Promise {
- console.log(
- `${LOG} executeGraph request: instanceId=${instanceId} workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`,
- { nodes: graph.nodes, connections: graph.connections, options }
- );
- const start = performance.now();
- try {
- const data: Record = { graph, workflowId };
- if (options?.entryPointId) data.entryPointId = options.entryPointId;
- if (options?.runEnvelope) data.runEnvelope = options.runEnvelope;
- if (options?.payload && Object.keys(options.payload).length > 0) data.payload = options.payload;
- const result = await request({
- url: `/api/workflows/${instanceId}/execute`,
- method: 'post',
- data,
- });
- const ms = Math.round(performance.now() - start);
- console.log(
- `${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`,
- result
- );
- return result;
- } catch (err) {
- const ms = Math.round(performance.now() - start);
- console.error(
- `${LOG} executeGraph FAILED (${ms}ms): instanceId=${instanceId}`,
- err
- );
- throw err;
- }
-}
-
-// -------------------------------------------------------------------------
-// Workflows CRUD
-// -------------------------------------------------------------------------
-
-export async function fetchWorkflows(
- request: ApiRequestFunction,
- instanceId: string,
- params?: { active?: boolean; pagination?: any }
-): Promise {
- const queryParams: Record = {};
- if (params?.active !== undefined) queryParams.active = params.active;
- if (params?.pagination) queryParams.pagination = JSON.stringify(params.pagination);
- const data = await request({
- url: `/api/workflows/${instanceId}/workflows`,
- method: 'get',
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
- });
- if (data?.items && data?.pagination) return data;
- return data?.workflows ?? [];
-}
-
-export async function fetchWorkflow(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}`,
- method: 'get',
- });
-}
-
-export async function createWorkflow(
- request: ApiRequestFunction,
- instanceId: string,
- body: {
- label: string;
- graph: Automation2Graph;
- invocations?: WorkflowEntryPoint[];
- targetFeatureInstanceId?: string | null;
- }
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/workflows`,
- method: 'post',
- data: body,
- });
-}
-
-export async function updateWorkflow(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string,
- body: {
- label?: string;
- graph?: Automation2Graph;
- invocations?: WorkflowEntryPoint[];
- active?: boolean;
- notifyOnFailure?: boolean;
- targetFeatureInstanceId?: string | null;
- }
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}`,
- method: 'put',
- data: body,
- });
-}
-
-export async function deleteWorkflow(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string
-): Promise {
- await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}`,
- method: 'delete',
- });
-}
-
-// -------------------------------------------------------------------------
-// Workflow file IO (envelopeVersioned, .workflow.json)
-// -------------------------------------------------------------------------
-
-/** envelopeVersioned schema 1.0 — keys mirror the gateway constants. */
-export const WORKFLOW_FILE_SCHEMA_VERSION = '1.0';
-export const WORKFLOW_FILE_KIND = 'poweron.workflow';
-export const WORKFLOW_FILE_EXTENSION = '.workflow.json';
-
-export interface WorkflowFileEnvelope {
- $schemaVersion: string;
- $kind: string;
- $exportedAt?: string;
- $gatewayVersion?: string;
- label: string;
- description?: string;
- tags?: string[];
- templateScope?: AutoTemplateScope;
- sharedReadOnly?: boolean;
- notifyOnFailure?: boolean;
- graph: Automation2Graph;
- invocations?: WorkflowEntryPoint[];
-}
-
-export interface ImportWorkflowResponse {
- workflow: AutoWorkflow;
- warnings: string[];
- created: boolean;
-}
-
-export interface ImportWorkflowOptions {
- /** Inline envelope payload (preferred for round-trip in the editor). */
- envelope?: WorkflowFileEnvelope;
- /** UDB FileItem.id of a previously uploaded ``.workflow.json``. */
- fileId?: string;
- /** When set, the existing workflow is replaced instead of a new one being created. */
- existingWorkflowId?: string;
-}
-
-/** POST /api/workflows/{instanceId}/workflows/import */
-export async function importWorkflowFromFile(
- request: ApiRequestFunction,
- instanceId: string,
- options: ImportWorkflowOptions,
-): Promise {
- if (!options.envelope && !options.fileId) {
- throw new Error('importWorkflowFromFile: either envelope or fileId is required');
- }
- return await request({
- url: `/api/workflows/${instanceId}/workflows/import`,
- method: 'post',
- data: options,
- });
-}
-
-export interface ExportWorkflowResult {
- fileName: string;
- envelope: WorkflowFileEnvelope;
-}
-
-/**
- * GET /api/workflows/{instanceId}/workflows/{workflowId}/export
+ * All functions have been migrated to workflowAutomationApi.ts (mandate-scoped).
+ * This file re-exports them so existing consumers still resolve.
*
- * The backend returns ``{ fileName, envelope }`` when ``download=false`` and a
- * raw JSON download (``Content-Disposition: attachment``) when ``download=true``.
- * For programmatic use (e.g. re-uploading to UDB) keep download=false.
+ * Only ClickUp-specific functions and `deleteSystemWorkflow` remain here
+ * because their endpoints are not yet covered by the new unified API.
*/
-export async function exportWorkflowToFile(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string,
- download = false,
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}/export`,
- method: 'get',
- params: { download },
- });
-}
-/** Quick content-sniffing — used by the UDB FilesTab to flag workflow files. */
-export function isWorkflowFileContent(payload: unknown): boolean {
- if (!payload || typeof payload !== 'object') return false;
- const p = payload as Record;
- return (
- typeof p.$schemaVersion === 'string' &&
- p.$kind === WORKFLOW_FILE_KIND &&
- !!p.graph &&
- typeof p.graph === 'object'
- );
-}
+// ============================================================================
+// RE-EXPORTS — Types & Interfaces
+// ============================================================================
-/** Suggest a safe filename from the workflow label (mirrors gateway buildFileName). */
-export function workflowFileNameFor(label: string): string {
- const slug = (label || 'workflow')
- .toLowerCase()
- .replace(/[^a-z0-9._-]+/g, '-')
- .replace(/^-+|-+$/g, '')
- .slice(0, 80) || 'workflow';
- return `${slug}${WORKFLOW_FILE_EXTENSION}`;
-}
+export type {
+ NodeTypeParameter,
+ PortField,
+ PortSchema,
+ DataPickOption,
+ OutputPickHint,
+ InputPortDef,
+ GraphDefinedSchemaRef,
+ OutputPortDef,
+ NodeType,
+ NodeTypeCategory,
+ SystemVariable,
+ FormFieldType,
+ ConditionOperatorDef,
+ NodeTypesResponse,
+ Automation2GraphNode,
+ Automation2Connection,
+ Automation2Graph,
+ ExecuteGraphResponse,
+ WorkflowEntryPoint,
+ Automation2Workflow,
+ AutoVersion,
+ AutoRun,
+ AutoWorkflow,
+ AutoTask,
+ AutoStepLog,
+ UpstreamPathEntry,
+ ConditionMetaResponse,
+ ConditionMetaRequest,
+ GraphDataSources,
+ ExecuteGraphOptions,
+ Automation2Run,
+ CompletedRun,
+ Automation2Task,
+ AutoWorkflowTemplate,
+ UserConnection,
+ ConnectionService,
+ BrowseEntry,
+ WorkflowMetrics,
+ WorkflowFileEnvelope,
+ ImportWorkflowResponse,
+ ImportWorkflowOptions,
+ ExportWorkflowResult,
+ WorkspaceRun,
+ WorkspaceRunDetail,
+ ApiRequestFunction,
+} from './workflowAutomationApi';
-/** Delete by workflow ID only (Automations dashboard / orphan rows without featureInstanceId). */
-export async function deleteSystemWorkflow(
- request: ApiRequestFunction,
- workflowId: string,
-): Promise {
- await request({
- url: `/api/system/workflow-runs/workflows/${workflowId}`,
- method: 'delete',
- });
-}
+export type {
+ AutoWorkflowStatus,
+ AutoRunStatus,
+ AutoStepStatus,
+ AutoTaskStatus,
+ AutoTemplateScope,
+} from './workflowAutomationApi';
-export interface Automation2Run {
- id: string;
- workflowId: string;
- status: string;
- nodeOutputs?: Record;
- currentNodeId?: string;
-}
+// ============================================================================
+// RE-EXPORTS — Constants
+// ============================================================================
-export async function fetchWorkflowRuns(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}/runs`,
- method: 'get',
- });
- return data?.runs ?? [];
-}
+export {
+ WORKFLOW_FILE_SCHEMA_VERSION,
+ WORKFLOW_FILE_KIND,
+ WORKFLOW_FILE_EXTENSION,
+} from './workflowAutomationApi';
-export interface CompletedRun extends Automation2Run {
- workflowLabel?: string;
- sysModifiedAt?: number;
- sysCreatedAt?: number;
-}
+// ============================================================================
+// RE-EXPORTS — API Functions (canonical implementations in workflowAutomationApi)
+// ============================================================================
-export async function fetchCompletedRuns(
- request: ApiRequestFunction,
- instanceId: string,
- limit = 20
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/runs/completed`,
- method: 'get',
- params: { limit },
- });
- return data?.runs ?? [];
-}
+export {
+ fetchNodeTypes,
+ fetchConditionMeta,
+ postUpstreamPaths,
+ fetchGraphDataSources,
+ getUpstreamPathsSaved,
+ executeGraph,
+ fetchWorkflows,
+ fetchWorkflow,
+ createWorkflow,
+ updateWorkflow,
+ deleteWorkflow,
+ importWorkflowFromFile,
+ exportWorkflowToFile,
+ isWorkflowFileContent,
+ workflowFileNameFor,
+ fetchWorkflowRuns,
+ fetchCompletedRuns,
+ fetchTasks,
+ completeTask,
+ cancelPendingTaskStopRun,
+ fetchVersions,
+ createDraftVersion,
+ publishVersion,
+ unpublishVersion,
+ archiveVersion,
+ fetchTemplates,
+ createTemplateFromWorkflow,
+ copyTemplate,
+ shareTemplate,
+ fetchConnections,
+ fetchConnectionServices,
+ fetchBrowse,
+ fetchMetrics,
+ fetchWorkspaceRuns,
+ fetchRunDetail as fetchWorkspaceRunDetail,
+} from './workflowAutomationApi';
-// -------------------------------------------------------------------------
-// Tasks
-// -------------------------------------------------------------------------
+// ============================================================================
+// KEPT — ClickUp-specific functions (not in workflow-automation API)
+// ============================================================================
-export interface Automation2Task {
- id: string;
- runId: string;
- workflowId: string;
- nodeId: string;
- nodeType: string;
- config: Record;
- status: string;
- result?: Record;
- /** Workflow label (enriched by API) */
- workflowLabel?: string;
- /** Unix timestamp ms (from sysCreatedAt) */
- createdAt?: number;
- /** Optional due date - configurable in future */
- dueAt?: number;
-}
+import type { ApiRequestFunction } from './workflowAutomationApi';
-export async function fetchTasks(
- request: ApiRequestFunction,
- instanceId: string,
- params?: { workflowId?: string; status?: string }
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/tasks`,
- method: 'get',
- params,
- });
- return data?.tasks ?? [];
-}
-
-export async function completeTask(
- request: ApiRequestFunction,
- instanceId: string,
- taskId: string,
- result: Record
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/tasks/${taskId}/complete`,
- method: 'post',
- data: { result },
- });
-}
-
-/** Cancel a pending human task and stop its workflow run (Graphical Editor). */
-export async function cancelPendingTaskStopRun(
- request: ApiRequestFunction,
- instanceId: string,
- taskId: string
-): Promise<{ success: boolean; runId?: string | null; taskId: string }> {
- const data = await request({
- url: `/api/workflows/${instanceId}/tasks/${taskId}/cancel`,
- method: 'post',
- });
- return {
- success: Boolean(data?.success),
- runId: data?.runId,
- taskId: data?.taskId ?? taskId,
- };
-}
-
-// -------------------------------------------------------------------------
-// Versions (AutoVersion Lifecycle)
-// -------------------------------------------------------------------------
-
-export async function fetchVersions(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}/versions`,
- method: 'get',
- });
- return data?.versions ?? [];
-}
-
-export async function createDraftVersion(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/workflows/${workflowId}/versions/draft`,
- method: 'post',
- });
-}
-
-export async function publishVersion(
- request: ApiRequestFunction,
- instanceId: string,
- versionId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/versions/${versionId}/publish`,
- method: 'post',
- });
-}
-
-export async function unpublishVersion(
- request: ApiRequestFunction,
- instanceId: string,
- versionId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/versions/${versionId}/unpublish`,
- method: 'post',
- });
-}
-
-export async function archiveVersion(
- request: ApiRequestFunction,
- instanceId: string,
- versionId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/versions/${versionId}/archive`,
- method: 'post',
- });
-}
-
-// -------------------------------------------------------------------------
-// Templates
-// -------------------------------------------------------------------------
-
-export interface AutoWorkflowTemplate extends Automation2Workflow {
- isTemplate: boolean;
- templateScope?: AutoTemplateScope;
- templateSourceId?: string;
- sharedReadOnly?: boolean;
-}
-
-export async function fetchTemplates(
- request: ApiRequestFunction,
- instanceId: string,
- scope?: AutoTemplateScope,
- pagination?: any
-): Promise {
- const queryParams: Record = {};
- if (scope) queryParams.scope = scope;
- if (pagination) queryParams.pagination = JSON.stringify(pagination);
- const data = await request({
- url: `/api/workflows/${instanceId}/templates`,
- method: 'get',
- params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
- });
- if (data?.items && data?.pagination) return data;
- return data?.templates ?? [];
-}
-
-export async function createTemplateFromWorkflow(
- request: ApiRequestFunction,
- instanceId: string,
- workflowId: string,
- scope: AutoTemplateScope = 'user'
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/templates/from-workflow`,
- method: 'post',
- data: { workflowId, scope },
- });
-}
-
-export async function copyTemplate(
- request: ApiRequestFunction,
- instanceId: string,
- templateId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/templates/${templateId}/copy`,
- method: 'post',
- });
-}
-
-export async function shareTemplate(
- request: ApiRequestFunction,
- instanceId: string,
- templateId: string,
- scope: AutoTemplateScope
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/templates/${templateId}/share`,
- method: 'post',
- data: { scope },
- });
-}
-
-// -------------------------------------------------------------------------
-// Connections and Browse (for Email/SharePoint node config)
-// -------------------------------------------------------------------------
-
-export interface UserConnection {
- id: string;
- authority: string;
- externalUsername?: string;
- externalEmail?: string;
- status: string;
-}
-
-export async function fetchConnections(
- request: ApiRequestFunction,
- instanceId: string
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/connections`,
- method: 'get',
- });
- return data?.connections ?? [];
-}
-
-export interface ConnectionService {
- service: string;
- label: string;
- icon: string;
-}
-
-/** Encode connection id/reference for URL path segments (may contain spaces/colons). */
function _encodedConnectionId(connectionId: string): string {
return encodeURIComponent(connectionId);
}
-export async function fetchConnectionServices(
- request: ApiRequestFunction,
- instanceId: string,
- connectionId: string
-): Promise {
- const data = await request({
- url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/services`,
- method: 'get',
- });
- return data?.services ?? [];
-}
-
-export interface BrowseEntry {
- name: string;
- path: string;
- isFolder: boolean;
- size?: number;
- mimeType?: string;
- metadata?: Record;
-}
-
-export async function fetchBrowse(
- request: ApiRequestFunction,
- instanceId: string,
- connectionId: string,
- service: string,
- path = '/'
-): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
- const data = await request({
- url: `/api/workflows/${instanceId}/connections/${_encodedConnectionId(connectionId)}/browse`,
- method: 'get',
- params: { service, path },
- });
- return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service };
-}
-
/** ClickUp GET /task/{taskId} — list.id for resolving list-scoped fields when only task id is known. */
export async function fetchClickupTask(
request: ApiRequestFunction,
@@ -1064,32 +210,7 @@ export async function fetchClickupListTasks(
} & Record;
}
-// -------------------------------------------------------------------------
-// Monitoring / Metrics
-// -------------------------------------------------------------------------
-
-export interface WorkflowMetrics {
- workflowCount: number;
- activeWorkflows: number;
- totalRuns: number;
- runsByStatus: Record;
- totalTasks: number;
- tasksByStatus: Record;
- totalTokens: number;
- totalCredits: number;
-}
-
-export async function fetchMetrics(
- request: ApiRequestFunction,
- instanceId: string
-): Promise {
- return await request({
- url: `/api/workflows/${instanceId}/metrics`,
- method: 'get',
- });
-}
-
-/** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe“. */
+/** Paginated tasks in a list — for ClickUp relationship dropdowns and input.form „ClickUp-Aufgabe". */
export async function loadClickupListTasksForDropdown(
request: ApiRequestFunction,
connectionId: string,
@@ -1130,94 +251,17 @@ export async function loadClickupListTasksForDropdown(
return acc;
}
-
// ============================================================================
-// AUTOMATION WORKSPACE API (user-facing run workspace)
+// KEPT — deleteSystemWorkflow (uses /api/system/... not covered by new API)
// ============================================================================
-export interface WorkspaceRun {
- id: string;
- workflowId: string;
- workflowLabel?: string;
- status: string;
- startedAt?: number;
- completedAt?: number;
- ownerId?: string;
- mandateId?: string;
- mandateLabel?: string;
- targetFeatureInstanceId?: string;
- targetInstanceLabel?: string;
- costTokens?: number;
- costCredits?: number;
- error?: string;
-}
-
-export interface WorkspaceRunDetail {
- run: WorkspaceRun & { nodeOutputs?: Record };
- workflow: {
- id: string;
- label: string;
- targetFeatureInstanceId?: string;
- featureInstanceId?: string;
- tags?: string[];
- } | null;
- steps: Array<{
- id: string;
- runId: string;
- nodeId: string;
- nodeType: string;
- status: string;
- inputSnapshot?: Record;
- output?: Record;
- inputFiles?: Array<{ id: string; fileName?: string }>;
- outputFiles?: Array<{ id: string; fileName?: string }>;
- error?: string;
- startedAt?: number;
- completedAt?: number;
- durationMs?: number;
- tokensUsed?: number;
- retryCount?: number;
- }>;
- files: Array<{
- id: string;
- fileName?: string;
- contentType?: string;
- sizeBytes?: number;
- }>;
- unassignedFiles?: Array<{
- id: string;
- fileName?: string;
- }>;
-}
-
-export async function fetchWorkspaceRuns(
+/** Delete by workflow ID only (Automations dashboard / orphan rows without featureInstanceId). */
+export async function deleteSystemWorkflow(
request: ApiRequestFunction,
- params: {
- scope?: 'mine' | 'mandate';
- status?: string;
- targetInstanceId?: string;
- workflowId?: string;
- limit?: number;
- offset?: number;
- } = {},
-): Promise<{ runs: WorkspaceRun[]; total: number }> {
- const query = new URLSearchParams();
- if (params.scope) query.set('scope', params.scope);
- if (params.status) query.set('status', params.status);
- if (params.targetInstanceId) query.set('targetInstanceId', params.targetInstanceId);
- if (params.workflowId) query.set('workflowId', params.workflowId);
- if (params.limit) query.set('limit', String(params.limit));
- if (params.offset) query.set('offset', String(params.offset));
- const qs = query.toString();
- const url = `/api/automations/runs${qs ? `?${qs}` : ''}`;
- const resp = await request({ url, method: 'get' });
- return resp as { runs: WorkspaceRun[]; total: number };
-}
-
-export async function fetchWorkspaceRunDetail(
- request: ApiRequestFunction,
- runId: string,
-): Promise {
- const resp = await request({ url: `/api/automations/runs/${runId}/detail`, method: 'get' });
- return resp as WorkspaceRunDetail;
+ workflowId: string,
+): Promise {
+ await request({
+ url: `/api/workflow-automation/workflows/${workflowId}`,
+ method: 'delete',
+ });
}
diff --git a/src/api/workflowAutomationApi.ts b/src/api/workflowAutomationApi.ts
new file mode 100644
index 0000000..fbb1632
--- /dev/null
+++ b/src/api/workflowAutomationApi.ts
@@ -0,0 +1,1193 @@
+/**
+ * Workflow Automation API (mandate-scoped)
+ *
+ * Replaces instance-scoped /api/workflows/{instanceId}/...
+ * with the unified /api/workflow-automation/... base path.
+ *
+ * Also unifies the former /api/system/workflow-runs/... and
+ * /api/automations/runs/... endpoints under the same base.
+ */
+
+import type { ApiRequestOptions } from '../hooks/useApi';
+
+const LOG = '[WorkflowAutomation]';
+const BASE = '/api/workflow-automation';
+
+// ============================================================================
+// TYPES
+// ============================================================================
+
+export interface NodeTypeParameter {
+ name: string;
+ type: string;
+ required?: boolean;
+ description?: string;
+ default?: unknown;
+ frontendType?: string;
+ frontendOptions?: Record;
+ options?: unknown[];
+ validation?: Record;
+}
+
+export interface PortField {
+ name: string;
+ type: string;
+ /** Plain string or per-language map from the API catalog. */
+ description: string | Record;
+ required: boolean;
+ enumValues?: string[] | null;
+ /** When true, surface at the top of the DataPicker as the most common/recommended pick. */
+ recommended?: boolean;
+ /** Human label from portTypeCatalog (backend). Preferred over technical path in DataPicker. */
+ pickerLabel?: string | null;
+ /** Backend: segment for one list element (between List field and nested field). */
+ pickerItemLabel?: string | null;
+}
+
+export interface PortSchema {
+ name: string;
+ fields: PortField[];
+}
+
+/** One pickable binding — defined on ``outputPorts[n].dataPickOptions`` (authoritative list from gateway). */
+export interface DataPickOption {
+ path: (string | number)[];
+ pickerLabel: string;
+ detail?: string;
+ recommended?: boolean;
+ iterable?: boolean;
+ /** For display and optional strict compatibility (e.g. str, Any). */
+ type?: string;
+}
+
+/** @deprecated Prefer ``outputPorts[].dataPickOptions``; kept for older payloads. */
+export type OutputPickHint = DataPickOption;
+
+export interface InputPortDef {
+ accepts: string[];
+}
+
+/** Graph-defined output schema (e.g. form fields from node parameters). */
+export interface GraphDefinedSchemaRef {
+ kind: 'fromGraph';
+ parameter: string;
+}
+
+export interface OutputPortDef {
+ schema: string | GraphDefinedSchemaRef;
+ dynamic?: boolean;
+ deriveFrom?: string;
+ /**
+ * When set, DataPicker uses **only** this list for that port (no portTypeCatalog expansion).
+ * Authoritative, like `parameters` for node configuration.
+ */
+ dataPickOptions?: DataPickOption[];
+}
+
+export interface NodeType {
+ id: string;
+ category: string;
+ label: string;
+ description: string;
+ parameters: NodeTypeParameter[];
+ inputs: number;
+ outputs: number;
+ outputLabels?: string[];
+ executor: string;
+ inputPorts?: Record;
+ outputPorts?: Record;
+ meta?: {
+ icon?: string;
+ color?: string;
+ /** True if this node performs an LLM / AI call (credits). */
+ usesAi?: boolean;
+ method?: string;
+ action?: string;
+ };
+}
+export interface NodeTypeCategory {
+ id: string;
+ label: Record | string;
+}
+
+export interface SystemVariable {
+ type: string;
+ description: string;
+}
+
+/** Single form field type with its canonical port primitive. Delivered by GET /node-types. */
+export interface FormFieldType {
+ id: string;
+ label: string;
+ portType: string;
+}
+
+export interface ConditionOperatorDef {
+ id: string;
+ label: string;
+ labelKey?: string;
+ needsValue: boolean;
+ valueInput?: { kind: string; options?: string[] };
+}
+
+export interface NodeTypesResponse {
+ nodeTypes: NodeType[];
+ categories: NodeTypeCategory[];
+ portTypeCatalog?: Record;
+ conditionOperatorCatalog?: Record;
+ systemVariables?: Record;
+ formFieldTypes?: FormFieldType[];
+}
+
+export interface Automation2GraphNode {
+ id: string;
+ type: string;
+ parameters?: Record;
+ inputPorts?: Array<{ name: string; schema: string; accepts?: string[] }>;
+ outputPorts?: Array<{ name: string; schema: string | GraphDefinedSchemaRef }>;
+}
+
+export interface Automation2Connection {
+ source: string;
+ target: string;
+ sourceOutput?: number;
+ targetInput?: number;
+}
+
+export interface Automation2Graph {
+ nodes: Automation2GraphNode[];
+ connections: Automation2Connection[];
+}
+
+export interface ExecuteGraphResponse {
+ success: boolean;
+ nodeOutputs?: Record;
+ error?: string;
+ /** Soft, non-blocking message displayed alongside a successful response. */
+ warning?: string;
+ stopped?: boolean;
+ failedNode?: string;
+ paused?: boolean;
+ taskId?: string;
+ runId?: string;
+ nodeId?: string;
+}
+
+/** Entry point / start configured outside the canvas (manual, form, schedule, …) */
+export interface WorkflowEntryPoint {
+ id: string;
+ kind: string;
+ category: 'on_demand' | 'always_on';
+ enabled: boolean;
+ title: Record | string;
+ description?: Record;
+ config: Record;
+}
+
+export interface Automation2Workflow {
+ id: string;
+ label: string;
+ graph: Automation2Graph;
+ active?: boolean;
+ /** Target feature instance for execution data scope (NULL for templates) */
+ targetFeatureInstanceId?: string | null;
+ /** Entry points (Starts) — how this workflow may be invoked */
+ invocations?: WorkflowEntryPoint[];
+ /** Enriched: run count */
+ runCount?: number;
+ /** Enriched: has active (running/paused) run */
+ isRunning?: boolean;
+ /** Enriched: status of active run */
+ runStatus?: string;
+ /** Enriched: nodeId where workflow is stuck (paused) */
+ stuckAtNodeId?: string;
+ /** Enriched: human-readable label for stuck node */
+ stuckAtNodeLabel?: string;
+ /** From PowerOnModel base — record creation timestamp (seconds) */
+ sysCreatedAt?: number;
+ /** Enriched: last run started timestamp (seconds) */
+ lastStartedAt?: number;
+}
+
+// ============================================================================
+// AUTO-PREFIX TYPES (Greenfield)
+// ============================================================================
+
+export type AutoWorkflowStatus = 'draft' | 'published' | 'archived';
+export type AutoRunStatus = 'running' | 'paused' | 'completed' | 'failed' | 'cancelled';
+export type AutoStepStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
+export type AutoTaskStatus = 'pending' | 'completed' | 'cancelled' | 'expired';
+export type AutoTemplateScope = 'user' | 'instance' | 'mandate' | 'system';
+
+export interface AutoVersion {
+ id: string;
+ workflowId: string;
+ versionNumber: number;
+ status: AutoWorkflowStatus;
+ graph: Automation2Graph;
+ invocations?: WorkflowEntryPoint[];
+ publishedAt?: number;
+ publishedBy?: string;
+}
+
+export interface AutoRun {
+ id: string;
+ workflowId: string;
+ versionId?: string;
+ status: AutoRunStatus;
+ trigger?: Record;
+ startedAt?: number;
+ completedAt?: number;
+ nodeOutputs?: Record;
+ currentNodeId?: string;
+ resumeContext?: Record;
+ error?: string;
+ costTokens?: number;
+ costCredits?: number;
+}
+
+export interface AutoWorkflow {
+ id: string;
+ mandateId: string;
+ featureInstanceId: string;
+ label: string;
+ description?: string;
+ tags?: string[];
+ isTemplate: boolean;
+ templateSourceId?: string;
+ templateScope?: AutoTemplateScope;
+ sharedReadOnly?: boolean;
+ currentVersionId?: string;
+ active: boolean;
+ eventId?: string;
+ notifyOnFailure?: boolean;
+ graph: Automation2Graph;
+ invocations?: WorkflowEntryPoint[];
+ sysCreatedBy?: string;
+ sysCreatedAt?: number;
+ sysModifiedBy?: string;
+ sysModifiedAt?: number;
+}
+
+export interface AutoTask {
+ id: string;
+ runId: string;
+ workflowId: string;
+ nodeId: string;
+ nodeType: string;
+ config: Record;
+ assigneeId?: string;
+ status: AutoTaskStatus;
+ result?: Record;
+ expiresAt?: number;
+ sysCreatedAt?: number;
+}
+
+export interface AutoStepLog {
+ id: string;
+ runId: string;
+ nodeId: string;
+ nodeType: string;
+ status: AutoStepStatus;
+ inputSnapshot?: Record;
+ output?: Record;
+ error?: string;
+ startedAt?: number;
+ completedAt?: number;
+ durationMs?: number;
+ tokensUsed?: number;
+ retryCount?: number;
+}
+
+// ============================================================================
+// ADDITIONAL TYPES
+// ============================================================================
+
+export interface UpstreamPathEntry {
+ producerNodeId: string;
+ producerLabel?: string;
+ path: (string | number)[];
+ type: string;
+ label: string;
+ scopeOrigin: 'data' | 'loop' | 'system';
+ valueKind?: string;
+}
+
+export interface ConditionMetaResponse {
+ valueKind: string;
+ operators: ConditionOperatorDef[];
+}
+
+export interface ConditionMetaRequest {
+ graph: Automation2Graph;
+ nodeId?: string;
+ ref: { type: 'ref'; nodeId: string; path: (string | number)[] };
+}
+
+/** Scope-aware data sources for the DataPicker. */
+export interface GraphDataSources {
+ /** Ancestor node IDs that are valid sources. */
+ availableSourceIds: string[];
+ /** Maps nodeId → output port index to use instead of 0. */
+ portIndexOverrides: Record;
+ /** IDs of flow.loop nodes whose body the current node is inside. */
+ loopBodyContextIds: string[];
+}
+
+export interface ExecuteGraphOptions {
+ /** Use a configured start on the saved workflow */
+ entryPointId?: string;
+ /** Full run envelope (overrides entry point mapping) */
+ runEnvelope?: Record;
+ /** Merged into envelope.payload */
+ payload?: Record;
+}
+
+export interface Automation2Run {
+ id: string;
+ workflowId: string;
+ status: string;
+ nodeOutputs?: Record;
+ currentNodeId?: string;
+}
+
+export interface CompletedRun extends Automation2Run {
+ workflowLabel?: string;
+ sysModifiedAt?: number;
+ sysCreatedAt?: number;
+}
+
+export interface Automation2Task {
+ id: string;
+ runId: string;
+ workflowId: string;
+ nodeId: string;
+ nodeType: string;
+ config: Record;
+ status: string;
+ result?: Record;
+ /** Workflow label (enriched by API) */
+ workflowLabel?: string;
+ /** Unix timestamp ms (from sysCreatedAt) */
+ createdAt?: number;
+ /** Optional due date — configurable in future */
+ dueAt?: number;
+}
+
+export interface AutoWorkflowTemplate extends Automation2Workflow {
+ isTemplate: boolean;
+ templateScope?: AutoTemplateScope;
+ templateSourceId?: string;
+ sharedReadOnly?: boolean;
+}
+
+export interface UserConnection {
+ id: string;
+ authority: string;
+ externalUsername?: string;
+ externalEmail?: string;
+ status: string;
+}
+
+export interface ConnectionService {
+ service: string;
+ label: string;
+ icon: string;
+}
+
+export interface BrowseEntry {
+ name: string;
+ path: string;
+ isFolder: boolean;
+ size?: number;
+ mimeType?: string;
+ metadata?: Record;
+}
+
+export interface WorkflowMetrics {
+ workflowCount: number;
+ activeWorkflows: number;
+ totalRuns: number;
+ runsByStatus: Record;
+ totalTasks: number;
+ tasksByStatus: Record;
+ totalTokens: number;
+ totalCredits: number;
+}
+
+// -------------------------------------------------------------------------
+// Workflow file IO (envelopeVersioned, .workflow.json)
+// -------------------------------------------------------------------------
+
+export const WORKFLOW_FILE_SCHEMA_VERSION = '1.0';
+export const WORKFLOW_FILE_KIND = 'poweron.workflow';
+export const WORKFLOW_FILE_EXTENSION = '.workflow.json';
+
+export interface WorkflowFileEnvelope {
+ $schemaVersion: string;
+ $kind: string;
+ $exportedAt?: string;
+ $gatewayVersion?: string;
+ label: string;
+ description?: string;
+ tags?: string[];
+ templateScope?: AutoTemplateScope;
+ sharedReadOnly?: boolean;
+ notifyOnFailure?: boolean;
+ graph: Automation2Graph;
+ invocations?: WorkflowEntryPoint[];
+}
+
+export interface ImportWorkflowResponse {
+ workflow: AutoWorkflow;
+ warnings: string[];
+ created: boolean;
+}
+
+export interface ImportWorkflowOptions {
+ /** Inline envelope payload (preferred for round-trip in the editor). */
+ envelope?: WorkflowFileEnvelope;
+ /** UDB FileItem.id of a previously uploaded ``.workflow.json``. */
+ fileId?: string;
+ /** When set, the existing workflow is replaced instead of a new one being created. */
+ existingWorkflowId?: string;
+}
+
+export interface ExportWorkflowResult {
+ fileName: string;
+ envelope: WorkflowFileEnvelope;
+}
+
+// -------------------------------------------------------------------------
+// Workspace run types (user-facing run workspace)
+// -------------------------------------------------------------------------
+
+export interface WorkspaceRun {
+ id: string;
+ workflowId: string;
+ workflowLabel?: string;
+ status: string;
+ startedAt?: number;
+ completedAt?: number;
+ ownerId?: string;
+ mandateId?: string;
+ mandateLabel?: string;
+ targetFeatureInstanceId?: string;
+ targetInstanceLabel?: string;
+ costTokens?: number;
+ costCredits?: number;
+ error?: string;
+}
+
+export interface WorkspaceRunDetail {
+ run: WorkspaceRun & { nodeOutputs?: Record };
+ workflow: {
+ id: string;
+ label: string;
+ targetFeatureInstanceId?: string;
+ featureInstanceId?: string;
+ tags?: string[];
+ } | null;
+ steps: Array<{
+ id: string;
+ runId: string;
+ nodeId: string;
+ nodeType: string;
+ status: string;
+ inputSnapshot?: Record;
+ output?: Record;
+ inputFiles?: Array<{ id: string; fileName?: string }>;
+ outputFiles?: Array<{ id: string; fileName?: string }>;
+ error?: string;
+ startedAt?: number;
+ completedAt?: number;
+ durationMs?: number;
+ tokensUsed?: number;
+ retryCount?: number;
+ }>;
+ files: Array<{
+ id: string;
+ fileName?: string;
+ contentType?: string;
+ sizeBytes?: number;
+ }>;
+ unassignedFiles?: Array<{
+ id: string;
+ fileName?: string;
+ }>;
+}
+
+// ============================================================================
+// API FUNCTIONS
+// ============================================================================
+
+export type ApiRequestFunction = (options: ApiRequestOptions) => Promise;
+
+// -------------------------------------------------------------------------
+// Node types & graph helpers
+// -------------------------------------------------------------------------
+
+/**
+ * Fetch node types for the flow builder (backend-driven).
+ * GET /api/workflow-automation/node-types?language=de
+ */
+export async function fetchNodeTypes(
+ request: ApiRequestFunction,
+ language = 'de'
+): Promise {
+ console.log(`${LOG} fetchNodeTypes: language=${language}`);
+ const data = await request({
+ url: `${BASE}/node-types`,
+ method: 'get',
+ params: { language },
+ });
+ const nodeTypes = data?.nodeTypes ?? [];
+ const categories = data?.categories ?? [];
+ const portTypeCatalog = data?.portTypeCatalog ?? undefined;
+ const conditionOperatorCatalog = data?.conditionOperatorCatalog ?? undefined;
+ const systemVariables = data?.systemVariables ?? undefined;
+ const formFieldTypes = data?.formFieldTypes ?? undefined;
+ console.log(
+ `${LOG} fetchNodeTypes response: ${nodeTypes.length} nodeTypes, ${categories.length} categories, ` +
+ `${portTypeCatalog ? Object.keys(portTypeCatalog).length : 0} portTypes, ` +
+ `${conditionOperatorCatalog ? Object.keys(conditionOperatorCatalog).length : 0} conditionKinds, ` +
+ `${systemVariables ? Object.keys(systemVariables).length : 0} sysVars, ` +
+ `${formFieldTypes ? formFieldTypes.length : 0} formFieldTypes`
+ );
+ return { nodeTypes, categories, portTypeCatalog, conditionOperatorCatalog, systemVariables, formFieldTypes };
+}
+
+/**
+ * POST /api/workflow-automation/condition-meta — operators for a DataRef (If/Else).
+ */
+export async function fetchConditionMeta(
+ request: ApiRequestFunction,
+ body: ConditionMetaRequest,
+ language = 'de'
+): Promise {
+ const data = await request({
+ url: `${BASE}/condition-meta`,
+ method: 'post',
+ params: { language },
+ data: body,
+ });
+ return {
+ valueKind: String(data?.valueKind ?? 'unknown'),
+ operators: (data?.operators ?? []) as ConditionOperatorDef[],
+ };
+}
+
+/**
+ * POST /api/workflow-automation/upstream-paths — pickable upstream paths for DataPicker / AI.
+ */
+export async function postUpstreamPaths(
+ request: ApiRequestFunction,
+ graph: Automation2Graph,
+ nodeId: string
+): Promise<{ paths: UpstreamPathEntry[] }> {
+ const data = await request({
+ url: `${BASE}/upstream-paths`,
+ method: 'post',
+ data: { graph, nodeId },
+ });
+ return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
+}
+
+/**
+ * POST /api/workflow-automation/graph-data-sources
+ *
+ * Returns scope-aware source list so the DataPicker needs zero graph-traversal logic.
+ */
+export async function fetchGraphDataSources(
+ request: ApiRequestFunction,
+ nodeId: string,
+ nodes: Array<{ id: string; type?: string }>,
+ connections: Array<{ source: string; target: string; sourceOutput?: number; targetInput?: number }>,
+): Promise {
+ const data = await request({
+ url: `${BASE}/graph-data-sources`,
+ method: 'post',
+ data: { nodeId, graph: { nodes, connections } },
+ });
+ return {
+ availableSourceIds: data?.availableSourceIds ?? [],
+ portIndexOverrides: data?.portIndexOverrides ?? {},
+ loopBodyContextIds: data?.loopBodyContextIds ?? [],
+ };
+}
+
+/** GET saved workflow graph variant of upstream-paths (requires workflowId). */
+export async function getUpstreamPathsSaved(
+ request: ApiRequestFunction,
+ workflowId: string,
+ nodeId: string
+): Promise<{ paths: UpstreamPathEntry[] }> {
+ const data = await request({
+ url: `${BASE}/upstream-paths/${encodeURIComponent(nodeId)}`,
+ method: 'get',
+ params: { workflowId },
+ });
+ return { paths: (data?.paths ?? []) as UpstreamPathEntry[] };
+}
+
+// -------------------------------------------------------------------------
+// Options (dropdown data for node parameters)
+// -------------------------------------------------------------------------
+
+/** GET /api/workflow-automation/options/user.connection */
+export async function fetchUserConnectionOptions(
+ request: ApiRequestFunction,
+): Promise> {
+ const data = await request({
+ url: `${BASE}/options/user.connection`,
+ method: 'get',
+ });
+ return data?.options ?? data ?? [];
+}
+
+/** GET /api/workflow-automation/options/feature.instance */
+export async function fetchFeatureInstanceOptions(
+ request: ApiRequestFunction,
+): Promise> {
+ const data = await request({
+ url: `${BASE}/options/feature.instance`,
+ method: 'get',
+ });
+ return data?.options ?? data ?? [];
+}
+
+// -------------------------------------------------------------------------
+// Execute
+// -------------------------------------------------------------------------
+
+/**
+ * Execute an automation2 graph.
+ * POST /api/workflow-automation/workflows/{workflowId}/execute
+ */
+export async function executeGraph(
+ request: ApiRequestFunction,
+ graph: Automation2Graph,
+ workflowId?: string,
+ options?: ExecuteGraphOptions
+): Promise {
+ console.log(
+ `${LOG} executeGraph request: workflowId=${workflowId} nodes=${graph.nodes.length} connections=${graph.connections.length}`,
+ { nodes: graph.nodes, connections: graph.connections, options }
+ );
+ const start = performance.now();
+ try {
+ const body: Record = { graph, workflowId };
+ if (options?.entryPointId) body.entryPointId = options.entryPointId;
+ if (options?.runEnvelope) body.runEnvelope = options.runEnvelope;
+ if (options?.payload && Object.keys(options.payload).length > 0) body.payload = options.payload;
+ const url = workflowId
+ ? `${BASE}/workflows/${workflowId}/execute`
+ : `${BASE}/execute`;
+ const result = await request({ url, method: 'post', data: body });
+ const ms = Math.round(performance.now() - start);
+ console.log(
+ `${LOG} executeGraph response (${ms}ms): success=${result?.success} error=${result?.error ?? 'none'} nodeOutputs_keys=${Object.keys(result?.nodeOutputs ?? {}).join(',')} failedNode=${result?.failedNode ?? '-'}`,
+ result
+ );
+ return result;
+ } catch (err) {
+ const ms = Math.round(performance.now() - start);
+ console.error(`${LOG} executeGraph FAILED (${ms}ms):`, err);
+ throw err;
+ }
+}
+
+// -------------------------------------------------------------------------
+// Workflows CRUD
+// -------------------------------------------------------------------------
+
+export async function fetchWorkflows(
+ request: ApiRequestFunction,
+ params?: { active?: boolean; pagination?: any; mandateId?: string }
+): Promise {
+ const queryParams: Record = {};
+ if (params?.active !== undefined) queryParams.active = params.active;
+ if (params?.pagination) queryParams.pagination = JSON.stringify(params.pagination);
+ if (params?.mandateId) queryParams.mandateId = params.mandateId;
+ const data = await request({
+ url: `${BASE}/workflows`,
+ method: 'get',
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
+ });
+ if (data?.items && data?.pagination) return data;
+ return data?.workflows ?? [];
+}
+
+export async function fetchWorkflow(
+ request: ApiRequestFunction,
+ workflowId: string
+): Promise {
+ return await request({
+ url: `${BASE}/workflows/${workflowId}`,
+ method: 'get',
+ });
+}
+
+export async function createWorkflow(
+ request: ApiRequestFunction,
+ body: {
+ label: string;
+ graph: Automation2Graph;
+ invocations?: WorkflowEntryPoint[];
+ targetFeatureInstanceId?: string | null;
+ mandateId?: string;
+ }
+): Promise {
+ return await request({
+ url: `${BASE}/workflows`,
+ method: 'post',
+ data: body,
+ });
+}
+
+export async function updateWorkflow(
+ request: ApiRequestFunction,
+ workflowId: string,
+ body: {
+ label?: string;
+ graph?: Automation2Graph;
+ invocations?: WorkflowEntryPoint[];
+ active?: boolean;
+ notifyOnFailure?: boolean;
+ targetFeatureInstanceId?: string | null;
+ }
+): Promise {
+ return await request({
+ url: `${BASE}/workflows/${workflowId}`,
+ method: 'put',
+ data: body,
+ });
+}
+
+export async function deleteWorkflow(
+ request: ApiRequestFunction,
+ workflowId: string
+): Promise {
+ await request({
+ url: `${BASE}/workflows/${workflowId}`,
+ method: 'delete',
+ });
+}
+
+// -------------------------------------------------------------------------
+// Workflow file IO (envelopeVersioned, .workflow.json)
+// -------------------------------------------------------------------------
+
+/** POST /api/workflow-automation/workflows/import */
+export async function importWorkflowFromFile(
+ request: ApiRequestFunction,
+ options: ImportWorkflowOptions,
+): Promise {
+ if (!options.envelope && !options.fileId) {
+ throw new Error('importWorkflowFromFile: either envelope or fileId is required');
+ }
+ return await request({
+ url: `${BASE}/workflows/import`,
+ method: 'post',
+ data: options,
+ });
+}
+
+/**
+ * GET /api/workflow-automation/workflows/{workflowId}/export
+ *
+ * Returns ``{ fileName, envelope }`` when ``download=false`` and a raw JSON
+ * download (``Content-Disposition: attachment``) when ``download=true``.
+ */
+export async function exportWorkflowToFile(
+ request: ApiRequestFunction,
+ workflowId: string,
+ download = false,
+): Promise {
+ return await request({
+ url: `${BASE}/workflows/${workflowId}/export`,
+ method: 'get',
+ params: { download },
+ });
+}
+
+/** Quick content-sniffing — used by the UDB FilesTab to flag workflow files. */
+export function isWorkflowFileContent(payload: unknown): boolean {
+ if (!payload || typeof payload !== 'object') return false;
+ const p = payload as Record;
+ return (
+ typeof p.$schemaVersion === 'string' &&
+ p.$kind === WORKFLOW_FILE_KIND &&
+ !!p.graph &&
+ typeof p.graph === 'object'
+ );
+}
+
+/** Suggest a safe filename from the workflow label (mirrors gateway buildFileName). */
+export function workflowFileNameFor(label: string): string {
+ const slug = (label || 'workflow')
+ .toLowerCase()
+ .replace(/[^a-z0-9._-]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .slice(0, 80) || 'workflow';
+ return `${slug}${WORKFLOW_FILE_EXTENSION}`;
+}
+
+// -------------------------------------------------------------------------
+// Runs
+// -------------------------------------------------------------------------
+
+/**
+ * Fetch runs for a specific workflow.
+ * GET /api/workflow-automation/runs?workflowId={workflowId}
+ *
+ * Replaces both:
+ * /api/workflows/{instanceId}/workflows/{workflowId}/runs
+ * /api/system/workflow-runs (with workflowId filter)
+ */
+export async function fetchWorkflowRuns(
+ request: ApiRequestFunction,
+ workflowId: string
+): Promise {
+ const data = await request({
+ url: `${BASE}/runs`,
+ method: 'get',
+ params: { workflowId },
+ });
+ return data?.runs ?? [];
+}
+
+/**
+ * Fetch completed runs across all workflows.
+ * GET /api/workflow-automation/runs?status=completed
+ */
+export async function fetchCompletedRuns(
+ request: ApiRequestFunction,
+ limit = 20
+): Promise {
+ const data = await request({
+ url: `${BASE}/runs`,
+ method: 'get',
+ params: { status: 'completed', limit },
+ });
+ return data?.runs ?? [];
+}
+
+/**
+ * Fetch all runs (system-level / cross-workflow).
+ * GET /api/workflow-automation/runs
+ *
+ * Replaces /api/system/workflow-runs and /api/automations/runs.
+ */
+export async function fetchRuns(
+ request: ApiRequestFunction,
+ params?: {
+ status?: string;
+ workflowId?: string;
+ limit?: number;
+ offset?: number;
+ }
+): Promise<{ runs: Automation2Run[]; total?: number }> {
+ const data = await request({
+ url: `${BASE}/runs`,
+ method: 'get',
+ params,
+ });
+ return { runs: data?.runs ?? [], total: data?.total };
+}
+
+/** GET /api/workflow-automation/runs/{runId}/steps */
+export async function fetchRunSteps(
+ request: ApiRequestFunction,
+ runId: string
+): Promise {
+ const data = await request({
+ url: `${BASE}/runs/${runId}/steps`,
+ method: 'get',
+ });
+ return data?.steps ?? [];
+}
+
+/**
+ * Returns the SSE stream URL for a running workflow.
+ * GET /api/workflow-automation/runs/{runId}/stream
+ */
+export async function fetchRunStream(
+ request: ApiRequestFunction,
+ runId: string
+): Promise {
+ return await request({
+ url: `${BASE}/runs/${runId}/stream`,
+ method: 'get',
+ });
+}
+
+/** POST /api/workflow-automation/runs/{runId}/stop */
+export async function stopRun(
+ request: ApiRequestFunction,
+ runId: string
+): Promise<{ success: boolean }> {
+ const data = await request({
+ url: `${BASE}/runs/${runId}/stop`,
+ method: 'post',
+ });
+ return { success: Boolean(data?.success) };
+}
+
+/** GET /api/workflow-automation/runs/{runId}/detail */
+export async function fetchRunDetail(
+ request: ApiRequestFunction,
+ runId: string,
+): Promise {
+ const resp = await request({ url: `${BASE}/runs/${runId}/detail`, method: 'get' });
+ return resp as WorkspaceRunDetail;
+}
+
+// -------------------------------------------------------------------------
+// Workspace runs (user-facing, paginated)
+// -------------------------------------------------------------------------
+
+/**
+ * Paginated workspace runs (replaces /api/automations/runs).
+ * GET /api/workflow-automation/runs
+ */
+export async function fetchWorkspaceRuns(
+ request: ApiRequestFunction,
+ params: {
+ scope?: 'mine' | 'mandate';
+ status?: string;
+ targetInstanceId?: string;
+ workflowId?: string;
+ limit?: number;
+ offset?: number;
+ } = {},
+): Promise<{ runs: WorkspaceRun[]; total: number }> {
+ const query = new URLSearchParams();
+ if (params.scope) query.set('scope', params.scope);
+ if (params.status) query.set('status', params.status);
+ if (params.targetInstanceId) query.set('targetInstanceId', params.targetInstanceId);
+ if (params.workflowId) query.set('workflowId', params.workflowId);
+ if (params.limit) query.set('limit', String(params.limit));
+ if (params.offset) query.set('offset', String(params.offset));
+ const qs = query.toString();
+ const url = `${BASE}/runs${qs ? `?${qs}` : ''}`;
+ const resp = await request({ url, method: 'get' });
+ return resp as { runs: WorkspaceRun[]; total: number };
+}
+
+// -------------------------------------------------------------------------
+// Tasks
+// -------------------------------------------------------------------------
+
+export async function fetchTasks(
+ request: ApiRequestFunction,
+ params?: { workflowId?: string; status?: string }
+): Promise {
+ const data = await request({
+ url: `${BASE}/tasks`,
+ method: 'get',
+ params,
+ });
+ return data?.tasks ?? [];
+}
+
+export async function completeTask(
+ request: ApiRequestFunction,
+ taskId: string,
+ result: Record
+): Promise {
+ return await request({
+ url: `${BASE}/tasks/${taskId}/complete`,
+ method: 'post',
+ data: { result },
+ });
+}
+
+/** Cancel a pending human task and stop its workflow run. */
+export async function cancelPendingTaskStopRun(
+ request: ApiRequestFunction,
+ taskId: string
+): Promise<{ success: boolean; runId?: string | null; taskId: string }> {
+ const data = await request({
+ url: `${BASE}/tasks/${taskId}/cancel`,
+ method: 'post',
+ });
+ return {
+ success: Boolean(data?.success),
+ runId: data?.runId,
+ taskId: data?.taskId ?? taskId,
+ };
+}
+
+// -------------------------------------------------------------------------
+// Versions (AutoVersion Lifecycle)
+// -------------------------------------------------------------------------
+
+export async function fetchVersions(
+ request: ApiRequestFunction,
+ workflowId: string
+): Promise {
+ const data = await request({
+ url: `${BASE}/workflows/${workflowId}/versions`,
+ method: 'get',
+ });
+ return data?.versions ?? [];
+}
+
+export async function createDraftVersion(
+ request: ApiRequestFunction,
+ workflowId: string
+): Promise {
+ return await request({
+ url: `${BASE}/workflows/${workflowId}/versions/draft`,
+ method: 'post',
+ });
+}
+
+export async function publishVersion(
+ request: ApiRequestFunction,
+ versionId: string
+): Promise {
+ return await request({
+ url: `${BASE}/versions/${versionId}/publish`,
+ method: 'post',
+ });
+}
+
+export async function unpublishVersion(
+ request: ApiRequestFunction,
+ versionId: string
+): Promise {
+ return await request({
+ url: `${BASE}/versions/${versionId}/unpublish`,
+ method: 'post',
+ });
+}
+
+export async function archiveVersion(
+ request: ApiRequestFunction,
+ versionId: string
+): Promise {
+ return await request({
+ url: `${BASE}/versions/${versionId}/archive`,
+ method: 'post',
+ });
+}
+
+// -------------------------------------------------------------------------
+// Templates
+// -------------------------------------------------------------------------
+
+export async function fetchTemplates(
+ request: ApiRequestFunction,
+ scope?: AutoTemplateScope,
+ pagination?: any
+): Promise {
+ const queryParams: Record = {};
+ if (scope) queryParams.scope = scope;
+ if (pagination) queryParams.pagination = JSON.stringify(pagination);
+ const data = await request({
+ url: `${BASE}/templates`,
+ method: 'get',
+ params: Object.keys(queryParams).length > 0 ? queryParams : undefined,
+ });
+ if (data?.items && data?.pagination) return data;
+ return data?.templates ?? [];
+}
+
+export async function createTemplateFromWorkflow(
+ request: ApiRequestFunction,
+ workflowId: string,
+ scope: AutoTemplateScope = 'user'
+): Promise {
+ return await request({
+ url: `${BASE}/templates/from-workflow`,
+ method: 'post',
+ data: { workflowId, scope },
+ });
+}
+
+export async function copyTemplate(
+ request: ApiRequestFunction,
+ templateId: string
+): Promise {
+ return await request({
+ url: `${BASE}/templates/${templateId}/copy`,
+ method: 'post',
+ });
+}
+
+export async function shareTemplate(
+ request: ApiRequestFunction,
+ templateId: string,
+ scope: AutoTemplateScope
+): Promise {
+ return await request({
+ url: `${BASE}/templates/${templateId}/share`,
+ method: 'post',
+ data: { scope },
+ });
+}
+
+// -------------------------------------------------------------------------
+// Connections and Browse (for Email/SharePoint node config)
+// -------------------------------------------------------------------------
+
+/** Encode connection id/reference for URL path segments (may contain spaces/colons). */
+function _encodedConnectionId(connectionId: string): string {
+ return encodeURIComponent(connectionId);
+}
+
+export async function fetchConnections(
+ request: ApiRequestFunction,
+): Promise {
+ const data = await request({
+ url: `${BASE}/connections`,
+ method: 'get',
+ });
+ return data?.connections ?? [];
+}
+
+export async function fetchConnectionServices(
+ request: ApiRequestFunction,
+ connectionId: string
+): Promise {
+ const data = await request({
+ url: `${BASE}/connections/${_encodedConnectionId(connectionId)}/services`,
+ method: 'get',
+ });
+ return data?.services ?? [];
+}
+
+export async function fetchBrowse(
+ request: ApiRequestFunction,
+ connectionId: string,
+ service: string,
+ path = '/'
+): Promise<{ items: BrowseEntry[]; path: string; service: string }> {
+ const data = await request({
+ url: `${BASE}/connections/${_encodedConnectionId(connectionId)}/browse`,
+ method: 'get',
+ params: { service, path },
+ });
+ return { items: data?.items ?? [], path: data?.path ?? path, service: data?.service ?? service };
+}
+
+// -------------------------------------------------------------------------
+// Monitoring / Metrics
+// -------------------------------------------------------------------------
+
+/**
+ * GET /api/workflow-automation/metrics
+ *
+ * Replaces both /api/workflows/{instanceId}/metrics
+ * and /api/system/workflow-runs/metrics.
+ */
+export async function fetchMetrics(
+ request: ApiRequestFunction,
+): Promise {
+ return await request({
+ url: `${BASE}/metrics`,
+ method: 'get',
+ });
+}
diff --git a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx
index b9923ce..1a3900e 100644
--- a/src/components/FlowEditor/editor/Automation2FlowEditor.tsx
+++ b/src/components/FlowEditor/editor/Automation2FlowEditor.tsx
@@ -23,6 +23,7 @@ import {
createTemplateFromWorkflow,
copyTemplate,
importWorkflowFromFile,
+ WORKFLOW_FILE_EXTENSION,
type NodeType,
type NodeTypeCategory,
type Automation2Graph,
@@ -153,7 +154,7 @@ export const Automation2FlowEditor: React.FC = ({ in
instanceId,
mandateId: mandateId || '',
featureInstanceId: instanceId,
- surface: 'graphEditor',
+ surface: 'workflowAutomation',
}), [instanceId, mandateId]);
const [versions, setVersions] = useState([]);
const [currentVersionId, setCurrentVersionId] = useState(null);
@@ -354,7 +355,7 @@ export const Automation2FlowEditor: React.FC = ({ in
setExecuteResult(null);
try {
const ep = currentWorkflowId ? invocations[0]?.id : undefined;
- const result = await executeGraph(request, instanceId, graph, currentWorkflowId ?? undefined, {
+ const result = await executeGraph(request, graph, currentWorkflowId ?? undefined, {
...(ep ? { entryPointId: ep } : {}),
});
setExecuteResult(result);
@@ -403,7 +404,7 @@ export const Automation2FlowEditor: React.FC = ({ in
setSaving(true);
try {
if (currentWorkflowId) {
- const updated = await updateWorkflow(request, instanceId, currentWorkflowId, {
+ const updated = await updateWorkflow(request, currentWorkflowId, {
graph,
invocations,
targetFeatureInstanceId,
@@ -420,11 +421,12 @@ export const Automation2FlowEditor: React.FC = ({ in
setSaving(false);
return;
}
- const created = await createWorkflow(request, instanceId, {
+ const created = await createWorkflow(request, {
label: label.trim() || t('Neuer Workflow'),
graph,
invocations,
targetFeatureInstanceId,
+ mandateId,
});
setCurrentWorkflowId(created.id);
setInvocations(created.invocations ?? []);
@@ -436,12 +438,12 @@ export const Automation2FlowEditor: React.FC = ({ in
} finally {
setSaving(false);
}
- }, [request, instanceId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t, nodeErrors, targetFeatureInstanceId, hasCanvasStartNode]);
+ }, [request, mandateId, canvasNodes, canvasConnections, currentWorkflowId, promptInput, invocations, t, nodeErrors, targetFeatureInstanceId, hasCanvasStartNode]);
const handleLoad = useCallback(
async (workflowId: string) => {
try {
- const wf = await fetchWorkflow(request, instanceId, workflowId);
+ const wf = await fetchWorkflow(request, workflowId);
if (wf.graph) {
handleFromApiGraph(wf.graph, wf.invocations);
} else {
@@ -463,7 +465,7 @@ export const Automation2FlowEditor: React.FC = ({ in
setExecuteResult(null);
applyGraphWithSync({ nodes: [], connections: [] }, []);
try {
- const result = await fetchWorkflows(request, instanceId);
+ const result = await fetchWorkflows(request);
setWorkflows(Array.isArray(result) ? result : result.items);
} catch (refreshErr) {
console.error(`${LOG} workflows refresh failed`, refreshErr);
@@ -476,7 +478,7 @@ export const Automation2FlowEditor: React.FC = ({ in
});
}
},
- [request, instanceId, handleFromApiGraph, applyGraphWithSync, t]
+ [request, handleFromApiGraph, applyGraphWithSync, t]
);
const handleWorkflowSelect = useCallback(
@@ -544,11 +546,10 @@ export const Automation2FlowEditor: React.FC = ({ in
);
const loadNodeTypes = useCallback(async () => {
- if (!instanceId) return;
setLoading(true);
setError(null);
try {
- const data = await fetchNodeTypes(request, instanceId, language);
+ const data = await fetchNodeTypes(request, language);
setNodeTypes(data.nodeTypes);
setCategories(data.categories);
if (data.portTypeCatalog) {
@@ -565,17 +566,16 @@ export const Automation2FlowEditor: React.FC = ({ in
} finally {
setLoading(false);
}
- }, [instanceId, language, request]);
+ }, [language, request]);
const loadWorkflows = useCallback(async () => {
- if (!instanceId) return;
try {
- const result = await fetchWorkflows(request, instanceId);
+ const result = await fetchWorkflows(request);
setWorkflows(Array.isArray(result) ? result : result.items);
} catch (e) {
console.error(`${LOG} loadWorkflows failed`, e);
}
- }, [instanceId, request]);
+ }, [request]);
useEffect(() => {
loadNodeTypes();
@@ -665,17 +665,17 @@ export const Automation2FlowEditor: React.FC = ({ in
);
const loadVersions = useCallback(async () => {
- if (!instanceId || !currentWorkflowId) {
+ if (!currentWorkflowId) {
setVersions([]);
return;
}
try {
- const v = await fetchVersions(request, instanceId, currentWorkflowId);
+ const v = await fetchVersions(request, currentWorkflowId);
setVersions(v);
} catch (e) {
console.error(`${LOG} loadVersions failed`, e);
}
- }, [instanceId, currentWorkflowId, request]);
+ }, [currentWorkflowId, request]);
useEffect(() => {
loadVersions();
@@ -696,10 +696,9 @@ export const Automation2FlowEditor: React.FC = ({ in
const handlePublishVersion = useCallback(
async (versionId: string) => {
- if (!instanceId) return;
setVersionLoading(true);
try {
- await publishVersion(request, instanceId, versionId);
+ await publishVersion(request, versionId);
await loadVersions();
} catch (e: unknown) {
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
@@ -707,15 +706,14 @@ export const Automation2FlowEditor: React.FC = ({ in
setVersionLoading(false);
}
},
- [request, instanceId, loadVersions]
+ [request, loadVersions]
);
const handleUnpublishVersion = useCallback(
async (versionId: string) => {
- if (!instanceId) return;
setVersionLoading(true);
try {
- await unpublishVersion(request, instanceId, versionId);
+ await unpublishVersion(request, versionId);
await loadVersions();
} catch (e: unknown) {
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
@@ -723,15 +721,14 @@ export const Automation2FlowEditor: React.FC = ({ in
setVersionLoading(false);
}
},
- [request, instanceId, loadVersions]
+ [request, loadVersions]
);
const handleArchiveVersion = useCallback(
async (versionId: string) => {
- if (!instanceId) return;
setVersionLoading(true);
try {
- await archiveVersion(request, instanceId, versionId);
+ await archiveVersion(request, versionId);
await loadVersions();
} catch (e: unknown) {
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
@@ -739,14 +736,14 @@ export const Automation2FlowEditor: React.FC = ({ in
setVersionLoading(false);
}
},
- [request, instanceId, loadVersions]
+ [request, loadVersions]
);
const handleCreateDraft = useCallback(async () => {
- if (!instanceId || !currentWorkflowId) return;
+ if (!currentWorkflowId) return;
setVersionLoading(true);
try {
- const draft = await createDraftVersion(request, instanceId, currentWorkflowId);
+ const draft = await createDraftVersion(request, currentWorkflowId);
await loadVersions();
setCurrentVersionId(draft.id);
} catch (e: unknown) {
@@ -754,16 +751,16 @@ export const Automation2FlowEditor: React.FC = ({ in
} finally {
setVersionLoading(false);
}
- }, [request, instanceId, currentWorkflowId, loadVersions]);
+ }, [request, currentWorkflowId, loadVersions]);
// Template: save current workflow as template
const [templateSaving, setTemplateSaving] = useState(false);
const handleSaveAsTemplate = useCallback(
async (scope: AutoTemplateScope) => {
- if (!instanceId || !currentWorkflowId) return;
+ if (!currentWorkflowId) return;
setTemplateSaving(true);
try {
- await createTemplateFromWorkflow(request, instanceId, currentWorkflowId, scope);
+ await createTemplateFromWorkflow(request, currentWorkflowId, scope);
setExecuteResult({ success: true, error: undefined } as unknown as ExecuteGraphResponse);
} catch (e: unknown) {
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
@@ -771,16 +768,15 @@ export const Automation2FlowEditor: React.FC = ({ in
setTemplateSaving(false);
}
},
- [request, instanceId, currentWorkflowId]
+ [request, currentWorkflowId]
);
// Template: new workflow from template
const [templatePickerOpen, setTemplatePickerOpen] = useState(false);
const handleNewFromTemplate = useCallback(
async (templateId: string) => {
- if (!instanceId) return;
try {
- const wf = await copyTemplate(request, instanceId, templateId);
+ const wf = await copyTemplate(request, templateId);
setWorkflows((prev) => [...prev, wf]);
setCurrentWorkflowId(wf.id);
if (wf.graph) handleFromApiGraph(wf.graph, wf.invocations);
@@ -789,7 +785,7 @@ export const Automation2FlowEditor: React.FC = ({ in
setExecuteResult({ success: false, error: e instanceof Error ? e.message : String(e) });
}
},
- [request, instanceId, handleFromApiGraph]
+ [request, handleFromApiGraph]
);
@@ -947,12 +943,20 @@ export const Automation2FlowEditor: React.FC = ({ in
activeTab={udbTab as UdbTab}
onTabChange={(tab) => setUdbTab(tab as LeftTab)}
hideTabs={['chats']}
- onFileSelect={onFileSelect}
- onSourcesChanged={onSourcesChanged}
- onWorkflowImportedFromFile={async (workflowId) => {
- await loadWorkflows();
- handleWorkflowSelect(workflowId);
+ onFileSelect={async (fileId, fileName) => {
+ if (fileName?.toLowerCase().endsWith(WORKFLOW_FILE_EXTENSION)) {
+ try {
+ const result = await importWorkflowFromFile(request, { fileId });
+ await loadWorkflows();
+ if (result?.workflow?.id) handleWorkflowSelect(result.workflow.id);
+ } catch (e) {
+ console.error('[workflowAutomation] workflow file import failed', e);
+ }
+ return;
+ }
+ onFileSelect?.(fileId, fileName);
}}
+ onSourcesChanged={onSourcesChanged}
/>
)}
@@ -1024,12 +1028,12 @@ export const Automation2FlowEditor: React.FC = ({ in
stickyNotes={canvasStickyNotes}
onStickyNotesChange={setCanvasStickyNotes}
onExternalDrop={async (mime, payload) => {
- if (mime !== 'application/json+workflow' || !instanceId) return false;
+ if (mime !== 'application/json+workflow') return false;
const p = payload as { files?: Array<{ id: string }> } | undefined;
const fileId = p?.files?.[0]?.id;
if (!fileId) return false;
try {
- const result = await importWorkflowFromFile(request, instanceId, { fileId });
+ const result = await importWorkflowFromFile(request, { fileId });
await loadWorkflows();
if (result?.workflow?.id) handleWorkflowSelect(result.workflow.id);
return true;
diff --git a/src/components/FlowEditor/editor/EditorChatPanel.tsx b/src/components/FlowEditor/editor/EditorChatPanel.tsx
index 3ce248e..0eb8a25 100644
--- a/src/components/FlowEditor/editor/EditorChatPanel.tsx
+++ b/src/components/FlowEditor/editor/EditorChatPanel.tsx
@@ -1,7 +1,7 @@
/**
* EditorChatPanel
*
- * AI Chat sidebar for the GraphicalEditor.
+ * AI Chat sidebar for the WorkflowAutomation editor.
* Streams responses via SSE (same pattern as Workspace chat).
* File & data-source attachment UX mirrors WorkspaceInput:
* - Files: drag & drop from FilesTab (UDB) onto input area, or click in UDB
@@ -87,7 +87,7 @@ export const EditorChatPanel: React.FC = ({ instanceId,
// Load persisted chat history from the backend whenever the workflow changes.
// The chat is stored in `ChatWorkflow.linkedWorkflowId == workflowId` and is
- // returned by `GET /api/workflows/{instanceId}/{workflowId}/chat/messages`.
+ // returned by `GET /api/workflow-automation/{workflowId}/chat/messages`.
// For an unsaved workflow (workflowId == null) we just clear the panel.
useEffect(() => {
if (!workflowId) {
@@ -99,7 +99,7 @@ export const EditorChatPanel: React.FC = ({ instanceId,
setHistoryLoading(true);
try {
const res = await api.get(
- `/api/workflows/${instanceId}/${workflowId}/chat/messages`,
+ `/api/workflow-automation/${workflowId}/chat/messages`,
);
if (cancelled) return;
const persisted = (res.data?.messages || []).map((m): ChatMessage => ({
@@ -166,7 +166,7 @@ export const EditorChatPanel: React.FC = ({ instanceId,
const baseURL = api.defaults.baseURL || '';
const cleanup = startSseStream({
- url: `${baseURL}/api/workflows/${instanceId}/${workflowId}/chat/stream`,
+ url: `${baseURL}/api/workflow-automation/${workflowId}/chat/stream`,
body,
handlers: {
onChunk: (event) => {
@@ -227,7 +227,7 @@ export const EditorChatPanel: React.FC = ({ instanceId,
: m));
}
try {
- await api.post(`/api/workflows/${instanceId}/${workflowId}/chat/stop`);
+ await api.post(`/api/workflow-automation/${workflowId}/chat/stop`);
} catch {
}
abortRef.current?.();
diff --git a/src/components/FlowEditor/editor/EditorWorkflowChatList.tsx b/src/components/FlowEditor/editor/EditorWorkflowChatList.tsx
index 7973c98..f050d79 100644
--- a/src/components/FlowEditor/editor/EditorWorkflowChatList.tsx
+++ b/src/components/FlowEditor/editor/EditorWorkflowChatList.tsx
@@ -1,11 +1,11 @@
/**
* EditorWorkflowChatList
*
- * UDB "Chats" tab content for the GraphicalEditor: each AutoWorkflow is treated
- * as one editor chat session. Lists workflows already loaded by the parent
- * editor (no extra fetch), supports search and "+ Neu" to start a fresh
+ * UDB "Chats" tab content for the WorkflowAutomation editor: each AutoWorkflow
+ * is treated as one editor chat session. Lists workflows already loaded by the
+ * parent editor (no extra fetch), supports search and "+ Neu" to start a fresh
* workflow chat. Mirrors the spirit of the Workspace ChatsTab but uses
- * GraphicalEditor data instead of the workspace endpoint.
+ * WorkflowAutomation data instead of the workspace endpoint.
*/
import React, { useMemo, useState } from 'react';
import type { Automation2Workflow } from '../../../api/workflowApi';
diff --git a/src/components/FlowEditor/editor/RunTracingPanel.tsx b/src/components/FlowEditor/editor/RunTracingPanel.tsx
index 238ae63..c3eecfa 100644
--- a/src/components/FlowEditor/editor/RunTracingPanel.tsx
+++ b/src/components/FlowEditor/editor/RunTracingPanel.tsx
@@ -98,7 +98,7 @@ export const RunTracingPanel: React.FC = ({
setLoading(true);
try {
const data = await request({
- url: `/api/workflows/${instanceId}/runs/${runId}/steps`,
+ url: `/api/workflow-automation/runs/${runId}/steps`,
method: 'get',
});
setSteps(data?.steps || []);
@@ -115,7 +115,7 @@ export const RunTracingPanel: React.FC = ({
loadSteps();
const baseUrl = api.defaults.baseURL || '';
- const url = `${baseUrl}/api/workflows/${instanceId}/runs/${runId}/stream`;
+ const url = `${baseUrl}/api/workflow-automation/runs/${runId}/stream`;
const es = new EventSource(url, { withCredentials: true });
eventSourceRef.current = es;
diff --git a/src/components/FlowEditor/editor/TemplatePicker.tsx b/src/components/FlowEditor/editor/TemplatePicker.tsx
index 6cf16ef..d25af24 100644
--- a/src/components/FlowEditor/editor/TemplatePicker.tsx
+++ b/src/components/FlowEditor/editor/TemplatePicker.tsx
@@ -50,7 +50,7 @@ export const TemplatePicker: React.FC = ({
setLoading(true);
try {
const scope = activeScope === 'all' ? undefined : activeScope;
- const result = await fetchTemplates(request, instanceId, scope);
+ const result = await fetchTemplates(request, scope);
setTemplates(Array.isArray(result) ? result : result.items);
} catch {
setTemplates([]);
diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/CaseListEditor.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/CaseListEditor.tsx
index a6722ba..267ff88 100644
--- a/src/components/FlowEditor/nodes/frontendTypeRenderers/CaseListEditor.tsx
+++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/CaseListEditor.tsx
@@ -157,7 +157,7 @@ export const CaseListEditor: React.FC = ({
if (dataFlow?.instanceId && dataFlow.request) {
setLoading(true);
- fetchConditionMeta(dataFlow.request, dataFlow.instanceId, {
+ fetchConditionMeta(dataFlow.request, {
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
nodeId: dataFlow.currentNodeId,
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/ConditionEditor.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/ConditionEditor.tsx
index ff23dfc..b50983d 100644
--- a/src/components/FlowEditor/nodes/frontendTypeRenderers/ConditionEditor.tsx
+++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/ConditionEditor.tsx
@@ -83,7 +83,7 @@ export const ConditionEditor: React.FC = ({
if (dataFlow?.instanceId && dataFlow.request) {
setLoading(true);
- fetchConditionMeta(dataFlow.request, dataFlow.instanceId, {
+ fetchConditionMeta(dataFlow.request, {
graph: toApiGraph(dataFlow.nodes, dataFlow.connections),
nodeId: dataFlow.currentNodeId,
ref: { type: 'ref', nodeId: ref.nodeId, path: ref.path },
diff --git a/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx b/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx
index 925e310..572ac3d 100644
--- a/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx
+++ b/src/components/FlowEditor/nodes/frontendTypeRenderers/FeatureInstancePicker.tsx
@@ -3,7 +3,7 @@
*
* Modeled on ConnectionPicker. Loads mandate-scoped FeatureInstances filtered
* by `frontendOptions.featureCode` (e.g. "trustee", "redmine") via
- * GET /api/workflows/{instanceId}/options/feature.instance?featureCode=
+ * GET /api/workflow-automation/options/feature.instance?featureCode=
*
* Behavior matches the rest of the editor:
* - 0 results -> hint to create a feature instance for this mandate
@@ -42,7 +42,7 @@ export const FeatureInstancePicker: React.FC