Protokoll #5265

ID5265
Zeitstempel2025-12-22 19:03:24.329947
Clientroot
IP145.224.96.190
Modellclaude-sonnet-4-20250514
Statuscompleted
Tokens13,273 (Input: 2,158, Output: 11,115)
Dauer100 ms
Request-Zeit2025-12-22 19:03:24.329947
Response-Zeit2025-12-22 19:03:24.430189

Request

{
    "event": "PreToolUse",
    "tool_name": "Write",
    "tool_input": {
        "file_path": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Controller\/ContentPipelineController.php",
        "content": "<?php\n\nnamespace Controller;\n\nuse Application\\PipelineStepService;\nuse Framework\\Controller;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Config\\PipelineStepConfig;\nuse Infrastructure\\Persistence\\PipelineRepository;\n\nclass ContentPipelineController extends Controller\n{\n    private PipelineRepository $repository;\n    private PipelineStepService $stepService;\n\n    public function __construct()\n    {\n        $this->repository = new PipelineRepository();\n        $this->stepService = new PipelineStepService($this->repository);\n    }\n\n    \/**\n     * GET \/content-pipeline\n     *\/\n    public function index(): void\n    {\n        $this->view('content-pipeline.index', [\n            'title' => 'Content Pipeline',\n            'pipelines' => $this->repository->findAll(),\n            'stats' => $this->repository->getStatistics(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/import\n     *\/\n    public function import(): void\n    {\n        $pipeline = $this->repository->findDefault();\n\n        if ($pipeline === null) {\n            $pipelines = $this->repository->findAll(1);\n            $pipeline = $pipelines[0] ?? null;\n        }\n\n        $this->view('content-pipeline.import', [\n            'title' => 'Import Pipeline',\n            'pipeline' => $pipeline,\n            'latestRun' => $pipeline !== null\n                ? $this->repository->findLatestRun((int) $pipeline['id'])\n                : null,\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/new\n     *\/\n    public function pipelineNew(): void\n    {\n        $this->view('content-pipeline.form', [\n            'title' => 'Neue Pipeline',\n            'pipeline' => null,\n            'stepTypes' => PipelineStepConfig::getStepTypes(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\n     *\/\n    public function show(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->view('content-pipeline.show', [\n            'title' => 'Pipeline: ' . $pipeline['name'],\n            'pipeline' => $pipeline,\n            'runs' => $this->repository->findRuns((int) $id, 10),\n            'stepTypes' => PipelineStepConfig::getStepTypes(),\n            'models' => ModelConfig::getAll(),\n            'defaultModel' => ModelConfig::DEFAULT_MODEL,\n            'collections' => PipelineStepConfig::getCollections(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\/edit\n     *\/\n    public function edit(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->view('content-pipeline.form', [\n            'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],\n            'pipeline' => $pipeline,\n            'stepTypes' => PipelineStepConfig::getStepTypes(),\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\n     *\/\n    public function store(): void\n    {\n        $this->requireCsrf();\n\n        $name = trim($_POST['name'] ?? '');\n\n        if ($name === '') {\n            $_SESSION['error'] = 'Name ist erforderlich.';\n            $this->redirect('\/content-pipeline\/new');\n        }\n\n        $pipelineId = $this->repository->create([\n            'name' => $name,\n            'description' => trim($_POST['description'] ?? ''),\n            'source_path' => trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents'),\n            'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),\n            'is_default' => isset($_POST['is_default']) ? 1 : 0,\n        ]);\n\n        $this->stepService->createDefaultSteps($pipelineId);\n\n        $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';\n        $this->redirect('\/content-pipeline\/' . $pipelineId);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\n     *\/\n    public function update(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $name = trim($_POST['name'] ?? '');\n\n        if ($name === '') {\n            $_SESSION['error'] = 'Name ist erforderlich.';\n            $this->redirect('\/content-pipeline\/' . $id . '\/edit');\n        }\n\n        $this->repository->update((int) $id, [\n            'name' => $name,\n            'description' => trim($_POST['description'] ?? ''),\n            'source_path' => trim($_POST['source_path'] ?? ''),\n            'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),\n            'is_default' => isset($_POST['is_default']) ? 1 : 0,\n        ]);\n\n        $_SESSION['success'] = 'Pipeline aktualisiert.';\n        $this->redirect('\/content-pipeline\/' . $id);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/run\n     *\/\n    public function run(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $runId = $this->repository->createRun((int) $id);\n\n        \/\/ Pipeline im Hintergrund starten\n        $cmd = sprintf(\n            'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',\n            escapeshellarg('\/opt\/scripts\/pipeline\/venv\/bin\/python'),\n            escapeshellarg('\/opt\/scripts\/pipeline\/pipeline.py'),\n            (int) $id,\n            $runId,\n            escapeshellarg('\/tmp\/pipeline_run_' . $runId . '.log')\n        );\n\n        exec($cmd);\n\n        $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';\n        $this->redirect('\/content-pipeline\/' . $id);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\/status (AJAX)\n     *\/\n    public function status(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n            return;\n        }\n\n        $this->json([\n            'pipeline_id' => (int) $id,\n            'run' => $this->repository->findLatestRun((int) $id),\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/toggle\n     *\/\n    public function toggleStep(string $id, string $stepId): void\n    {\n        $this->requireCsrf();\n\n        if (!$this->stepService->toggleStep((int) $id, (int) $stepId)) {\n            $this->notFound('Pipeline oder Schritt nicht gefunden');\n        }\n\n        $this->redirect('\/content-pipeline\/' . $id);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)\n     *\/\n    public function updateStepModel(string $id, string $stepId): void\n    {\n        $result = $this->stepService->updateModel(\n            (int) $id,\n            (int) $stepId,\n            $_POST['model'] ?? ''\n        );\n\n        if (!$result['success']) {\n            $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);\n\n            return;\n        }\n\n        $this->json($result);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)\n     *\/\n    public function updateStepCollection(string $id, string $stepId): void\n    {\n        $result = $this->stepService->updateCollection(\n            (int) $id,\n            (int) $stepId,\n            $_POST['collection'] ?? ''\n        );\n\n        if (!$result['success']) {\n            $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);\n\n            return;\n        }\n\n        $this->json($result);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/delete\n     *\/\n    public function delete(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->repository->delete((int) $id);\n\n        $_SESSION['success'] = 'Pipeline geloescht.';\n        $this->redirect('\/content-pipeline');\n    }\n}\n"
    }
}

Response

{
    "tool_response": {
        "type": "update",
        "filePath": "\/var\/www\/dev.campus.systemische-tools.de\/src\/Controller\/ContentPipelineController.php",
        "content": "<?php\n\nnamespace Controller;\n\nuse Application\\PipelineStepService;\nuse Framework\\Controller;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Config\\PipelineStepConfig;\nuse Infrastructure\\Persistence\\PipelineRepository;\n\nclass ContentPipelineController extends Controller\n{\n    private PipelineRepository $repository;\n    private PipelineStepService $stepService;\n\n    public function __construct()\n    {\n        $this->repository = new PipelineRepository();\n        $this->stepService = new PipelineStepService($this->repository);\n    }\n\n    \/**\n     * GET \/content-pipeline\n     *\/\n    public function index(): void\n    {\n        $this->view('content-pipeline.index', [\n            'title' => 'Content Pipeline',\n            'pipelines' => $this->repository->findAll(),\n            'stats' => $this->repository->getStatistics(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/import\n     *\/\n    public function import(): void\n    {\n        $pipeline = $this->repository->findDefault();\n\n        if ($pipeline === null) {\n            $pipelines = $this->repository->findAll(1);\n            $pipeline = $pipelines[0] ?? null;\n        }\n\n        $this->view('content-pipeline.import', [\n            'title' => 'Import Pipeline',\n            'pipeline' => $pipeline,\n            'latestRun' => $pipeline !== null\n                ? $this->repository->findLatestRun((int) $pipeline['id'])\n                : null,\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/new\n     *\/\n    public function pipelineNew(): void\n    {\n        $this->view('content-pipeline.form', [\n            'title' => 'Neue Pipeline',\n            'pipeline' => null,\n            'stepTypes' => PipelineStepConfig::getStepTypes(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\n     *\/\n    public function show(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->view('content-pipeline.show', [\n            'title' => 'Pipeline: ' . $pipeline['name'],\n            'pipeline' => $pipeline,\n            'runs' => $this->repository->findRuns((int) $id, 10),\n            'stepTypes' => PipelineStepConfig::getStepTypes(),\n            'models' => ModelConfig::getAll(),\n            'defaultModel' => ModelConfig::DEFAULT_MODEL,\n            'collections' => PipelineStepConfig::getCollections(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\/edit\n     *\/\n    public function edit(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->view('content-pipeline.form', [\n            'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],\n            'pipeline' => $pipeline,\n            'stepTypes' => PipelineStepConfig::getStepTypes(),\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\n     *\/\n    public function store(): void\n    {\n        $this->requireCsrf();\n\n        $name = trim($_POST['name'] ?? '');\n\n        if ($name === '') {\n            $_SESSION['error'] = 'Name ist erforderlich.';\n            $this->redirect('\/content-pipeline\/new');\n        }\n\n        $pipelineId = $this->repository->create([\n            'name' => $name,\n            'description' => trim($_POST['description'] ?? ''),\n            'source_path' => trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents'),\n            'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),\n            'is_default' => isset($_POST['is_default']) ? 1 : 0,\n        ]);\n\n        $this->stepService->createDefaultSteps($pipelineId);\n\n        $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';\n        $this->redirect('\/content-pipeline\/' . $pipelineId);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\n     *\/\n    public function update(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $name = trim($_POST['name'] ?? '');\n\n        if ($name === '') {\n            $_SESSION['error'] = 'Name ist erforderlich.';\n            $this->redirect('\/content-pipeline\/' . $id . '\/edit');\n        }\n\n        $this->repository->update((int) $id, [\n            'name' => $name,\n            'description' => trim($_POST['description'] ?? ''),\n            'source_path' => trim($_POST['source_path'] ?? ''),\n            'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),\n            'is_default' => isset($_POST['is_default']) ? 1 : 0,\n        ]);\n\n        $_SESSION['success'] = 'Pipeline aktualisiert.';\n        $this->redirect('\/content-pipeline\/' . $id);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/run\n     *\/\n    public function run(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $runId = $this->repository->createRun((int) $id);\n\n        \/\/ Pipeline im Hintergrund starten\n        $cmd = sprintf(\n            'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',\n            escapeshellarg('\/opt\/scripts\/pipeline\/venv\/bin\/python'),\n            escapeshellarg('\/opt\/scripts\/pipeline\/pipeline.py'),\n            (int) $id,\n            $runId,\n            escapeshellarg('\/tmp\/pipeline_run_' . $runId . '.log')\n        );\n\n        exec($cmd);\n\n        $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';\n        $this->redirect('\/content-pipeline\/' . $id);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\/status (AJAX)\n     *\/\n    public function status(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n            return;\n        }\n\n        $this->json([\n            'pipeline_id' => (int) $id,\n            'run' => $this->repository->findLatestRun((int) $id),\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/toggle\n     *\/\n    public function toggleStep(string $id, string $stepId): void\n    {\n        $this->requireCsrf();\n\n        if (!$this->stepService->toggleStep((int) $id, (int) $stepId)) {\n            $this->notFound('Pipeline oder Schritt nicht gefunden');\n        }\n\n        $this->redirect('\/content-pipeline\/' . $id);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)\n     *\/\n    public function updateStepModel(string $id, string $stepId): void\n    {\n        $result = $this->stepService->updateModel(\n            (int) $id,\n            (int) $stepId,\n            $_POST['model'] ?? ''\n        );\n\n        if (!$result['success']) {\n            $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);\n\n            return;\n        }\n\n        $this->json($result);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)\n     *\/\n    public function updateStepCollection(string $id, string $stepId): void\n    {\n        $result = $this->stepService->updateCollection(\n            (int) $id,\n            (int) $stepId,\n            $_POST['collection'] ?? ''\n        );\n\n        if (!$result['success']) {\n            $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);\n\n            return;\n        }\n\n        $this->json($result);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/delete\n     *\/\n    public function delete(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->repository->delete((int) $id);\n\n        $_SESSION['success'] = 'Pipeline geloescht.';\n        $this->redirect('\/content-pipeline');\n    }\n}\n",
        "structuredPatch": [
            {
                "oldStart": 2,
                "oldLines": 17,
                "newStart": 2,
                "newLines": 21,
                "lines": [
                    " ",
                    " namespace Controller;",
                    " ",
                    "+use Application\\PipelineStepService;",
                    " use Framework\\Controller;",
                    " use Infrastructure\\AI\\ModelConfig;",
                    "+use Infrastructure\\Config\\PipelineStepConfig;",
                    " use Infrastructure\\Persistence\\PipelineRepository;",
                    " ",
                    " class ContentPipelineController extends Controller",
                    " {",
                    "     private PipelineRepository $repository;",
                    "+    private PipelineStepService $stepService;",
                    " ",
                    "     public function __construct()",
                    "     {",
                    "         $this->repository = new PipelineRepository();",
                    "+        $this->stepService = new PipelineStepService($this->repository);",
                    "     }",
                    " ",
                    "     \/**"
                ]
            },
            {
                "oldStart": 20,
                "oldLines": 13,
                "newStart": 24,
                "newLines": 10,
                "lines": [
                    "      *\/",
                    "     public function index(): void",
                    "     {",
                    "-        $pipelines = $this->repository->findAll();",
                    "-        $stats = $this->repository->getStatistics();",
                    "-",
                    "         $this->view('content-pipeline.index', [",
                    "             'title' => 'Content Pipeline',",
                    "-            'pipelines' => $pipelines,",
                    "-            'stats' => $stats,",
                    "+            'pipelines' => $this->repository->findAll(),",
                    "+            'stats' => $this->repository->getStatistics(),",
                    "         ]);",
                    "     }",
                    " "
                ]
            },
            {
                "oldStart": 42,
                "oldLines": 14,
                "newStart": 43,
                "newLines": 12,
                "lines": [
                    "             $pipeline = $pipelines[0] ?? null;",
                    "         }",
                    " ",
                    "-        $latestRun = $pipeline !== null",
                    "-            ? $this->repository->findLatestRun((int) $pipeline['id'])",
                    "-            : null;",
                    "-",
                    "         $this->view('content-pipeline.import', [",
                    "             'title' => 'Import Pipeline',",
                    "             'pipeline' => $pipeline,",
                    "-            'latestRun' => $latestRun,",
                    "+            'latestRun' => $pipeline !== null",
                    "+                ? $this->repository->findLatestRun((int) $pipeline['id'])",
                    "+                : null,",
                    "         ]);",
                    "     }",
                    " "
                ]
            },
            {
                "oldStart": 61,
                "oldLines": 7,
                "newStart": 60,
                "newLines": 7,
                "lines": [
                    "         $this->view('content-pipeline.form', [",
                    "             'title' => 'Neue Pipeline',",
                    "             'pipeline' => null,",
                    "-            'stepTypes' => $this->getStepTypes(),",
                    "+            'stepTypes' => PipelineStepConfig::getStepTypes(),",
                    "         ]);",
                    "     }",
                    " "
                ]
            },
            {
                "oldStart": 76,
                "oldLines": 16,
                "newStart": 75,
                "newLines": 14,
                "lines": [
                    "             $this->notFound('Pipeline nicht gefunden');",
                    "         }",
                    " ",
                    "-        $runs = $this->repository->findRuns((int) $id, 10);",
                    "-",
                    "         $this->view('content-pipeline.show', [",
                    "             'title' => 'Pipeline: ' . $pipeline['name'],",
                    "             'pipeline' => $pipeline,",
                    "-            'runs' => $runs,",
                    "-            'stepTypes' => $this->getStepTypes(),",
                    "+            'runs' => $this->repository->findRuns((int) $id, 10),",
                    "+            'stepTypes' => PipelineStepConfig::getStepTypes(),",
                    "             'models' => ModelConfig::getAll(),",
                    "             'defaultModel' => ModelConfig::DEFAULT_MODEL,",
                    "-            'collections' => $this->getAvailableCollections(),",
                    "+            'collections' => PipelineStepConfig::getCollections(),",
                    "         ]);",
                    "     }",
                    " "
                ]
            },
            {
                "oldStart": 103,
                "oldLines": 7,
                "newStart": 100,
                "newLines": 7,
                "lines": [
                    "         $this->view('content-pipeline.form', [",
                    "             'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],",
                    "             'pipeline' => $pipeline,",
                    "-            'stepTypes' => $this->getStepTypes(),",
                    "+            'stepTypes' => PipelineStepConfig::getStepTypes(),",
                    "         ]);",
                    "     }",
                    " "
                ]
            },
            {
                "oldStart": 115,
                "oldLines": 31,
                "newStart": 112,
                "newLines": 24,
                "lines": [
                    "         $this->requireCsrf();",
                    " ",
                    "         $name = trim($_POST['name'] ?? '');",
                    "-        $description = trim($_POST['description'] ?? '');",
                    "-        $sourcePath = trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents');",
                    "-        $extensions = $this->parseExtensions($_POST['extensions'] ?? '');",
                    "-        $isDefault = isset($_POST['is_default']) ? 1 : 0;",
                    " ",
                    "         if ($name === '') {",
                    "             $_SESSION['error'] = 'Name ist erforderlich.';",
                    "-            header('Location: \/content-pipeline\/new');",
                    "-            exit;",
                    "+            $this->redirect('\/content-pipeline\/new');",
                    "         }",
                    " ",
                    "         $pipelineId = $this->repository->create([",
                    "             'name' => $name,",
                    "-            'description' => $description,",
                    "-            'source_path' => $sourcePath,",
                    "-            'extensions' => $extensions,",
                    "-            'is_default' => $isDefault,",
                    "+            'description' => trim($_POST['description'] ?? ''),",
                    "+            'source_path' => trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents'),",
                    "+            'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),",
                    "+            'is_default' => isset($_POST['is_default']) ? 1 : 0,",
                    "         ]);",
                    " ",
                    "-        \/\/ Standard-Steps hinzufuegen",
                    "-        $this->createDefaultSteps($pipelineId);",
                    "+        $this->stepService->createDefaultSteps($pipelineId);",
                    " ",
                    "         $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';",
                    "-        header('Location: \/content-pipeline\/' . $pipelineId);",
                    "-        exit;",
                    "+        $this->redirect('\/content-pipeline\/' . $pipelineId);",
                    "     }",
                    " ",
                    "     \/**"
                ]
            },
            {
                "oldStart": 156,
                "oldLines": 28,
                "newStart": 146,
                "newLines": 22,
                "lines": [
                    "         }",
                    " ",
                    "         $name = trim($_POST['name'] ?? '');",
                    "-        $description = trim($_POST['description'] ?? '');",
                    "-        $sourcePath = trim($_POST['source_path'] ?? '');",
                    "-        $extensions = $this->parseExtensions($_POST['extensions'] ?? '');",
                    "-        $isDefault = isset($_POST['is_default']) ? 1 : 0;",
                    " ",
                    "         if ($name === '') {",
                    "             $_SESSION['error'] = 'Name ist erforderlich.';",
                    "-            header('Location: \/content-pipeline\/' . $id . '\/edit');",
                    "-            exit;",
                    "+            $this->redirect('\/content-pipeline\/' . $id . '\/edit');",
                    "         }",
                    " ",
                    "         $this->repository->update((int) $id, [",
                    "             'name' => $name,",
                    "-            'description' => $description,",
                    "-            'source_path' => $sourcePath,",
                    "-            'extensions' => $extensions,",
                    "-            'is_default' => $isDefault,",
                    "+            'description' => trim($_POST['description'] ?? ''),",
                    "+            'source_path' => trim($_POST['source_path'] ?? ''),",
                    "+            'extensions' => PipelineStepConfig::parseExtensions($_POST['extensions'] ?? ''),",
                    "+            'is_default' => isset($_POST['is_default']) ? 1 : 0,",
                    "         ]);",
                    " ",
                    "         $_SESSION['success'] = 'Pipeline aktualisiert.';",
                    "-        header('Location: \/content-pipeline\/' . $id);",
                    "-        exit;",
                    "+        $this->redirect('\/content-pipeline\/' . $id);",
                    "     }",
                    " ",
                    "     \/**"
                ]
            },
            {
                "oldStart": 193,
                "oldLines": 33,
                "newStart": 177,
                "newLines": 26,
                "lines": [
                    "             $this->notFound('Pipeline nicht gefunden');",
                    "         }",
                    " ",
                    "-        \/\/ Neuen Run erstellen",
                    "         $runId = $this->repository->createRun((int) $id);",
                    " ",
                    "         \/\/ Pipeline im Hintergrund starten",
                    "-        $pipelineScript = '\/opt\/scripts\/pipeline\/pipeline.py';",
                    "-        $venvPython = '\/opt\/scripts\/pipeline\/venv\/bin\/python';",
                    "-        $logFile = '\/tmp\/pipeline_run_' . $runId . '.log';",
                    "-",
                    "         $cmd = sprintf(",
                    "             'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',",
                    "-            escapeshellarg($venvPython),",
                    "-            escapeshellarg($pipelineScript),",
                    "+            escapeshellarg('\/opt\/scripts\/pipeline\/venv\/bin\/python'),",
                    "+            escapeshellarg('\/opt\/scripts\/pipeline\/pipeline.py'),",
                    "             (int) $id,",
                    "             $runId,",
                    "-            escapeshellarg($logFile)",
                    "+            escapeshellarg('\/tmp\/pipeline_run_' . $runId . '.log')",
                    "         );",
                    " ",
                    "         exec($cmd);",
                    " ",
                    "         $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';",
                    "-        header('Location: \/content-pipeline\/' . $id);",
                    "-        exit;",
                    "+        $this->redirect('\/content-pipeline\/' . $id);",
                    "     }",
                    " ",
                    "     \/**",
                    "-     * GET \/content-pipeline\/{id}\/status",
                    "-     * AJAX endpoint for run status",
                    "+     * GET \/content-pipeline\/{id}\/status (AJAX)",
                    "      *\/",
                    "     public function status(string $id): void",
                    "     {"
                ]
            },
            {
                "oldStart": 231,
                "oldLines": 11,
                "newStart": 208,
                "newLines": 9,
                "lines": [
                    "             return;",
                    "         }",
                    " ",
                    "-        $latestRun = $this->repository->findLatestRun((int) $id);",
                    "-",
                    "         $this->json([",
                    "             'pipeline_id' => (int) $id,",
                    "-            'run' => $latestRun,",
                    "+            'run' => $this->repository->findLatestRun((int) $id),",
                    "         ]);",
                    "     }",
                    " "
                ]
            },
            {
                "oldStart": 246,
                "oldLines": 82,
                "newStart": 221,
                "newLines": 51,
                "lines": [
                    "     {",
                    "         $this->requireCsrf();",
                    " ",
                    "-        $pipeline = $this->repository->findById((int) $id);",
                    "-",
                    "-        if ($pipeline === null) {",
                    "-            $this->notFound('Pipeline nicht gefunden');",
                    "+        if (!$this->stepService->toggleStep((int) $id, (int) $stepId)) {",
                    "+            $this->notFound('Pipeline oder Schritt nicht gefunden');",
                    "         }",
                    " ",
                    "-        \/\/ Find step and toggle",
                    "-        foreach ($pipeline['steps'] as $step) {",
                    "-            if ((int) $step['id'] === (int) $stepId) {",
                    "-                $this->repository->updateStep((int) $stepId, [",
                    "-                    'enabled' => $step['enabled'] ? 0 : 1,",
                    "-                ]);",
                    "-                break;",
                    "-            }",
                    "-        }",
                    "-",
                    "-        header('Location: \/content-pipeline\/' . $id);",
                    "-        exit;",
                    "+        $this->redirect('\/content-pipeline\/' . $id);",
                    "     }",
                    " ",
                    "     \/**",
                    "      * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)",
                    "-     * Update step model configuration",
                    "      *\/",
                    "     public function updateStepModel(string $id, string $stepId): void",
                    "     {",
                    "-        $pipeline = $this->repository->findById((int) $id);",
                    "+        $result = $this->stepService->updateModel(",
                    "+            (int) $id,",
                    "+            (int) $stepId,",
                    "+            $_POST['model'] ?? ''",
                    "+        );",
                    " ",
                    "-        if ($pipeline === null) {",
                    "-            $this->json(['error' => 'Pipeline nicht gefunden'], 404);",
                    "+        if (!$result['success']) {",
                    "+            $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);",
                    " ",
                    "             return;",
                    "         }",
                    " ",
                    "-        $model = trim($_POST['model'] ?? '');",
                    "+        $this->json($result);",
                    "+    }",
                    " ",
                    "-        if ($model === '' || !ModelConfig::isValid($model)) {",
                    "-            $this->json(['error' => 'Ungültiges Modell'], 400);",
                    "+    \/**",
                    "+     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)",
                    "+     *\/",
                    "+    public function updateStepCollection(string $id, string $stepId): void",
                    "+    {",
                    "+        $result = $this->stepService->updateCollection(",
                    "+            (int) $id,",
                    "+            (int) $stepId,",
                    "+            $_POST['collection'] ?? ''",
                    "+        );",
                    " ",
                    "-            return;",
                    "-        }",
                    "+        if (!$result['success']) {",
                    "+            $this->json(['error' => $result['error']], $result['error'] === 'Schritt nicht gefunden' ? 404 : 400);",
                    " ",
                    "-        \/\/ Find step",
                    "-        $stepFound = false;",
                    "-        foreach ($pipeline['steps'] as $step) {",
                    "-            if ((int) $step['id'] === (int) $stepId) {",
                    "-                $stepFound = true;",
                    "-                $config = $step['config'] ?? [];",
                    "-",
                    "-                \/\/ Determine provider from model",
                    "-                $provider = ModelConfig::isLocal($model) ? 'ollama' : 'anthropic';",
                    "-",
                    "-                \/\/ Update config with new model",
                    "-                $config['model'] = ModelConfig::isLocal($model)",
                    "-                    ? substr($model, 7)  \/\/ Remove 'ollama:' prefix",
                    "-                    : $model;",
                    "-                $config['provider'] = $provider;",
                    "-",
                    "-                $this->repository->updateStep((int) $stepId, [",
                    "-                    'config' => $config,",
                    "-                ]);",
                    "-                break;",
                    "-            }",
                    "-        }",
                    "-",
                    "-        if (!$stepFound) {",
                    "-            $this->json(['error' => 'Schritt nicht gefunden'], 404);",
                    "-",
                    "             return;",
                    "         }",
                    " ",
                    "-        $this->json([",
                    "-            'success' => true,",
                    "-            'model' => $model,",
                    "-            'label' => ModelConfig::getLabel($model),",
                    "-        ]);",
                    "+        $this->json($result);",
                    "     }",
                    " ",
                    "     \/**"
                ]
            },
            {
                "oldStart": 340,
                "oldLines": 292,
                "newStart": 284,
                "newLines": 6,
                "lines": [
                    "         $this->repository->delete((int) $id);",
                    " ",
                    "         $_SESSION['success'] = 'Pipeline geloescht.';",
                    "-        header('Location: \/content-pipeline');",
                    "-        exit;",
                    "+        $this->redirect('\/content-pipeline');",
                    "     }",
                    "-",
                    "-    \/**",
                    "-     * @return array<string, array<string, mixed>>",
                    "-     *\/",
                    "-    private function getStepTypes(): array",
                    "-    {",
                    "-        return [",
                    "-            \/\/ Phase 1: Vorverarbeitung",
                    "-            'detect' => [",
                    "-                'label' => 'Erkennung',",
                    "-                'description' => 'Dateien scannen und Format prüfen',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => null,",
                    "-            ],",
                    "-            'validate' => [",
                    "-                'label' => 'Validierung',",
                    "-                'description' => 'Datei-Prüfung auf Lesbarkeit und Korruption',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => null,",
                    "-            ],",
                    "-            'page_split' => [",
                    "-                'label' => 'Seitenzerlegung',",
                    "-                'description' => 'PDF in Einzelseiten zerlegen für Referenz und Vision-Analyse',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => 'ki_content.document_pages',",
                    "-            ],",
                    "-            'vision_analyze' => [",
                    "-                'label' => 'Bildanalyse',",
                    "-                'description' => 'Seiten via Vision-Modell analysieren, Bilder und Grafiken erkennen',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => 'ki_content.document_pages (vision_analysis)',",
                    "-                'uses_vision' => true,",
                    "-            ],",
                    "-            'extract' => [",
                    "-                'label' => 'Textextraktion',",
                    "-                'description' => 'Text extrahieren, OCR für Bilder mit Text',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => null,",
                    "-            ],",
                    "-            'structure' => [",
                    "-                'label' => 'Strukturerkennung',",
                    "-                'description' => 'Überschriften, Listen und Hierarchie erkennen',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => 'ki_content.document_sections',",
                    "-            ],",
                    "-            'segment' => [",
                    "-                'label' => 'Abschnitte',",
                    "-                'description' => 'Logische Dokumentgliederung nach Struktur',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => 'ki_content.document_sections',",
                    "-            ],",
                    "-            'chunk' => [",
                    "-                'label' => 'Textbausteine',",
                    "-                'description' => 'Chunks erstellen (max 800 Token) mit Seitenreferenz',",
                    "-                'phase' => 'Vorverarbeitung',",
                    "-                'storage' => 'ki_content.chunks',",
                    "-            ],",
                    "-            \/\/ Phase 2: Speicherung & Vektorisierung",
                    "-            'metadata_store' => [",
                    "-                'label' => 'DB-Speicherung',",
                    "-                'description' => 'Dokument, Seiten und Chunks in MariaDB speichern',",
                    "-                'phase' => 'Speicherung',",
                    "-                'storage' => 'ki_content.documents, .document_pages, .chunks',",
                    "-            ],",
                    "-            'embed' => [",
                    "-                'label' => 'Vektorisierung',",
                    "-                'description' => 'Embeddings erstellen für Vektor-Suche',",
                    "-                'phase' => 'Speicherung',",
                    "-                'storage' => 'Qdrant: {collection}',",
                    "-                'fixed_model' => 'mxbai-embed-large (1024-dim)',",
                    "-                'has_collection' => true,",
                    "-            ],",
                    "-            'collection_setup' => [",
                    "-                'label' => 'Collection',",
                    "-                'description' => 'Qdrant-Collection einrichten falls nötig',",
                    "-                'phase' => 'Speicherung',",
                    "-                'storage' => 'Qdrant: {collection}',",
                    "-            ],",
                    "-            'vector_store' => [",
                    "-                'label' => 'Vektorspeicherung',",
                    "-                'description' => 'Vektoren in Qdrant mit MariaDB-ID als Referenz',",
                    "-                'phase' => 'Speicherung',",
                    "-                'storage' => 'Qdrant: {collection}',",
                    "-            ],",
                    "-            'index_optimize' => [",
                    "-                'label' => 'Index-Optimierung',",
                    "-                'description' => 'HNSW-Index für schnelle Suche optimieren',",
                    "-                'phase' => 'Speicherung',",
                    "-                'storage' => 'Qdrant: {collection}',",
                    "-            ],",
                    "-            \/\/ Phase 3: Wissensextraktion (3 Ebenen)",
                    "-            'knowledge_page' => [",
                    "-                'label' => 'Seiten-Wissen',",
                    "-                'description' => 'Pro Seite: Entitäten → Semantik → Ontologie → Taxonomie',",
                    "-                'phase' => 'Wissen',",
                    "-                'storage' => 'ki_content.page_knowledge, .entities, .entity_semantics',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'knowledge_section' => [",
                    "-                'label' => 'Abschnitt-Wissen',",
                    "-                'description' => 'Pro Kapitel: Aggregierte Wissensrepräsentation',",
                    "-                'phase' => 'Wissen',",
                    "-                'storage' => 'ki_content.section_knowledge',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'knowledge_document' => [",
                    "-                'label' => 'Dokument-Wissen',",
                    "-                'description' => 'Konsolidierte Gesamtsicht des Dokuments',",
                    "-                'phase' => 'Wissen',",
                    "-                'storage' => 'ki_content.document_knowledge',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'knowledge_validate' => [",
                    "-                'label' => 'Wissens-Validierung',",
                    "-                'description' => 'Abgleich mit DB, Duplikate zusammenführen, neue validieren',",
                    "-                'phase' => 'Wissen',",
                    "-                'storage' => 'ki_content.entities (merged)',",
                    "-            ],",
                    "-            \/\/ Legacy Analyse-Schritte",
                    "-            'entity_extract' => [",
                    "-                'label' => 'Entitäten (Legacy)',",
                    "-                'description' => 'Personen, Organisationen, Konzepte, Methoden erkennen',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.chunk_entities',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'relation_extract' => [",
                    "-                'label' => 'Beziehungen (Legacy)',",
                    "-                'description' => 'Relationen zwischen Entitäten extrahieren',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.entity_relations',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'taxonomy_build' => [",
                    "-                'label' => 'Taxonomie (Legacy)',",
                    "-                'description' => 'Hierarchische Kategorisierung aufbauen',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.chunk_taxonomy, .taxonomy_terms',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'semantic_analyze' => [",
                    "-                'label' => 'Semantik (Legacy)',",
                    "-                'description' => 'Bedeutungs-Analyse, Konzepte und Definitionen',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.chunk_semantics',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'summarize' => [",
                    "-                'label' => 'Zusammenfassung',",
                    "-                'description' => 'Dokument- und Seiten-Zusammenfassungen erstellen',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.documents (summary), .document_pages',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'question_generate' => [",
                    "-                'label' => 'Fragengenerierung',",
                    "-                'description' => 'Beispielfragen für RAG-Chat erstellen',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.generated_questions',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-            'finalize' => [",
                    "-                'label' => 'Abschluss',",
                    "-                'description' => 'Status finalisieren und Job beenden',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.documents (status)',",
                    "-            ],",
                    "-            \/\/ Legacy",
                    "-            'analyze' => [",
                    "-                'label' => 'Analyse (Legacy)',",
                    "-                'description' => 'Kombinierte Analyse (veraltet)',",
                    "-                'phase' => 'Analyse',",
                    "-                'storage' => 'ki_content.chunk_entities, .chunk_semantics',",
                    "-                'uses_llm' => true,",
                    "-            ],",
                    "-        ];",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * @param string $input",
                    "-     * @return array<string>",
                    "-     *\/",
                    "-    private function parseExtensions(string $input): array",
                    "-    {",
                    "-        $extensions = [];",
                    "-        $parts = preg_split('\/[\\s,;]+\/', $input);",
                    "-",
                    "-        if ($parts === false) {",
                    "-            return ['.pdf', '.docx', '.pptx', '.md', '.txt'];",
                    "-        }",
                    "-",
                    "-        foreach ($parts as $ext) {",
                    "-            $ext = trim($ext);",
                    "-            if ($ext === '') {",
                    "-                continue;",
                    "-            }",
                    "-            if ($ext[0] !== '.') {",
                    "-                $ext = '.' . $ext;",
                    "-            }",
                    "-            $extensions[] = strtolower($ext);",
                    "-        }",
                    "-",
                    "-        return $extensions !== [] ? $extensions : ['.pdf', '.docx', '.pptx', '.md', '.txt'];",
                    "-    }",
                    "-",
                    "-    private function createDefaultSteps(int $pipelineId): void",
                    "-    {",
                    "-        $defaultSteps = [",
                    "-            ['step_type' => 'detect', 'config' => ['hash_algorithm' => 'sha256'], 'sort_order' => 1, 'enabled' => 1],",
                    "-            ['step_type' => 'extract', 'config' => ['ocr_enabled' => true, 'ocr_language' => 'deu'], 'sort_order' => 2, 'enabled' => 1],",
                    "-            ['step_type' => 'chunk', 'config' => ['min_size' => 100, 'max_size' => 2000, 'overlap' => 0.1], 'sort_order' => 3, 'enabled' => 1],",
                    "-            ['step_type' => 'embed', 'config' => ['model' => 'mxbai-embed-large', 'collection' => 'documents', 'dimensions' => 1024], 'sort_order' => 4, 'enabled' => 1],",
                    "-            ['step_type' => 'analyze', 'config' => ['extract_entities' => true, 'extract_relations' => true, 'classify_taxonomy' => true], 'sort_order' => 5, 'enabled' => 0],",
                    "-        ];",
                    "-",
                    "-        foreach ($defaultSteps as $step) {",
                    "-            $this->repository->addStep($pipelineId, $step);",
                    "-        }",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * Get available Qdrant collections.",
                    "-     *",
                    "-     * @return array<string, string>",
                    "-     *\/",
                    "-    private function getAvailableCollections(): array",
                    "-    {",
                    "-        return [",
                    "-            'documents' => 'Documents (Schulungsunterlagen)',",
                    "-            'mail' => 'Mail (E-Mails)',",
                    "-            'entities' => 'Entities (Entitäten)',",
                    "-            'knowledge' => 'Knowledge (Wissensbasis)',",
                    "-        ];",
                    "-    }",
                    "-",
                    "-    \/**",
                    "-     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/collection (AJAX)",
                    "-     * Update step collection configuration",
                    "-     *\/",
                    "-    public function updateStepCollection(string $id, string $stepId): void",
                    "-    {",
                    "-        $pipeline = $this->repository->findById((int) $id);",
                    "-",
                    "-        if ($pipeline === null) {",
                    "-            $this->json(['error' => 'Pipeline nicht gefunden'], 404);",
                    "-",
                    "-            return;",
                    "-        }",
                    "-",
                    "-        $collection = trim($_POST['collection'] ?? '');",
                    "-        $validCollections = array_keys($this->getAvailableCollections());",
                    "-",
                    "-        if ($collection === '' || !in_array($collection, $validCollections, true)) {",
                    "-            $this->json(['error' => 'Ungültige Collection'], 400);",
                    "-",
                    "-            return;",
                    "-        }",
                    "-",
                    "-        \/\/ Find step",
                    "-        $stepFound = false;",
                    "-        foreach ($pipeline['steps'] as $step) {",
                    "-            if ((int) $step['id'] === (int) $stepId) {",
                    "-                $stepFound = true;",
                    "-                $config = $step['config'] ?? [];",
                    "-                $config['collection'] = $collection;",
                    "-",
                    "-                $this->repository->updateStep((int) $stepId, [",
                    "-                    'config' => $config,",
                    "-                ]);",
                    "-                break;",
                    "-            }",
                    "-        }",
                    "-",
                    "-        if (!$stepFound) {",
                    "-            $this->json(['error' => 'Schritt nicht gefunden'], 404);",
                    "-",
                    "-            return;",
                    "-        }",
                    "-",
                    "-        $this->json([",
                    "-            'success' => true,",
                    "-            'collection' => $collection,",
                    "-            'label' => $this->getAvailableCollections()[$collection] ?? $collection,",
                    "-        ]);",
                    "-    }",
                    " }"
                ]
            }
        ],
        "originalFile": "<?php\n\nnamespace Controller;\n\nuse Framework\\Controller;\nuse Infrastructure\\AI\\ModelConfig;\nuse Infrastructure\\Persistence\\PipelineRepository;\n\nclass ContentPipelineController extends Controller\n{\n    private PipelineRepository $repository;\n\n    public function __construct()\n    {\n        $this->repository = new PipelineRepository();\n    }\n\n    \/**\n     * GET \/content-pipeline\n     *\/\n    public function index(): void\n    {\n        $pipelines = $this->repository->findAll();\n        $stats = $this->repository->getStatistics();\n\n        $this->view('content-pipeline.index', [\n            'title' => 'Content Pipeline',\n            'pipelines' => $pipelines,\n            'stats' => $stats,\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/import\n     *\/\n    public function import(): void\n    {\n        $pipeline = $this->repository->findDefault();\n\n        if ($pipeline === null) {\n            $pipelines = $this->repository->findAll(1);\n            $pipeline = $pipelines[0] ?? null;\n        }\n\n        $latestRun = $pipeline !== null\n            ? $this->repository->findLatestRun((int) $pipeline['id'])\n            : null;\n\n        $this->view('content-pipeline.import', [\n            'title' => 'Import Pipeline',\n            'pipeline' => $pipeline,\n            'latestRun' => $latestRun,\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/new\n     *\/\n    public function pipelineNew(): void\n    {\n        $this->view('content-pipeline.form', [\n            'title' => 'Neue Pipeline',\n            'pipeline' => null,\n            'stepTypes' => $this->getStepTypes(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\n     *\/\n    public function show(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $runs = $this->repository->findRuns((int) $id, 10);\n\n        $this->view('content-pipeline.show', [\n            'title' => 'Pipeline: ' . $pipeline['name'],\n            'pipeline' => $pipeline,\n            'runs' => $runs,\n            'stepTypes' => $this->getStepTypes(),\n            'models' => ModelConfig::getAll(),\n            'defaultModel' => ModelConfig::DEFAULT_MODEL,\n            'collections' => $this->getAvailableCollections(),\n        ]);\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\/edit\n     *\/\n    public function edit(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->view('content-pipeline.form', [\n            'title' => 'Pipeline bearbeiten: ' . $pipeline['name'],\n            'pipeline' => $pipeline,\n            'stepTypes' => $this->getStepTypes(),\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\n     *\/\n    public function store(): void\n    {\n        $this->requireCsrf();\n\n        $name = trim($_POST['name'] ?? '');\n        $description = trim($_POST['description'] ?? '');\n        $sourcePath = trim($_POST['source_path'] ?? '\/var\/www\/nextcloud\/data\/root\/files\/Documents');\n        $extensions = $this->parseExtensions($_POST['extensions'] ?? '');\n        $isDefault = isset($_POST['is_default']) ? 1 : 0;\n\n        if ($name === '') {\n            $_SESSION['error'] = 'Name ist erforderlich.';\n            header('Location: \/content-pipeline\/new');\n            exit;\n        }\n\n        $pipelineId = $this->repository->create([\n            'name' => $name,\n            'description' => $description,\n            'source_path' => $sourcePath,\n            'extensions' => $extensions,\n            'is_default' => $isDefault,\n        ]);\n\n        \/\/ Standard-Steps hinzufuegen\n        $this->createDefaultSteps($pipelineId);\n\n        $_SESSION['success'] = 'Pipeline erfolgreich erstellt.';\n        header('Location: \/content-pipeline\/' . $pipelineId);\n        exit;\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\n     *\/\n    public function update(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $name = trim($_POST['name'] ?? '');\n        $description = trim($_POST['description'] ?? '');\n        $sourcePath = trim($_POST['source_path'] ?? '');\n        $extensions = $this->parseExtensions($_POST['extensions'] ?? '');\n        $isDefault = isset($_POST['is_default']) ? 1 : 0;\n\n        if ($name === '') {\n            $_SESSION['error'] = 'Name ist erforderlich.';\n            header('Location: \/content-pipeline\/' . $id . '\/edit');\n            exit;\n        }\n\n        $this->repository->update((int) $id, [\n            'name' => $name,\n            'description' => $description,\n            'source_path' => $sourcePath,\n            'extensions' => $extensions,\n            'is_default' => $isDefault,\n        ]);\n\n        $_SESSION['success'] = 'Pipeline aktualisiert.';\n        header('Location: \/content-pipeline\/' . $id);\n        exit;\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/run\n     *\/\n    public function run(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        \/\/ Neuen Run erstellen\n        $runId = $this->repository->createRun((int) $id);\n\n        \/\/ Pipeline im Hintergrund starten\n        $pipelineScript = '\/opt\/scripts\/pipeline\/pipeline.py';\n        $venvPython = '\/opt\/scripts\/pipeline\/venv\/bin\/python';\n        $logFile = '\/tmp\/pipeline_run_' . $runId . '.log';\n\n        $cmd = sprintf(\n            'nohup %s %s all --pipeline-id=%d --run-id=%d > %s 2>&1 &',\n            escapeshellarg($venvPython),\n            escapeshellarg($pipelineScript),\n            (int) $id,\n            $runId,\n            escapeshellarg($logFile)\n        );\n\n        exec($cmd);\n\n        $_SESSION['success'] = 'Pipeline gestartet (Run #' . $runId . ')';\n        header('Location: \/content-pipeline\/' . $id);\n        exit;\n    }\n\n    \/**\n     * GET \/content-pipeline\/{id}\/status\n     * AJAX endpoint for run status\n     *\/\n    public function status(string $id): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n            return;\n        }\n\n        $latestRun = $this->repository->findLatestRun((int) $id);\n\n        $this->json([\n            'pipeline_id' => (int) $id,\n            'run' => $latestRun,\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/toggle\n     *\/\n    public function toggleStep(string $id, string $stepId): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        \/\/ Find step and toggle\n        foreach ($pipeline['steps'] as $step) {\n            if ((int) $step['id'] === (int) $stepId) {\n                $this->repository->updateStep((int) $stepId, [\n                    'enabled' => $step['enabled'] ? 0 : 1,\n                ]);\n                break;\n            }\n        }\n\n        header('Location: \/content-pipeline\/' . $id);\n        exit;\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/steps\/{stepId}\/model (AJAX)\n     * Update step model configuration\n     *\/\n    public function updateStepModel(string $id, string $stepId): void\n    {\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->json(['error' => 'Pipeline nicht gefunden'], 404);\n\n            return;\n        }\n\n        $model = trim($_POST['model'] ?? '');\n\n        if ($model === '' || !ModelConfig::isValid($model)) {\n            $this->json(['error' => 'Ungültiges Modell'], 400);\n\n            return;\n        }\n\n        \/\/ Find step\n        $stepFound = false;\n        foreach ($pipeline['steps'] as $step) {\n            if ((int) $step['id'] === (int) $stepId) {\n                $stepFound = true;\n                $config = $step['config'] ?? [];\n\n                \/\/ Determine provider from model\n                $provider = ModelConfig::isLocal($model) ? 'ollama' : 'anthropic';\n\n                \/\/ Update config with new model\n                $config['model'] = ModelConfig::isLocal($model)\n                    ? substr($model, 7)  \/\/ Remove 'ollama:' prefix\n                    : $model;\n                $config['provider'] = $provider;\n\n                $this->repository->updateStep((int) $stepId, [\n                    'config' => $config,\n                ]);\n                break;\n            }\n        }\n\n        if (!$stepFound) {\n            $this->json(['error' => 'Schritt nicht gefunden'], 404);\n\n            return;\n        }\n\n        $this->json([\n            'success' => true,\n            'model' => $model,\n            'label' => ModelConfig::getLabel($model),\n        ]);\n    }\n\n    \/**\n     * POST \/content-pipeline\/{id}\/delete\n     *\/\n    public function delete(string $id): void\n    {\n        $this->requireCsrf();\n\n        $pipeline = $this->repository->findById((int) $id);\n\n        if ($pipeline === null) {\n            $this->notFound('Pipeline nicht gefunden');\n        }\n\n        $this->repository->delete((int) $id);\n\n        $_SESSION['success'] = 'Pipeline geloescht.';\n        header('Location: \/content-pipeline');\n        exit;\n    }\n\n    \/**\n     * @return array<string, array<string, mixed>>\n     *\/\n    private function getStepTypes(): array\n    {\n        return [\n            \/\/ Phase 1: Vorverarbeitung\n            'detect' => [\n                'label' => 'Erkennung',\n                'description' => 'Dateien scannen und Format prüfen',\n                'phase' => 'Vorverarbeitung',\n                'storage' => null,\n            ],\n            'validate' ... [TRUNCATED-0bcb1c3422ec6156]"
    }
}
← Vorheriger Zur Liste Nächster →