Files
TaskMate/backend/routes/coding.js
hendrik_gebhardt@gmx.de ef153789cc UI-Anpassungen
2026-01-10 10:32:52 +00:00

715 Zeilen
22 KiB
JavaScript

/**
* 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;