/** * TASKMATE - Coding Routes * ======================== * Verwaltung von Server-Anwendungen mit Claude/Codex Integration */ const express = require('express'); const router = express.Router(); const fs = require('fs'); const path = require('path'); const { getDb } = require('../database'); const logger = require('../utils/logger'); const gitService = require('../services/gitService'); /** * Prüft ob ein Pfad ein Server-Pfad (Linux) ist */ function isServerPath(localPath) { return localPath && localPath.startsWith('/'); } /** * Schreibt CLAUDE.md in ein Verzeichnis */ function writeCLAUDEmd(directoryPath, content) { if (!content || !directoryPath) return false; try { const claudePath = path.join(directoryPath, 'CLAUDE.md'); fs.writeFileSync(claudePath, content, 'utf8'); logger.info(`CLAUDE.md geschrieben: ${claudePath}`); return true; } catch (e) { logger.error('CLAUDE.md schreiben fehlgeschlagen:', e); return false; } } /** * Liest CLAUDE.md aus einem Verzeichnis */ function readCLAUDEmd(directoryPath) { if (!directoryPath) { logger.info('readCLAUDEmd: No directoryPath provided'); return null; } try { const claudePath = path.join(directoryPath, 'CLAUDE.md'); logger.info(`readCLAUDEmd: Checking path ${claudePath}`); if (fs.existsSync(claudePath)) { const content = fs.readFileSync(claudePath, 'utf8'); logger.info(`readCLAUDEmd: Successfully read ${content.length} characters from ${claudePath}`); return content; } else { logger.info(`readCLAUDEmd: File does not exist: ${claudePath}`); } } catch (e) { logger.error('CLAUDE.md lesen fehlgeschlagen:', e); } return null; } /** * GET /api/coding/directories * Alle Coding-Verzeichnisse abrufen */ router.get('/directories', (req, res) => { try { const db = getDb(); const directories = db.prepare(` SELECT cd.*, u.display_name as creator_name FROM coding_directories cd LEFT JOIN users u ON cd.created_by = u.id ORDER BY cd.position ASC, cd.name ASC `).all(); res.json(directories.map(dir => { // CLAUDE.md aus dem Dateisystem lesen falls vorhanden let claudeMdFromDisk = null; if (isServerPath(dir.local_path)) { claudeMdFromDisk = readCLAUDEmd(dir.local_path); // Fallback: Wenn Pfad /home/claude-dev/TaskMate ist, versuche /app/taskmate-source if (!claudeMdFromDisk && dir.local_path === '/home/claude-dev/TaskMate') { logger.info('Trying fallback path for TaskMate: /app/taskmate-source'); claudeMdFromDisk = readCLAUDEmd('/app/taskmate-source'); } } return { id: dir.id, name: dir.name, localPath: dir.local_path, description: dir.description, color: dir.color, claudeInstructions: dir.claude_instructions, claudeMdFromDisk: claudeMdFromDisk, hasCLAUDEmd: !!claudeMdFromDisk, giteaRepoUrl: dir.gitea_repo_url, giteaRepoOwner: dir.gitea_repo_owner, giteaRepoName: dir.gitea_repo_name, defaultBranch: dir.default_branch, lastSync: dir.last_sync, position: dir.position, createdAt: dir.created_at, createdBy: dir.created_by, creatorName: dir.creator_name }; })); } catch (error) { logger.error('Fehler beim Abrufen der Coding-Verzeichnisse:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/directories * Neues Coding-Verzeichnis erstellen */ router.post('/directories', (req, res) => { try { const { name, localPath, description, color, claudeInstructions, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch } = req.body; if (!name || !localPath) { return res.status(400).json({ error: 'Name und Server-Pfad sind erforderlich' }); } const db = getDb(); // Prüfe ob Pfad bereits existiert const existing = db.prepare('SELECT id FROM coding_directories WHERE local_path = ?').get(localPath); if (existing) { return res.status(400).json({ error: 'Diese Anwendung ist bereits registriert' }); } // Höchste Position ermitteln const maxPos = db.prepare('SELECT COALESCE(MAX(position), -1) as max FROM coding_directories').get().max; const result = db.prepare(` INSERT INTO coding_directories (name, local_path, description, color, claude_instructions, gitea_repo_url, gitea_repo_owner, gitea_repo_name, default_branch, position, created_by) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( name, localPath, description || null, color || '#4F46E5', null, // claudeInstructions wird nicht mehr gespeichert giteaRepoUrl || null, giteaRepoOwner || null, giteaRepoName || null, defaultBranch || 'main', maxPos + 1, req.user.id ); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(result.lastInsertRowid); // Ordner erstellen falls nicht vorhanden let directoryCreated = false; if (isServerPath(localPath) && !fs.existsSync(localPath)) { try { fs.mkdirSync(localPath, { recursive: true }); directoryCreated = true; logger.info(`Anwendungsordner erstellt: ${localPath}`); } catch (e) { logger.error('Ordner erstellen fehlgeschlagen:', e); } } // CLAUDE.md wird nicht mehr geschrieben - nur readonly let claudeMdWritten = false; logger.info(`Coding-Anwendung erstellt: ${name} (${localPath})`); // CLAUDE.md aus dem Dateisystem lesen für aktuelle Anzeige const claudeMdFromDisk = isServerPath(directory.local_path) ? readCLAUDEmd(directory.local_path) : null; res.status(201).json({ id: directory.id, name: directory.name, localPath: directory.local_path, description: directory.description, color: directory.color, claudeInstructions: directory.claude_instructions, claudeMdFromDisk: claudeMdFromDisk, hasCLAUDEmd: !!claudeMdFromDisk, giteaRepoUrl: directory.gitea_repo_url, giteaRepoOwner: directory.gitea_repo_owner, giteaRepoName: directory.gitea_repo_name, defaultBranch: directory.default_branch, position: directory.position, createdAt: directory.created_at, directoryCreated, claudeMdWritten }); } catch (error) { logger.error('Fehler beim Erstellen der Coding-Anwendung:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * PUT /api/coding/directories/:id * Coding-Anwendung aktualisieren */ router.put('/directories/:id', (req, res) => { try { const { id } = req.params; const { name, localPath, description, color, claudeInstructions, giteaRepoUrl, giteaRepoOwner, giteaRepoName, defaultBranch, position } = req.body; const db = getDb(); const existing = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!existing) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } // Prüfe ob neuer Pfad bereits von anderem Eintrag verwendet wird if (localPath && localPath !== existing.local_path) { const pathExists = db.prepare('SELECT id FROM coding_directories WHERE local_path = ? AND id != ?').get(localPath, id); if (pathExists) { return res.status(400).json({ error: 'Dieser Server-Pfad ist bereits registriert' }); } } db.prepare(` UPDATE coding_directories SET name = COALESCE(?, name), local_path = COALESCE(?, local_path), description = ?, color = COALESCE(?, color), claude_instructions = ?, gitea_repo_url = ?, gitea_repo_owner = ?, gitea_repo_name = ?, default_branch = COALESCE(?, default_branch), position = COALESCE(?, position) WHERE id = ? `).run( name || null, localPath || null, description !== undefined ? description : existing.description, color || null, null, // claudeInstructions wird nicht mehr aktualisiert giteaRepoUrl !== undefined ? giteaRepoUrl : existing.gitea_repo_url, giteaRepoOwner !== undefined ? giteaRepoOwner : existing.gitea_repo_owner, giteaRepoName !== undefined ? giteaRepoName : existing.gitea_repo_name, defaultBranch || null, position !== undefined ? position : null, id ); const updated = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); const finalPath = updated.local_path; // Ordner erstellen falls nicht vorhanden let directoryCreated = false; if (isServerPath(finalPath) && !fs.existsSync(finalPath)) { try { fs.mkdirSync(finalPath, { recursive: true }); directoryCreated = true; logger.info(`Anwendungsordner erstellt: ${finalPath}`); } catch (e) { logger.error('Ordner erstellen fehlgeschlagen:', e); } } // CLAUDE.md wird nicht mehr geschrieben - nur readonly let claudeMdWritten = false; logger.info(`Coding-Anwendung aktualisiert: ${updated.name}`); // CLAUDE.md aus dem Dateisystem lesen für aktuelle Anzeige const claudeMdFromDisk = isServerPath(updated.local_path) ? readCLAUDEmd(updated.local_path) : null; res.json({ id: updated.id, name: updated.name, localPath: updated.local_path, description: updated.description, color: updated.color, claudeInstructions: updated.claude_instructions, claudeMdFromDisk: claudeMdFromDisk, hasCLAUDEmd: !!claudeMdFromDisk, giteaRepoUrl: updated.gitea_repo_url, giteaRepoOwner: updated.gitea_repo_owner, giteaRepoName: updated.gitea_repo_name, defaultBranch: updated.default_branch, position: updated.position, createdAt: updated.created_at, directoryCreated, claudeMdWritten }); } catch (error) { logger.error('Fehler beim Aktualisieren der Coding-Anwendung:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * DELETE /api/coding/directories/:id * Coding-Anwendung löschen */ router.delete('/directories/:id', (req, res) => { try { const { id } = req.params; const db = getDb(); const existing = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!existing) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } db.prepare('DELETE FROM coding_directories WHERE id = ?').run(id); logger.info(`Coding-Anwendung gelöscht: ${existing.name}`); res.json({ message: 'Anwendung gelöscht' }); } catch (error) { logger.error('Fehler beim Löschen der Coding-Anwendung:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/coding/directories/:id/status * Git-Status eines Verzeichnisses abrufen */ router.get('/directories/:id/status', (req, res) => { try { const { id } = req.params; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const localPath = directory.local_path; // Prüfe ob es ein Git-Repository ist if (!gitService.isGitRepository(localPath)) { return res.json({ isGitRepo: false, message: 'Kein Git-Repository' }); } const status = gitService.getStatus(localPath); if (!status.success) { return res.status(500).json({ error: status.error }); } res.json({ isGitRepo: true, branch: status.branch, hasChanges: status.hasChanges, changes: status.changes, ahead: status.ahead, behind: status.behind, isClean: status.isClean }); } catch (error) { logger.error('Fehler beim Abrufen des Git-Status:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/directories/:id/fetch * Git Fetch ausführen */ router.post('/directories/:id/fetch', (req, res) => { try { const { id } = req.params; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const result = gitService.fetchRemote(directory.local_path); if (!result.success) { return res.status(500).json({ error: result.error }); } // Last sync aktualisieren db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id); logger.info(`Git fetch ausgeführt für: ${directory.name}`); res.json({ success: true, message: 'Fetch erfolgreich' }); } catch (error) { logger.error('Fehler beim Git Fetch:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/directories/:id/pull * Git Pull ausführen */ router.post('/directories/:id/pull', (req, res) => { try { const { id } = req.params; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const result = gitService.pullChanges(directory.local_path, { branch: directory.default_branch }); if (!result.success) { return res.status(500).json({ error: result.error }); } // Last sync aktualisieren db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id); logger.info(`Git pull ausgeführt für: ${directory.name}`); res.json({ success: true, message: 'Pull erfolgreich', output: result.output }); } catch (error) { logger.error('Fehler beim Git Pull:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/directories/:id/push * Git Push ausführen */ router.post('/directories/:id/push', (req, res) => { try { const { id } = req.params; const { force } = req.body; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const result = gitService.pushWithUpstream(directory.local_path, directory.default_branch, 'origin', force); if (!result.success) { return res.status(500).json({ error: result.error }); } // Last sync aktualisieren db.prepare('UPDATE coding_directories SET last_sync = CURRENT_TIMESTAMP WHERE id = ?').run(id); logger.info(`Git push ausgeführt für: ${directory.name}`); res.json({ success: true, message: 'Push erfolgreich', output: result.output }); } catch (error) { logger.error('Fehler beim Git Push:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/directories/:id/commit * Git Commit ausführen */ router.post('/directories/:id/commit', (req, res) => { try { const { id } = req.params; const { message } = req.body; const db = getDb(); if (!message || message.trim() === '') { return res.status(400).json({ error: 'Commit-Nachricht erforderlich' }); } const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } // Stage all changes const stageResult = gitService.stageAll(directory.local_path); if (!stageResult.success) { return res.status(500).json({ error: stageResult.error }); } // Commit with author info const author = { name: req.user.display_name || req.user.username, email: req.user.email || `${req.user.username}@taskmate.local` }; const result = gitService.commit(directory.local_path, message, author); if (!result.success) { return res.status(500).json({ error: result.error }); } logger.info(`Git commit ausgeführt für: ${directory.name} - "${message}"`); res.json({ success: true, message: 'Commit erfolgreich', output: result.output }); } catch (error) { logger.error('Fehler beim Git Commit:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/coding/directories/:id/branches * Branches abrufen */ router.get('/directories/:id/branches', (req, res) => { try { const { id } = req.params; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const result = gitService.getBranches(directory.local_path); if (!result.success) { return res.status(500).json({ error: result.error }); } res.json({ branches: result.branches }); } catch (error) { logger.error('Fehler beim Abrufen der Branches:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/directories/:id/checkout * Branch wechseln */ router.post('/directories/:id/checkout', (req, res) => { try { const { id } = req.params; const { branch } = req.body; const db = getDb(); if (!branch) { return res.status(400).json({ error: 'Branch erforderlich' }); } const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const result = gitService.checkoutBranch(directory.local_path, branch); if (!result.success) { return res.status(500).json({ error: result.error }); } logger.info(`Branch gewechselt für ${directory.name}: ${branch}`); res.json({ success: true, message: `Gewechselt zu Branch: ${branch}` }); } catch (error) { logger.error('Fehler beim Branch-Wechsel:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * POST /api/coding/validate-path * Pfad validieren */ router.post('/validate-path', (req, res) => { try { const { path: localPath } = req.body; if (!localPath) { return res.status(400).json({ error: 'Pfad erforderlich' }); } // Nur Server-Pfade können validiert werden if (isServerPath(localPath)) { const containerPath = gitService.windowsToContainerPath(localPath); const exists = fs.existsSync(containerPath); const isGitRepo = exists && gitService.isGitRepository(localPath); res.json({ valid: true, exists, isGitRepo, isServerPath: true }); } else { // Windows-Pfad kann nicht serverseitig validiert werden res.json({ valid: true, exists: null, isGitRepo: null, isServerPath: false, message: 'Windows-Pfade können nicht serverseitig validiert werden' }); } } catch (error) { logger.error('Fehler bei der Pfad-Validierung:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/coding/directories/:id/commits * Commit-Historie abrufen */ router.get('/directories/:id/commits', (req, res) => { try { const { id } = req.params; const limit = parseInt(req.query.limit) || 20; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const result = gitService.getCommitHistory(directory.local_path, limit); if (!result.success) { return res.status(500).json({ error: result.error }); } res.json({ commits: result.commits }); } catch (error) { logger.error('Fehler beim Abrufen der Commit-Historie:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/coding/directories/:id/usage * Aktuelle Verbrauchsdaten abrufen (simuliert) */ router.get('/directories/:id/usage', (req, res) => { try { const { id } = req.params; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } // Simulierte Verbrauchsdaten generieren const usage = { cpu_percent: Math.random() * 100, memory_mb: Math.floor(Math.random() * 4096), disk_read_mb: Math.random() * 100, disk_write_mb: Math.random() * 50, network_recv_mb: Math.random() * 10, network_sent_mb: Math.random() * 10, process_count: Math.floor(Math.random() * 20) + 1, timestamp: new Date() }; // Speichere in Datenbank für Historie db.prepare(` INSERT INTO coding_usage (directory_id, cpu_percent, memory_mb, disk_read_mb, disk_write_mb, network_recv_mb, network_sent_mb, process_count) VALUES (?, ?, ?, ?, ?, ?, ?, ?) `).run(id, usage.cpu_percent, usage.memory_mb, usage.disk_read_mb, usage.disk_write_mb, usage.network_recv_mb, usage.network_sent_mb, usage.process_count); res.json({ usage }); } catch (error) { logger.error('Fehler beim Abrufen der Verbrauchsdaten:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); /** * GET /api/coding/directories/:id/usage/history * Historische Verbrauchsdaten abrufen */ router.get('/directories/:id/usage/history', (req, res) => { try { const { id } = req.params; const hours = parseInt(req.query.hours) || 24; const db = getDb(); const directory = db.prepare('SELECT * FROM coding_directories WHERE id = ?').get(id); if (!directory) { return res.status(404).json({ error: 'Anwendung nicht gefunden' }); } const history = db.prepare(` SELECT * FROM coding_usage WHERE directory_id = ? AND timestamp > datetime('now', '-${hours} hours') ORDER BY timestamp DESC LIMIT 100 `).all(id); res.json({ history }); } catch (error) { logger.error('Fehler beim Abrufen der Verbrauchshistorie:', error); res.status(500).json({ error: 'Interner Serverfehler' }); } }); module.exports = router;