Files
TaskMate/frontend/js/coding.js
2026-01-04 00:24:11 +00:00

778 Zeilen
22 KiB
JavaScript

/**
* TASKMATE - Coding Manager
* =========================
* Verwaltung von Server-Anwendungen mit Claude/Codex Integration
*/
import api from './api.js';
import { escapeHtml } from './utils.js';
// Toast-Funktion (verwendet das globale Toast-Event)
function showToast(message, type = 'info') {
window.dispatchEvent(new CustomEvent('toast:show', {
detail: { message, type }
}));
}
// Basis-Pfad für alle Anwendungen auf dem Server
const BASE_PATH = '/home/claude-dev';
// Farb-Presets für Anwendungen
const COLOR_PRESETS = [
'#4F46E5', // Indigo
'#7C3AED', // Violet
'#EC4899', // Pink
'#EF4444', // Red
'#F59E0B', // Amber
'#10B981', // Emerald
'#06B6D4', // Cyan
'#3B82F6', // Blue
'#8B5CF6', // Purple
'#6366F1' // Indigo Light
];
class CodingManager {
constructor() {
this.initialized = false;
this.directories = [];
this.refreshInterval = null;
this.editingDirectory = null;
this.giteaRepos = [];
}
/**
* Manager initialisieren
*/
async init() {
if (this.initialized) return;
this.bindEvents();
this.initialized = true;
console.log('[CodingManager] Initialisiert');
}
/**
* Event-Listener binden
*/
bindEvents() {
// Add-Button
const addBtn = document.getElementById('add-coding-directory-btn');
if (addBtn) {
addBtn.addEventListener('click', () => this.openModal());
}
// Modal Events
const modal = document.getElementById('coding-modal');
if (modal) {
// Close-Button
modal.querySelector('.modal-close')?.addEventListener('click', () => this.closeModal());
modal.querySelector('.modal-cancel')?.addEventListener('click', () => this.closeModal());
// Save-Button
document.getElementById('coding-save-btn')?.addEventListener('click', () => this.handleSave());
// Delete-Button
document.getElementById('coding-delete-btn')?.addEventListener('click', () => this.handleDelete());
// Backdrop-Click
modal.addEventListener('click', (e) => {
if (e.target === modal) this.closeModal();
});
// Farb-Presets
this.renderColorPresets();
// Name-Eingabe für Pfad-Preview
const nameInput = document.getElementById('coding-name');
if (nameInput) {
nameInput.addEventListener('input', () => this.updatePathPreview());
}
// CLAUDE.md Link Event
const claudeLink = document.getElementById('coding-claude-link');
if (claudeLink) {
claudeLink.addEventListener('click', () => this.openClaudeModal());
}
}
// Command-Modal Events
const cmdModal = document.getElementById('coding-command-modal');
if (cmdModal) {
cmdModal.querySelector('.modal-close')?.addEventListener('click', () => this.closeCommandModal());
cmdModal.addEventListener('click', (e) => {
if (e.target === cmdModal) this.closeCommandModal();
});
document.getElementById('coding-copy-command')?.addEventListener('click', () => this.copyCommand());
}
// Gitea-Repo Dropdown laden bei Details-Toggle
const giteaSection = document.querySelector('.coding-gitea-section');
if (giteaSection) {
giteaSection.addEventListener('toggle', (e) => {
if (e.target.open) {
this.loadGiteaRepos();
}
});
}
// CLAUDE.md Modal Events
const claudeModal = document.getElementById('claude-md-modal');
if (claudeModal) {
// Close-Button
claudeModal.querySelector('.modal-close')?.addEventListener('click', () => this.closeClaudeModal());
// Backdrop-Click
claudeModal.addEventListener('click', (e) => {
if (e.target === claudeModal) this.closeClaudeModal();
});
// ESC-Taste
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && !claudeModal.classList.contains('hidden')) {
this.closeClaudeModal();
}
});
}
}
/**
* Farb-Presets rendern
*/
renderColorPresets() {
const container = document.getElementById('coding-color-presets');
if (!container) return;
container.innerHTML = COLOR_PRESETS.map(color => `
<button type="button" class="color-preset" data-color="${color}" style="background-color: ${color};" title="${color}"></button>
`).join('') + `
<input type="color" id="coding-color-custom" class="color-picker-custom" value="#4F46E5" title="Eigene Farbe">
`;
// Event-Listener für Presets
container.querySelectorAll('.color-preset').forEach(btn => {
btn.addEventListener('click', () => {
container.querySelectorAll('.color-preset').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('coding-color-custom').value = btn.dataset.color;
});
});
// Custom Color Input
document.getElementById('coding-color-custom')?.addEventListener('input', (e) => {
container.querySelectorAll('.color-preset').forEach(b => b.classList.remove('selected'));
});
}
/**
* Pfad-Preview aktualisieren
*/
updatePathPreview() {
const nameInput = document.getElementById('coding-name');
const preview = document.getElementById('coding-path-preview');
if (!nameInput || !preview) return;
const name = nameInput.value.trim();
preview.textContent = name || '...';
}
// switchClaudeTab entfernt - CLAUDE.md ist jetzt nur readonly
/**
* CLAUDE.md Link aktualisieren
*/
updateClaudeLink(content, projectName) {
const link = document.getElementById('coding-claude-link');
const textSpan = link?.querySelector('.claude-text');
// Debug entfernt
if (!link || !textSpan) {
console.error('CLAUDE.md link elements not found!');
return;
}
// Content für Modal speichern
this.currentClaudeContent = content;
this.currentProjectName = projectName;
if (content) {
link.disabled = false;
textSpan.textContent = `CLAUDE.md anzeigen (${Math.round(content.length / 1024)}KB)`;
} else {
link.disabled = true;
textSpan.textContent = 'Keine CLAUDE.md vorhanden';
}
}
/**
* CLAUDE.md Modal öffnen
*/
openClaudeModal() {
if (!this.currentClaudeContent) {
console.warn('No CLAUDE.md content to display');
return;
}
const modal = document.getElementById('claude-md-modal');
const overlay = document.querySelector('.modal-overlay');
const content = document.getElementById('claude-md-content');
const title = modal?.querySelector('.modal-header h3');
if (!modal || !content) {
console.error('CLAUDE.md modal elements not found!');
return;
}
// Titel setzen
if (title && this.currentProjectName) {
title.textContent = `CLAUDE.md - ${this.currentProjectName}`;
}
// Content setzen
content.textContent = this.currentClaudeContent;
// Modal anzeigen
modal.classList.remove('hidden');
modal.classList.add('visible');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('visible');
}
// Modal opened
}
/**
* CLAUDE.md Modal schließen
*/
closeClaudeModal() {
const modal = document.getElementById('claude-md-modal');
const overlay = document.querySelector('.modal-overlay');
if (modal) {
modal.classList.remove('visible');
setTimeout(() => modal.classList.add('hidden'), 200);
}
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => overlay.classList.add('hidden'), 200);
}
// Modal closed
}
/**
* Gitea-Repositories laden
*/
async loadGiteaRepos() {
try {
const select = document.getElementById('coding-gitea-repo');
if (!select) return;
// Lade-Indikator
select.innerHTML = '<option value="">Laden...</option>';
const result = await api.getGiteaRepositories();
console.log('Gitea API Response:', result);
this.giteaRepos = result?.repositories || [];
select.innerHTML = '<option value="">-- Kein Repository --</option>' +
this.giteaRepos.map(repo => `
<option value="${repo.cloneUrl}" data-owner="${repo.owner || ''}" data-name="${repo.name}">
${escapeHtml(repo.fullName)}
</option>
`).join('');
// Wenn Editing, vorhandenen Wert setzen
if (this.editingDirectory?.giteaRepoUrl) {
select.value = this.editingDirectory.giteaRepoUrl;
}
} catch (error) {
console.error('Fehler beim Laden der Gitea-Repos:', error);
const select = document.getElementById('coding-gitea-repo');
if (select) {
select.innerHTML = '<option value="">Fehler beim Laden</option>';
}
}
}
/**
* Anwendungen laden
*/
async loadDirectories() {
try {
this.directories = await api.getCodingDirectories();
this.render();
} catch (error) {
console.error('Fehler beim Laden der Anwendungen:', error);
showToast('Fehler beim Laden der Anwendungen', 'error');
}
}
/**
* View rendern
*/
render() {
const grid = document.getElementById('coding-grid');
const empty = document.getElementById('coding-empty');
if (!grid) return;
if (this.directories.length === 0) {
grid.innerHTML = '';
grid.classList.add('hidden');
empty?.classList.remove('hidden');
return;
}
empty?.classList.add('hidden');
grid.classList.remove('hidden');
grid.innerHTML = this.directories.map(dir => this.renderTile(dir)).join('');
// Event-Listener für Tiles
this.bindTileEvents();
// Git-Status für jede Anwendung laden
this.directories.forEach(dir => this.updateTileStatus(dir.id));
}
/**
* Einzelne Kachel rendern
*/
renderTile(directory) {
const hasGitea = !!directory.giteaRepoUrl;
return `
<div class="coding-tile" data-id="${directory.id}">
<div class="coding-tile-color" style="background-color: ${directory.color || '#4F46E5'}"></div>
<div class="coding-tile-header">
<span class="coding-tile-icon">📁</span>
</div>
<div class="coding-tile-content">
<div class="coding-tile-name">${escapeHtml(directory.name)}</div>
<div class="coding-tile-path">${escapeHtml(directory.localPath)}</div>
${directory.description ? `<div class="coding-tile-description">${escapeHtml(directory.description)}</div>` : ''}
${directory.hasCLAUDEmd ? '<div class="coding-tile-badge">CLAUDE.md</div>' : ''}
</div>
<div class="coding-tile-status" id="coding-status-${directory.id}">
<span class="git-status-badge loading">Lade...</span>
</div>
<div class="coding-tile-actions">
<button class="btn-claude" data-id="${directory.id}" data-path="${escapeHtml(directory.localPath)}" title="SSH-Befehl für Claude kopieren">
Claude starten
</button>
</div>
${hasGitea ? `
<div class="coding-tile-git">
<button class="btn btn-sm btn-secondary coding-git-fetch" data-id="${directory.id}">Fetch</button>
<button class="btn btn-sm btn-secondary coding-git-pull" data-id="${directory.id}">Pull</button>
<button class="btn btn-sm btn-secondary coding-git-push" data-id="${directory.id}">Push</button>
<button class="btn btn-sm btn-secondary coding-git-commit" data-id="${directory.id}">Commit</button>
</div>
` : ''}
</div>
`;
}
/**
* Event-Listener für Tiles binden
*/
bindTileEvents() {
// Kachel-Klick für Modal
document.querySelectorAll('.coding-tile').forEach(tile => {
tile.addEventListener('click', (e) => {
// Nicht triggern wenn Button-Kind geklickt wird
if (e.target.closest('button')) return;
const id = parseInt(tile.dataset.id);
const dir = this.directories.find(d => d.id === id);
if (dir) this.openModal(dir);
});
});
// Claude-Buttons
document.querySelectorAll('.btn-claude').forEach(btn => {
btn.addEventListener('click', () => this.launchClaude(btn.dataset.path));
});
// Git-Buttons
document.querySelectorAll('.coding-git-fetch').forEach(btn => {
btn.addEventListener('click', () => this.gitFetch(parseInt(btn.dataset.id)));
});
document.querySelectorAll('.coding-git-pull').forEach(btn => {
btn.addEventListener('click', () => this.gitPull(parseInt(btn.dataset.id)));
});
document.querySelectorAll('.coding-git-push').forEach(btn => {
btn.addEventListener('click', () => this.gitPush(parseInt(btn.dataset.id)));
});
document.querySelectorAll('.coding-git-commit').forEach(btn => {
btn.addEventListener('click', () => this.promptCommit(parseInt(btn.dataset.id)));
});
}
/**
* Git-Status für eine Kachel aktualisieren
*/
async updateTileStatus(id) {
const statusEl = document.getElementById(`coding-status-${id}`);
if (!statusEl) return;
try {
const status = await api.getCodingDirectoryStatus(id);
if (!status.isGitRepo) {
statusEl.innerHTML = '<span class="git-status-badge">Kein Git-Repo</span>';
return;
}
const statusClass = status.isClean ? 'clean' : 'dirty';
const statusText = status.isClean ? 'Clean' : `${status.changes?.length || 0} Änderungen`;
statusEl.innerHTML = `
<span class="git-branch-badge">${escapeHtml(status.branch)}</span>
<span class="git-status-badge ${statusClass}">${statusText}</span>
${status.ahead > 0 ? `<span class="git-status-badge ahead">↑${status.ahead}</span>` : ''}
${status.behind > 0 ? `<span class="git-status-badge behind">↓${status.behind}</span>` : ''}
`;
} catch (error) {
statusEl.innerHTML = '<span class="git-status-badge error">Fehler</span>';
}
}
/**
* Claude Code starten - SSH-Befehl kopieren
*/
async launchClaude(path) {
const command = `ssh claude-dev@91.99.192.14 -t "cd ${path} && claude"`;
try {
await navigator.clipboard.writeText(command);
this.showCommandModal(
command,
'Befehl kopiert! Öffne Terminal/CMD und füge ein (Strg+V). Passwort: z0E1Al}q2H?Yqd!O'
);
showToast('SSH-Befehl kopiert!', 'success');
} catch (error) {
// Fallback wenn Clipboard nicht verfügbar
this.showCommandModal(
command,
'Kopiere diesen Befehl und füge ihn im Terminal ein. Passwort: z0E1Al}q2H?Yqd!O'
);
}
}
/**
* Command-Modal anzeigen
*/
showCommandModal(command, hint) {
const modal = document.getElementById('coding-command-modal');
const hintEl = document.getElementById('coding-command-hint');
const textEl = document.getElementById('coding-command-text');
if (!modal || !textEl) return;
hintEl.textContent = hint || 'Führe diesen Befehl aus:';
textEl.textContent = command;
this.currentCommand = command;
modal.classList.remove('hidden');
}
/**
* Command-Modal schließen
*/
closeCommandModal() {
const modal = document.getElementById('coding-command-modal');
if (modal) modal.classList.add('hidden');
}
/**
* Befehl in Zwischenablage kopieren
*/
async copyCommand() {
if (!this.currentCommand) return;
try {
await navigator.clipboard.writeText(this.currentCommand);
showToast('Befehl kopiert!', 'success');
} catch (error) {
console.error('Kopieren fehlgeschlagen:', error);
showToast('Kopieren fehlgeschlagen', 'error');
}
}
/**
* Git Fetch
*/
async gitFetch(id) {
try {
showToast('Fetch läuft...', 'info');
await api.codingGitFetch(id);
showToast('Fetch erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Fetch fehlgeschlagen', 'error');
}
}
/**
* Git Pull
*/
async gitPull(id) {
try {
showToast('Pull läuft...', 'info');
await api.codingGitPull(id);
showToast('Pull erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Pull fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
/**
* Git Push
*/
async gitPush(id) {
try {
showToast('Push läuft...', 'info');
await api.codingGitPush(id);
showToast('Push erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Push fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
/**
* Commit-Dialog anzeigen
*/
promptCommit(id) {
const message = prompt('Commit-Nachricht eingeben:');
if (message && message.trim()) {
this.gitCommit(id, message.trim());
}
}
/**
* Git Commit
*/
async gitCommit(id, message) {
try {
showToast('Commit läuft...', 'info');
await api.codingGitCommit(id, message);
showToast('Commit erfolgreich', 'success');
this.updateTileStatus(id);
} catch (error) {
showToast('Commit fehlgeschlagen: ' + (error.message || 'Unbekannter Fehler'), 'error');
}
}
/**
* Modal öffnen
*/
openModal(directory = null) {
this.editingDirectory = directory;
const modal = document.getElementById('coding-modal');
const overlay = document.querySelector('.modal-overlay');
const title = document.getElementById('coding-modal-title');
const deleteBtn = document.getElementById('coding-delete-btn');
if (!modal) return;
// Titel setzen
title.textContent = directory ? 'Anwendung bearbeiten' : 'Anwendung hinzufügen';
// Delete-Button anzeigen/verstecken
if (deleteBtn) {
deleteBtn.classList.toggle('hidden', !directory);
}
// Felder füllen
document.getElementById('coding-name').value = directory?.name || '';
document.getElementById('coding-description').value = directory?.description || '';
document.getElementById('coding-branch').value = directory?.defaultBranch || 'main';
// CLAUDE.md: Nur aus Dateisystem anzeigen
const claudeContent = directory?.claudeMdFromDisk || '';
this.updateClaudeLink(claudeContent, directory?.name);
// Pfad-Preview aktualisieren
this.updatePathPreview();
// Farbe setzen
const color = directory?.color || '#4F46E5';
document.getElementById('coding-color-custom').value = color;
document.querySelectorAll('.color-preset').forEach(btn => {
btn.classList.toggle('selected', btn.dataset.color === color);
});
// Gitea-Sektion zurücksetzen
const giteaSection = document.querySelector('.coding-gitea-section');
if (giteaSection) {
giteaSection.open = !!directory?.giteaRepoUrl;
}
// Repos laden wenn nötig
if (directory?.giteaRepoUrl) {
this.loadGiteaRepos();
}
// Modal und Overlay anzeigen
modal.classList.remove('hidden');
modal.classList.add('visible');
if (overlay) {
overlay.classList.remove('hidden');
overlay.classList.add('visible');
}
}
/**
* Modal schließen
*/
closeModal() {
const modal = document.getElementById('coding-modal');
const overlay = document.querySelector('.modal-overlay');
if (modal) {
modal.classList.remove('visible');
setTimeout(() => modal.classList.add('hidden'), 200);
this.editingDirectory = null;
}
if (overlay) {
overlay.classList.remove('visible');
setTimeout(() => overlay.classList.add('hidden'), 200);
}
}
/**
* Speichern-Handler
*/
async handleSave() {
const name = document.getElementById('coding-name').value.trim();
const description = document.getElementById('coding-description').value.trim();
// CLAUDE.md wird nicht mehr gespeichert - nur readonly
const defaultBranch = document.getElementById('coding-branch').value.trim() || 'main';
// Pfad automatisch aus Name generieren
const localPath = `${BASE_PATH}/${name}`;
// Farbe ermitteln
const selectedPreset = document.querySelector('.color-preset.selected');
const color = selectedPreset?.dataset.color || document.getElementById('coding-color-custom').value;
// Gitea-Daten
const giteaSelect = document.getElementById('coding-gitea-repo');
const giteaRepoUrl = giteaSelect?.value || null;
const giteaRepoOwner = giteaSelect?.selectedOptions[0]?.dataset.owner || null;
const giteaRepoName = giteaSelect?.selectedOptions[0]?.dataset.name || null;
if (!name) {
showToast('Anwendungsname ist erforderlich', 'error');
return;
}
const data = {
name,
localPath,
description,
color,
// claudeInstructions entfernt - CLAUDE.md ist readonly
giteaRepoUrl,
giteaRepoOwner,
giteaRepoName,
defaultBranch
};
try {
if (this.editingDirectory) {
await api.updateCodingDirectory(this.editingDirectory.id, data);
showToast('Anwendung aktualisiert', 'success');
} else {
await api.createCodingDirectory(data);
showToast('Anwendung hinzugefügt', 'success');
}
this.closeModal();
await this.loadDirectories();
} catch (error) {
console.error('Fehler beim Speichern:', error);
showToast(error.message || 'Fehler beim Speichern', 'error');
}
}
/**
* Löschen-Handler
*/
async handleDelete() {
if (!this.editingDirectory) return;
if (!confirm(`Anwendung "${this.editingDirectory.name}" wirklich löschen?`)) {
return;
}
try {
await api.deleteCodingDirectory(this.editingDirectory.id);
showToast('Anwendung gelöscht', 'success');
this.closeModal();
await this.loadDirectories();
} catch (error) {
console.error('Fehler beim Löschen:', error);
showToast('Fehler beim Löschen', 'error');
}
}
/**
* Auto-Refresh starten
*/
startAutoRefresh() {
this.stopAutoRefresh();
// Alle 30 Sekunden aktualisieren
this.refreshInterval = setInterval(() => {
this.directories.forEach(dir => this.updateTileStatus(dir.id));
}, 30000);
}
/**
* Auto-Refresh stoppen
*/
stopAutoRefresh() {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
/**
* View anzeigen
*/
async show() {
await this.loadDirectories();
this.startAutoRefresh();
}
/**
* View verstecken
*/
hide() {
this.stopAutoRefresh();
}
}
// Singleton-Instanz erstellen und exportieren
const codingManager = new CodingManager();
export default codingManager;