Push für Serveranwendung in Gitea implementiert
Dieser Commit ist enthalten in:
HG
2025-12-30 17:25:14 +00:00
committet von Server Deploy
Ursprung 87c391d2e6
Commit c8707d6cf4
30 geänderte Dateien mit 11907 neuen und 170 gelöschten Zeilen

Datei anzeigen

@ -1,6 +1,125 @@
TASKMATE - CHANGELOG
====================
================================================================================
30.12.2025 - Gitea-Integration: Server-Modus
================================================================================
FEATURE: SERVER-DATEIEN DIREKT INS GITEA PUSHEN
--------------------------------------------------------------------------------
- Neuer "Server-Anwendung" Modus im Gitea-Tab
- Ermöglicht Git-Operationen direkt auf dem Server-Verzeichnis /home/claude-dev/TaskMate
- Alle Änderungen an der Anwendung können sofort ins Gitea gepusht werden
- Kein separates Konfigurieren nötig - sofort einsatzbereit
FEATURE: ZWEI-MODUS-SCHALTER
--------------------------------------------------------------------------------
- Modus-Schalter oben im Gitea-Tab: "Server-Anwendung" und "Projekt-Repository"
- Server-Modus: Zeigt TaskMate Server-Dateien
- Projekt-Modus: Bisherige Funktionalität für lokale Pfade pro Projekt
SERVER-MODUS FUNKTIONEN
--------------------------------------------------------------------------------
- Git-Status anzeigen (geänderte Dateien, aktueller Branch)
- Branch-Wechsel
- Fetch, Pull, Push, Commit
- Commit-Historie anzeigen
- Änderungsliste mit Statusanzeige (M, A, D, ?)
TECHNISCHE ÄNDERUNGEN
--------------------------------------------------------------------------------
- docker-compose.yml: Neues Volume-Mount ".:/app/taskmate-source" für Zugriff auf Source-Code
- backend/routes/git.js: Neue Server-Endpoints:
* GET /api/git/server/info - Repository-Informationen
* GET /api/git/server/status - Git-Status
* GET /api/git/server/branches - Branch-Liste
* GET /api/git/server/commits - Commit-Historie
* GET /api/git/server/remote - Remote-URL
* POST /api/git/server/stage - Alle Änderungen stagen
* POST /api/git/server/commit - Commit erstellen
* POST /api/git/server/push - Push ausführen
* POST /api/git/server/pull - Pull ausführen
* POST /api/git/server/fetch - Fetch ausführen
* POST /api/git/server/checkout - Branch wechseln
- frontend/js/api.js: Neue API-Funktionen für Server-Modus
- frontend/js/gitea.js:
* GiteaManager mit currentMode Property
* Server-Modus Methoden (loadServerData, renderServer*, handleServer*)
* Modus-Wechsel Handler
- frontend/index.html: Server-Modus UI mit Modus-Schalter
- frontend/css/gitea.css: Styles für Modus-Schalter (.gitea-mode-switch, .gitea-mode-btn)
- frontend/sw.js: Cache-Version auf 131 erhöht
================================================================================
30.12.2025 - Session-Countdown auf Hauptoberfläche
================================================================================
FEATURE: SITZUNGS-COUNTDOWN IM HEADER
--------------------------------------------------------------------------------
- Countdown-Timer im Header zeigt verbleibende Sitzungszeit an
- Format: MM:SS (z.B. 09:45)
- Timer startet immer bei 10:00 Minuten
- Farbliche Warnung bei < 60 Sekunden (orange)
- Kritische Warnung bei < 30 Sekunden (rot, pulsierend)
- Automatischer Logout bei Ablauf mit Toast-Benachrichtigung
FEATURE: INTERAKTIONS-BASIERTER SESSION-REFRESH
--------------------------------------------------------------------------------
- Bei jedem Klick oder Tastendruck wird die Session automatisch verlängert
- Timer wird auf 10:00 zurückgesetzt bei Interaktion
- Debouncing verhindert zu viele Server-Anfragen (max. 1 pro Sekunde)
- Bei Browser-Aktualisierung (F5) wird Session automatisch refreshed
ÄNDERUNG: SITZUNGSZEIT AUF 10 MINUTEN REDUZIERT
--------------------------------------------------------------------------------
- SESSION_TIMEOUT in .env von 30 auf 10 Minuten geändert
- Erhöhte Sicherheit durch kürzere Sitzungsdauer
ÄNDERUNGEN
--------------------------------------------------------------------------------
- .env: SESSION_TIMEOUT=10
- frontend/index.html: Session-Timer-Element im Header hinzugefügt
- frontend/js/auth.js:
* SessionTimerHandler-Klasse für Countdown-Logik
* JWT-Token-Parsing für Ablaufzeit
* Automatischer Logout bei Session-Ablauf
* Interaktions-basiertes Token-Refresh (Click/Keydown Events)
* initFromExistingSession() für korrekten Timer bei Seiten-Reload
- frontend/js/api.js: X-New-Token-Header verarbeiten und Event dispatchen
- frontend/css/board.css:
* .session-timer Styles
* .warning und .critical Zustände
* pulse-critical Animation
- frontend/sw.js: Cache-Version auf 128 erhöht
================================================================================
30.12.2025 - Kalender: Dots nach Statusspalte gruppiert
================================================================================
FEATURE: AUFGABEN-DOTS IM WOCHENSTREIFEN NACH SPALTE GRUPPIERT
--------------------------------------------------------------------------------
- Im Wochenstreifen-Kalender (Board-Ansicht) wird jetzt nur ein Kreis pro
Statusspalte angezeigt statt einem Kreis pro Aufgabe
- Bei Mouseover werden alle Aufgaben dieser Spalte im Tooltip aufgelistet
- Tooltip zeigt: Spaltenname, Anzahl Aufgaben, Liste mit Start/Ende-Info
- Kreis-Typ basiert auf enthaltenen Aufgaben:
* Offener Kreis = alle Aufgaben haben nur Startdatum
* Gefüllter Kreis = alle Aufgaben haben nur Enddatum
* Kreis mit Ring = gemischt (Start und Ende)
- Klick auf Kreis öffnet die erste Aufgabe der Gruppe
ÄNDERUNGEN
--------------------------------------------------------------------------------
- frontend/js/board.js:
* getTasksForDay() gruppiert Aufgaben nach Spalte (Map statt Array)
* renderDayDots() rendert einen Dot pro Spalte mit data-task-ids
* showTaskTooltip() zeigt alle Aufgaben der Spalte im Tooltip
* openTaskFromDot() öffnet erste Aufgabe aus der Gruppe
- frontend/css/board.css:
* Neue Tooltip-Stile für gruppierte Aufgaben
* .week-strip-tooltip-header, -column-name, -count, -task-list, -task, etc.
- frontend/sw.js: Cache-Version auf 126 erhöht
================================================================================
29.12.2025 - Dokumentation aktualisiert
================================================================================

Datei anzeigen

@ -1,10 +1,17 @@
# TaskMate - Projektanweisungen
## Infrastruktur/Server
- **Docker-Container**: `taskmate` (hauptsächlich Backend), läuft auf Port 3001 intern → 3000 im Container
- **Frontend-Domain**: https://taskmate.aegis-sight.de
- **Gitea-Repository**: https://gitea-undso.aegis-sight.de/AegisSight/TaskMate
- **Gitea-Token**: `7c62fea51bfe0506a25131bd50ac710ac5aa7e3a9dca37a962e7822bdc7db840`
- **Projektverzeichnis auf Server**: `/home/claude-dev/TaskMate`
## Allgemein
- Sprache: Deutsch fuer Benutzer-Kommunikation
- Aenderungen immer in CHANGELOG.txt dokumentieren nach bisher bekannten Schema in der Datei
- Sprache: Deutsch für Benutzer-Kommunikation
- Änderungen immer in CHANGELOG.txt dokumentieren nach bisher bekanntem Schema in der Datei
- Beim Start ANWENDUNGSBESCHREIBUNG.txt lesen
- Cache-Version in frontend/sw.js erhoehen nach Aenderungen
- Cache-Version in frontend/sw.js erhöhen nach Änderungen
- Ich bin kein Mensch mit Fachwissen im Bereich Coding, daher musst du sämtliche Aufgaben in der Regel übernehmen.
## Technologie
@ -50,6 +57,4 @@
## Berechtigungen/Aktionen
- Du sollst den Dockercontainer eigenständig - sofern erforderlich - neu starten/neu bauen, dass Änderungen wirksam werden
- Nach Änderungen immer nach dem Neubau des Dockercontainers oder allgemein ohne Neustart des Dockercontainers, den Browser im Inkognito-Modus starten mit localhost:3000, um die Änderungen sichtbar zu machen.
- Erreichbarkeit der Anwendung über https://taskmate.aegis-sight.de (keine automatische Browser-Öffnung)

Datei anzeigen

@ -11,6 +11,9 @@ const logger = require('../utils/logger');
const gitService = require('../services/gitService');
const giteaService = require('../services/giteaService');
// Fester Pfad für Server-Modus (TaskMate Source-Code)
const SERVER_SOURCE_PATH = '/app/taskmate-source';
/**
* Hilfsfunktion: Anwendung für Projekt abrufen
*/
@ -508,4 +511,250 @@ router.post('/rename-branch/:projectId', (req, res) => {
}
});
// ============================================
// SERVER-MODUS ENDPOINTS
// Für direkte Git-Operationen auf dem TaskMate Source-Code
// ============================================
/**
* GET /api/git/server/status
* Git-Status für Server-Dateien abrufen
*/
router.get('/server/status', (req, res) => {
try {
// Prüfe ob der Pfad existiert und ein Git-Repo ist
if (!gitService.isPathAccessible(SERVER_SOURCE_PATH)) {
return res.status(500).json({
success: false,
error: 'Server-Verzeichnis nicht erreichbar'
});
}
if (!gitService.isGitRepository(SERVER_SOURCE_PATH)) {
return res.status(500).json({
success: false,
error: 'Server-Verzeichnis ist kein Git-Repository'
});
}
const result = gitService.getStatus(SERVER_SOURCE_PATH);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Status:', error);
res.status(500).json({ error: 'Serverfehler', details: error.message });
}
});
/**
* GET /api/git/server/branches
* Branches für Server-Dateien abrufen
*/
router.get('/server/branches', (req, res) => {
try {
const result = gitService.getBranches(SERVER_SOURCE_PATH);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Abrufen der Branches:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* GET /api/git/server/commits
* Commit-Historie für Server-Dateien abrufen
*/
router.get('/server/commits', (req, res) => {
try {
const limit = parseInt(req.query.limit) || 20;
const result = gitService.getCommitHistory(SERVER_SOURCE_PATH, limit);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Abrufen der Commits:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* GET /api/git/server/remote
* Remote-URL für Server-Dateien abrufen
*/
router.get('/server/remote', (req, res) => {
try {
const result = gitService.getRemoteUrl(SERVER_SOURCE_PATH);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Abrufen der Remote-URL:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* POST /api/git/server/stage
* Alle Änderungen für Server-Dateien stagen
*/
router.post('/server/stage', (req, res) => {
try {
const result = gitService.stageAll(SERVER_SOURCE_PATH);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Stagen:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* POST /api/git/server/commit
* Commit für Server-Dateien erstellen
*/
router.post('/server/commit', (req, res) => {
try {
const { message, stageAll } = req.body;
if (!message) {
return res.status(400).json({ error: 'Commit-Nachricht ist erforderlich' });
}
// Optional: Alle Änderungen stagen
if (stageAll !== false) {
const stageResult = gitService.stageAll(SERVER_SOURCE_PATH);
if (!stageResult.success) {
return res.json(stageResult);
}
}
// Autor aus eingeloggtem Benutzer
const author = req.user ? {
name: req.user.display_name || req.user.username,
email: req.user.email || `${req.user.username.toLowerCase()}@taskmate.local`
} : null;
const result = gitService.commit(SERVER_SOURCE_PATH, message, author);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Commit:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* POST /api/git/server/push
* Push für Server-Dateien ausführen
*/
router.post('/server/push', (req, res) => {
try {
const { branch, force } = req.body;
// Prüfe ob Remote existiert
if (!gitService.hasRemote(SERVER_SOURCE_PATH)) {
return res.json({
success: false,
error: 'Kein Remote konfiguriert'
});
}
let result;
if (force) {
// Force Push
result = gitService.pushWithUpstream(SERVER_SOURCE_PATH, branch || null, 'origin', true);
} else {
// Normaler Push
result = gitService.pushChanges(SERVER_SOURCE_PATH, { branch });
// Falls Push wegen fehlendem Upstream fehlschlägt, versuche mit -u
if (!result.success && result.error && result.error.includes('no upstream')) {
result = gitService.pushWithUpstream(SERVER_SOURCE_PATH, branch || null);
}
}
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Push:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* POST /api/git/server/pull
* Pull für Server-Dateien ausführen
*/
router.post('/server/pull', (req, res) => {
try {
const { branch } = req.body;
// Fetch zuerst
gitService.fetchRemote(SERVER_SOURCE_PATH);
// Dann Pull
const result = gitService.pullChanges(SERVER_SOURCE_PATH, { branch });
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Pull:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* POST /api/git/server/fetch
* Fetch für Server-Dateien ausführen
*/
router.post('/server/fetch', (req, res) => {
try {
const result = gitService.fetchRemote(SERVER_SOURCE_PATH);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Fetch:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* POST /api/git/server/checkout
* Branch für Server-Dateien wechseln
*/
router.post('/server/checkout', (req, res) => {
try {
const { branch } = req.body;
if (!branch) {
return res.status(400).json({ error: 'Branch ist erforderlich' });
}
const result = gitService.checkoutBranch(SERVER_SOURCE_PATH, branch);
res.json(result);
} catch (error) {
logger.error('[Server-Git] Fehler beim Branch-Wechsel:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
/**
* GET /api/git/server/info
* Grundlegende Infos über Server-Repository
*/
router.get('/server/info', (req, res) => {
try {
const isAccessible = gitService.isPathAccessible(SERVER_SOURCE_PATH);
const isRepo = isAccessible ? gitService.isGitRepository(SERVER_SOURCE_PATH) : false;
const hasRemote = isRepo ? gitService.hasRemote(SERVER_SOURCE_PATH) : false;
let remoteUrl = null;
if (hasRemote) {
const remoteResult = gitService.getRemoteUrl(SERVER_SOURCE_PATH);
remoteUrl = remoteResult.url || null;
}
res.json({
path: SERVER_SOURCE_PATH,
hostPath: '/home/claude-dev/TaskMate',
accessible: isAccessible,
isRepository: isRepo,
hasRemote: hasRemote,
remoteUrl: remoteUrl
});
} catch (error) {
logger.error('[Server-Git] Fehler beim Info-Abruf:', error);
res.status(500).json({ error: 'Serverfehler' });
}
});
module.exports = router;

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Binäre Datei nicht angezeigt.

Datei anzeigen

@ -10,6 +10,7 @@ services:
- ./backups:/app/backups
- ./logs:/app/logs
- ./uploads:/app/uploads
- .:/app/taskmate-source
environment:
- NODE_ENV=production
- JWT_SECRET=${JWT_SECRET}

Datei anzeigen

@ -314,6 +314,48 @@
50% { opacity: 0.5; }
}
/* Session Timer */
.session-timer {
display: flex;
align-items: center;
gap: var(--spacing-1);
padding: var(--spacing-1) var(--spacing-2);
font-size: var(--text-sm);
font-family: var(--font-mono, monospace);
color: var(--text-secondary);
background: var(--bg-tertiary);
border-radius: var(--radius-md);
transition: all var(--transition-fast);
}
.session-timer.hidden {
display: none;
}
.session-timer-icon {
opacity: 0.7;
}
.session-timer.warning {
color: var(--warning);
background: rgba(255, 165, 0, 0.15);
}
.session-timer.warning .session-timer-icon {
opacity: 1;
}
.session-timer.critical {
color: var(--error);
background: rgba(255, 59, 48, 0.15);
animation: pulse-critical 1s ease-in-out infinite;
}
@keyframes pulse-critical {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* User Menu */
.user-menu {
position: relative;
@ -1058,39 +1100,69 @@
pointer-events: none;
}
.week-strip-tooltip-title {
font-weight: var(--font-semibold);
color: var(--text-primary);
margin-bottom: 4px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.week-strip-tooltip-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--spacing-2);
padding-bottom: var(--spacing-2);
margin-bottom: var(--spacing-2);
border-bottom: 1px solid var(--border-default);
border-left: 3px solid;
padding-left: var(--spacing-2);
margin-left: calc(-1 * var(--spacing-3));
}
.week-strip-tooltip-meta {
display: flex;
flex-direction: column;
gap: 2px;
.week-strip-tooltip-column-name {
font-weight: var(--font-semibold);
color: var(--text-primary);
}
.week-strip-tooltip-count {
font-size: var(--text-2xs);
color: var(--text-tertiary);
}
.week-strip-tooltip-type {
.week-strip-tooltip-task-list {
display: flex;
align-items: center;
gap: 4px;
flex-direction: column;
gap: var(--spacing-1);
max-height: 200px;
overflow-y: auto;
}
.week-strip-tooltip-type .dot {
.week-strip-tooltip-task {
display: flex;
align-items: center;
gap: var(--spacing-1);
}
.week-strip-tooltip-task-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.week-strip-tooltip-type .dot.start {
.week-strip-tooltip-task-dot.start {
border: 1.5px solid currentColor;
background: transparent;
}
.week-strip-tooltip-type .dot.end {
.week-strip-tooltip-task-dot.end {
background: currentColor;
}
.week-strip-tooltip-task-title {
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 150px;
}
.week-strip-tooltip-task-type {
color: var(--text-tertiary);
font-size: var(--text-2xs);
flex-shrink: 0;
}

Datei anzeigen

@ -14,6 +14,51 @@
margin: 0 auto;
}
/* =============================================================================
MODE SWITCH
============================================================================= */
.gitea-mode-switch {
display: flex;
gap: var(--spacing-2);
margin-bottom: var(--spacing-4);
padding: var(--spacing-2);
background: var(--bg-tertiary);
border-radius: var(--radius-lg);
}
.gitea-mode-btn {
display: flex;
align-items: center;
gap: var(--spacing-2);
flex: 1;
padding: var(--spacing-3) var(--spacing-4);
background: transparent;
border: none;
border-radius: var(--radius-md);
color: var(--text-secondary);
font-size: var(--text-sm);
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
justify-content: center;
}
.gitea-mode-btn svg {
flex-shrink: 0;
}
.gitea-mode-btn:hover {
color: var(--text-primary);
background: var(--bg-hover);
}
.gitea-mode-btn.active {
background: var(--bg-primary);
color: var(--text-primary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
/* =============================================================================
GITEA SECTIONS
============================================================================= */
@ -641,4 +686,12 @@
.operation-btn {
width: 100%;
}
.gitea-mode-switch {
flex-direction: column;
}
.gitea-mode-btn {
justify-content: center;
}
}

Datei anzeigen

@ -282,6 +282,15 @@
</div>
</div>
<!-- Session Timer -->
<div id="session-timer" class="session-timer" title="Verbleibende Sitzungszeit">
<svg class="session-timer-icon" viewBox="0 0 24 24" width="16" height="16">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/>
<polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span id="session-countdown">--:--</span>
</div>
<!-- User Menu -->
<div class="user-menu">
<button id="user-menu-btn" class="user-avatar">
@ -501,8 +510,108 @@
<!-- Gitea View -->
<div id="view-gitea" class="view view-gitea hidden">
<!-- Kein Projekt ausgewählt -->
<div id="gitea-no-project" class="gitea-empty-state">
<!-- Modus-Schalter -->
<div id="gitea-mode-switch" class="gitea-mode-switch">
<button class="gitea-mode-btn active" data-mode="server">
<svg viewBox="0 0 24 24" width="18" height="18"><rect x="2" y="2" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><rect x="2" y="14" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
Server-Anwendung
</button>
<button class="gitea-mode-btn" data-mode="project">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Projekt-Repository
</button>
</div>
<!-- Server-Modus Ansicht -->
<div id="gitea-server-mode" class="gitea-section">
<!-- Server Repository Info Header -->
<div class="gitea-repo-header">
<div class="repo-info">
<h2 id="server-repo-name">
<svg viewBox="0 0 24 24" width="24" height="24"><rect x="2" y="2" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><rect x="2" y="14" width="20" height="8" rx="2" stroke="currentColor" stroke-width="2" fill="none"/><circle cx="6" cy="6" r="1" fill="currentColor"/><circle cx="6" cy="18" r="1" fill="currentColor"/></svg>
<span>TaskMate Server</span>
</h2>
<a id="server-repo-url" class="repo-url" href="#" target="_blank"></a>
</div>
</div>
<!-- Server-Pfad Anzeige -->
<div class="gitea-local-path">
<span class="path-label">Server-Pfad:</span>
<code id="server-local-path-display">/home/claude-dev/TaskMate</code>
</div>
<!-- Server Git-Status Panel -->
<div id="server-status-section" class="gitea-status-panel">
<div class="status-grid">
<div class="status-item">
<span class="status-label">Branch</span>
<div class="branch-select-group">
<select id="server-branch-select" class="branch-select">
<!-- Branches dynamisch -->
</select>
</div>
</div>
<div class="status-item">
<span class="status-label">Status</span>
<span id="server-status-indicator" class="status-badge">Prüfe...</span>
</div>
<div class="status-item">
<span class="status-label">Änderungen</span>
<span id="server-changes-count" class="changes-count">0</span>
</div>
</div>
</div>
<!-- Server Git-Operationen -->
<div id="server-operations-section" class="gitea-operations-panel">
<h3>Git-Operationen</h3>
<div class="operations-grid">
<button id="btn-server-fetch" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 3v5h5" stroke="currentColor" stroke-width="2" fill="none"/><path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" stroke="currentColor" stroke-width="2" fill="none"/><path d="M21 21v-5h-5" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Fetch
</button>
<button id="btn-server-pull" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 5v14M19 12l-7 7-7-7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Pull
</button>
<button id="btn-server-push" class="btn btn-primary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
Push
</button>
<button id="btn-server-commit" class="btn btn-secondary operation-btn">
<svg viewBox="0 0 24 24" width="18" height="18"><circle cx="12" cy="12" r="4" stroke="currentColor" stroke-width="2" fill="none"/><path d="M12 2v6M12 16v6M2 12h6M16 12h6" stroke="currentColor" stroke-width="2" fill="none"/></svg>
Commit
</button>
</div>
</div>
<!-- Server Änderungen-Liste -->
<div id="server-changes-section" class="gitea-changes-panel hidden">
<h3>Geänderte Dateien</h3>
<div id="server-changes-list" class="changes-list">
<!-- Dynamisch gefüllt -->
</div>
</div>
<!-- Server Commit-Historie -->
<div id="server-commits-section" class="gitea-commits-panel">
<div class="commits-header">
<h3>Letzte Commits</h3>
<button id="btn-server-clear-commits" class="btn btn-small btn-secondary" title="Alle aus Anzeige entfernen">
Alle ausblenden
</button>
</div>
<div id="server-commits-list" class="commits-list">
<!-- Dynamisch gefüllt -->
</div>
</div>
</div>
<!-- Projekt-Modus: Kein Projekt ausgewählt -->
<div id="gitea-no-project" class="gitea-empty-state hidden">
<svg viewBox="0 0 24 24" width="64" height="64"><path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
<h3>Kein Projekt ausgewählt</h3>
<p>Wählen Sie ein Projekt aus, um die Git-Integration zu konfigurieren.</p>

Datei anzeigen

@ -87,6 +87,15 @@ class ApiClient {
this.setCsrfToken(newCsrfToken);
}
// Update auth token if refreshed by server
const newAuthToken = response.headers.get('X-New-Token');
if (newAuthToken) {
this.setToken(newAuthToken);
window.dispatchEvent(new CustomEvent('auth:token-refreshed', {
detail: { token: newAuthToken }
}));
}
// Handle 401 Unauthorized
if (response.status === 401) {
this.setToken(null);
@ -827,6 +836,54 @@ class ApiClient {
async gitRenameBranch(projectId, oldName, newName) {
return this.post(`/git/rename-branch/${projectId}`, { oldName, newName });
}
// =====================
// SERVER GIT ENDPOINTS (Server-Modus)
// =====================
async getServerGitInfo() {
return this.get('/git/server/info');
}
async getServerGitStatus() {
return this.get('/git/server/status');
}
async getServerGitBranches() {
return this.get('/git/server/branches');
}
async getServerGitCommits(limit = 20) {
return this.get(`/git/server/commits?limit=${limit}`);
}
async getServerGitRemote() {
return this.get('/git/server/remote');
}
async serverGitStage() {
return this.post('/git/server/stage', {});
}
async serverGitCommit(message, stageAll = true) {
return this.post('/git/server/commit', { message, stageAll });
}
async serverGitPush(branch = null, force = false) {
return this.post('/git/server/push', { branch, force });
}
async serverGitPull(branch = null) {
return this.post('/git/server/pull', { branch });
}
async serverGitFetch() {
return this.post('/git/server/fetch', {});
}
async serverGitCheckout(branch) {
return this.post('/git/server/checkout', { branch });
}
}
// Custom API Error Class

Datei anzeigen

@ -370,6 +370,258 @@ class UserMenuHandler {
}
}
// Session Timer Handler
class SessionTimerHandler {
constructor(authManager) {
this.auth = authManager;
this.timerElement = null;
this.countdownElement = null;
this.intervalId = null;
this.expiresAt = null;
this.warningThreshold = 60; // Warnung bei 60 Sekunden verbleibend
this.refreshDebounceTimer = null;
this.refreshDebounceDelay = 1000; // 1 Sekunde Debounce
this.isRefreshing = false;
this.isActive = false; // Nur aktiv wenn eingeloggt und Timer läuft
}
init() {
this.timerElement = $('#session-timer');
this.countdownElement = $('#session-countdown');
// Bei Login neu initialisieren
window.addEventListener('auth:login', () => {
// Kurze Verzögerung um sicherzustellen, dass Token gespeichert ist
setTimeout(() => {
this.updateFromToken();
this.start();
this.isActive = true;
}, 100);
});
// Bei Logout stoppen
window.addEventListener('auth:logout', () => {
this.isActive = false;
this.stop();
this.hide();
});
// Bei Interaktionen Session refreshen (mit Debouncing)
this.bindInteractionEvents();
}
// Interaktions-Events binden für Session-Refresh
bindInteractionEvents() {
const refreshOnInteraction = (e) => {
// Nicht refreshen wenn nicht aktiv (nicht eingeloggt oder Timer läuft nicht)
if (!this.isActive) return;
// Nicht refreshen bei Klicks auf Login-Formular
if (e.target.closest('#login-form') || e.target.closest('.login-container')) return;
// Nur refreshen wenn Token existiert
if (!localStorage.getItem('auth_token')) return;
// Debounce: Nur alle X ms refreshen
if (this.refreshDebounceTimer) {
clearTimeout(this.refreshDebounceTimer);
}
this.refreshDebounceTimer = setTimeout(() => {
this.refreshSession();
}, this.refreshDebounceDelay);
};
// Click-Events auf dem gesamten Dokument
document.addEventListener('click', refreshOnInteraction);
// Keyboard-Events
document.addEventListener('keydown', refreshOnInteraction);
}
// Session beim Server refreshen
async refreshSession() {
if (this.isRefreshing) return;
const token = localStorage.getItem('auth_token');
if (!token) return;
this.isRefreshing = true;
try {
const response = await fetch('/api/auth/refresh', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
}
});
if (response.ok) {
const data = await response.json();
if (data.token) {
// Wichtig: api.setToken() verwenden, um den Cache zu aktualisieren
api.setToken(data.token);
this.expiresAt = this.parseToken(data.token);
this.timerElement?.classList.remove('warning', 'critical');
// CSRF-Token auch aktualisieren
if (data.csrfToken) {
api.setCsrfToken(data.csrfToken);
}
}
} else if (response.status === 401) {
// Token ungültig - ausloggen
this.auth.logout();
}
} catch (error) {
console.error('Session refresh error:', error);
} finally {
this.isRefreshing = false;
}
}
// JWT-Token parsen und Ablaufzeit extrahieren
parseToken(token) {
if (!token) return null;
try {
const payload = token.split('.')[1];
const decoded = JSON.parse(atob(payload));
return decoded.exp ? decoded.exp * 1000 : null; // exp ist in Sekunden, wir brauchen ms
} catch (e) {
console.error('Token parsing error:', e);
return null;
}
}
updateFromToken() {
const token = localStorage.getItem('auth_token');
this.expiresAt = this.parseToken(token);
}
// Beim Seiten-Reload aufrufen
async initFromExistingSession() {
const token = localStorage.getItem('auth_token');
if (!token) {
this.hide();
return false;
}
// Prüfen ob Token noch gültig ist
const expiresAt = this.parseToken(token);
if (!expiresAt || expiresAt <= Date.now()) {
// Token abgelaufen
this.hide();
return false;
}
// Session refreshen um neues Token zu bekommen
this.isActive = true; // Aktivieren damit refreshSession funktioniert
await this.refreshSession();
// Timer mit neuem Token starten
this.updateFromToken();
if (this.expiresAt) {
this.start();
return true;
}
this.isActive = false;
return false;
}
start() {
this.stop(); // Bestehenden Timer stoppen
if (!this.expiresAt) {
this.hide();
return;
}
this.show();
this.update(); // Sofort updaten
// Jede Sekunde aktualisieren
this.intervalId = setInterval(() => this.update(), 1000);
}
stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
this.intervalId = null;
}
}
update() {
if (!this.expiresAt || !this.countdownElement) return;
const now = Date.now();
const remaining = Math.max(0, this.expiresAt - now);
const seconds = Math.floor(remaining / 1000);
// Zeit formatieren
const minutes = Math.floor(seconds / 60);
const secs = seconds % 60;
const timeStr = `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
this.countdownElement.textContent = timeStr;
// Warnung bei wenig Zeit
if (seconds <= this.warningThreshold && seconds > 0) {
this.timerElement?.classList.add('warning');
} else {
this.timerElement?.classList.remove('warning');
}
// Kritisch bei < 30 Sekunden
if (seconds <= 30 && seconds > 0) {
this.timerElement?.classList.add('critical');
} else {
this.timerElement?.classList.remove('critical');
}
// Session abgelaufen
if (seconds <= 0) {
this.stop();
this.handleExpired();
}
}
handleExpired() {
// Toast anzeigen
window.dispatchEvent(new CustomEvent('toast:show', {
detail: {
message: 'Sitzung abgelaufen. Bitte erneut anmelden.',
type: 'warning',
duration: 5000
}
}));
// Automatisch ausloggen
this.auth.logout();
}
show() {
this.timerElement?.classList.remove('hidden');
}
hide() {
this.timerElement?.classList.add('hidden');
if (this.countdownElement) {
this.countdownElement.textContent = '--:--';
}
}
// Token wurde erneuert (z.B. durch API-Response mit X-New-Token)
refreshToken(newToken) {
if (newToken) {
// api.setToken() wird bereits in api.js aufgerufen, hier nur Timer aktualisieren
this.expiresAt = this.parseToken(newToken);
this.timerElement?.classList.remove('warning', 'critical');
}
}
}
// Change Password Modal Handler
class ChangePasswordHandler {
constructor(authManager) {
@ -516,12 +768,21 @@ const authManager = new AuthManager();
let loginFormHandler = null;
let userMenuHandler = null;
let changePasswordHandler = null;
let sessionTimerHandler = null;
// Initialize handlers when DOM is ready
function initAuthHandlers() {
async function initAuthHandlers() {
loginFormHandler = new LoginFormHandler(authManager);
userMenuHandler = new UserMenuHandler(authManager);
changePasswordHandler = new ChangePasswordHandler(authManager);
sessionTimerHandler = new SessionTimerHandler(authManager);
sessionTimerHandler.init();
// Bei bestehendem Token: Session refreshen und Timer starten
const token = localStorage.getItem('auth_token');
if (token) {
await sessionTimerHandler.initFromExistingSession();
}
}
// Listen for DOM ready
@ -536,11 +797,17 @@ window.addEventListener('auth:login', () => {
userMenuHandler?.update();
});
// Listen for token refresh event to update session timer
window.addEventListener('auth:token-refreshed', (e) => {
sessionTimerHandler?.refreshToken(e.detail?.token);
});
export {
authManager,
loginFormHandler,
userMenuHandler,
changePasswordHandler
changePasswordHandler,
sessionTimerHandler
};
export default authManager;

Datei anzeigen

@ -1225,7 +1225,9 @@ class BoardManager {
}
getTasksForDay(tasks, dateStr) {
const result = [];
// Gruppiere Aufgaben nach Spalte
const columnGroups = new Map();
const columns = store.get('columns');
tasks.forEach(task => {
const startDate = task.startDate ? task.startDate.split('T')[0] : null;
@ -1235,7 +1237,15 @@ class BoardManager {
const isEnd = dueDate === dateStr;
if (isStart || isEnd) {
result.push({
const columnId = task.columnId;
if (!columnGroups.has(columnId)) {
const column = columns.find(c => c.id === columnId);
columnGroups.set(columnId, {
column,
tasks: []
});
}
columnGroups.get(columnId).tasks.push({
task,
isStart,
isEnd
@ -1243,70 +1253,99 @@ class BoardManager {
}
});
return result;
return columnGroups;
}
renderDayDots(dayTasks, dateStr) {
if (dayTasks.length === 0) return '';
renderDayDots(columnGroups, dateStr) {
if (columnGroups.size === 0) return '';
const columns = store.get('columns');
const users = store.get('users');
const dots = [];
return dayTasks.map(({ task, isStart, isEnd }) => {
const column = columns.find(c => c.id === task.columnId);
columnGroups.forEach(({ column, tasks }, columnId) => {
const color = column?.color || '#6B7280';
const taskIds = tasks.map(t => t.task.id).join(',');
// Bestimme Dot-Typ basierend auf allen Aufgaben der Spalte
const hasStart = tasks.some(t => t.isStart);
const hasEnd = tasks.some(t => t.isEnd);
let typeClass = '';
if (isStart && isEnd) {
if (hasStart && hasEnd) {
typeClass = 'both';
} else if (isStart) {
} else if (hasStart) {
typeClass = 'start';
} else {
typeClass = 'end';
}
return `<span class="week-strip-dot ${typeClass}"
dots.push(`<span class="week-strip-dot ${typeClass}"
style="color: ${color}"
data-task-id="${task.id}"
data-is-start="${isStart}"
data-is-end="${isEnd}"></span>`;
}).join('');
data-column-id="${columnId}"
data-task-ids="${taskIds}"
data-column-name="${this.escapeHtml(column?.name || '')}"></span>`);
});
return dots.join('');
}
showTaskTooltip(event, dot) {
const taskId = parseInt(dot.dataset.taskId);
const isStart = dot.dataset.isStart === 'true';
const isEnd = dot.dataset.isEnd === 'true';
const taskIds = dot.dataset.taskIds ? dot.dataset.taskIds.split(',').map(id => parseInt(id)) : [];
const columnName = dot.dataset.columnName || 'Unbekannt';
const columnId = parseInt(dot.dataset.columnId);
const task = store.get('tasks').find(t => t.id === taskId);
if (!task) return;
if (taskIds.length === 0) return;
const allTasks = store.get('tasks');
const columns = store.get('columns');
const column = columns.find(c => c.id === task.columnId);
const column = columns.find(c => c.id === columnId);
const color = column?.color || '#6B7280';
// Sammle alle Aufgaben mit ihren Start/Ende-Infos
const tasksWithDates = taskIds.map(taskId => {
const task = allTasks.find(t => t.id === taskId);
if (!task) return null;
const dayDate = dot.closest('.week-strip-day')?.dataset.date;
const startDate = task.startDate ? task.startDate.split('T')[0] : null;
const dueDate = task.dueDate ? task.dueDate.split('T')[0] : null;
const isStart = startDate === dayDate;
const isEnd = dueDate === dayDate;
let typeLabel = '';
if (isStart && isEnd) {
typeLabel = 'Start & Ende';
} else if (isStart) {
typeLabel = 'Startdatum';
} else {
typeLabel = 'Enddatum';
typeLabel = 'Start';
} else if (isEnd) {
typeLabel = 'Ende';
}
return { task, typeLabel, isStart, isEnd };
}).filter(Boolean);
if (tasksWithDates.length === 0) return;
// Create tooltip
this.hideTooltip();
const tooltip = document.createElement('div');
tooltip.className = 'week-strip-tooltip';
tooltip.innerHTML = `
<div class="week-strip-tooltip-title">${this.escapeHtml(task.title)}</div>
<div class="week-strip-tooltip-meta">
<div class="week-strip-tooltip-type" style="color: ${color}">
<span class="dot ${isStart ? 'start' : 'end'}"></span>
${typeLabel}
const taskCount = tasksWithDates.length;
const taskListHtml = tasksWithDates.map(({ task, typeLabel, isStart }) => `
<div class="week-strip-tooltip-task">
<span class="week-strip-tooltip-task-dot ${isStart ? 'start' : 'end'}" style="color: ${color}"></span>
<span class="week-strip-tooltip-task-title">${this.escapeHtml(task.title)}</span>
<span class="week-strip-tooltip-task-type">(${typeLabel})</span>
</div>
${task.startDate ? `<div>Start: ${formatDate(task.startDate)}</div>` : ''}
${task.dueDate ? `<div>Ende: ${formatDate(task.dueDate)}</div>` : ''}
`).join('');
tooltip.innerHTML = `
<div class="week-strip-tooltip-header" style="border-left-color: ${color}">
<span class="week-strip-tooltip-column-name">${this.escapeHtml(columnName)}</span>
<span class="week-strip-tooltip-count">${taskCount} Aufgabe${taskCount > 1 ? 'n' : ''}</span>
</div>
<div class="week-strip-tooltip-task-list">
${taskListHtml}
</div>
`;
@ -1342,7 +1381,12 @@ class BoardManager {
openTaskFromDot(event, dot) {
event.stopPropagation();
const taskId = parseInt(dot.dataset.taskId);
const taskIds = dot.dataset.taskIds ? dot.dataset.taskIds.split(',').map(id => parseInt(id)) : [];
if (taskIds.length === 0) return;
// Öffne die erste Aufgabe
const taskId = taskIds[0];
window.dispatchEvent(new CustomEvent('modal:open', {
detail: { modalId: 'task-modal', mode: 'edit', data: { taskId } }

Datei anzeigen

@ -10,16 +10,27 @@ import store from './store.js';
class GiteaManager {
constructor() {
// Gemeinsame Eigenschaften
this.initialized = false;
this.refreshInterval = null;
this.isLoading = false;
this.currentMode = 'server'; // 'server' oder 'project'
// Projekt-Modus Eigenschaften
this.application = null;
this.gitStatus = null;
this.branches = [];
this.commits = [];
this.giteaRepos = [];
this.giteaConnected = false;
this.initialized = false;
this.refreshInterval = null;
this.isLoading = false;
this.hiddenCommits = new Set(); // Ausgeblendete Commits
this.hiddenCommits = new Set();
// Server-Modus Eigenschaften
this.serverInfo = null;
this.serverStatus = null;
this.serverBranches = [];
this.serverCommits = [];
this.hiddenServerCommits = new Set();
}
async init() {
@ -27,19 +38,31 @@ class GiteaManager {
return;
}
// DOM Elements
// DOM Elements - Gemeinsam
this.giteaView = $('#view-gitea');
this.modeSwitch = $('#gitea-mode-switch');
// DOM Elements - Server-Modus
this.serverModeSection = $('#gitea-server-mode');
// DOM Elements - Projekt-Modus
this.noProjectSection = $('#gitea-no-project');
this.configSection = $('#gitea-config-section');
this.mainSection = $('#gitea-main-section');
this.connectionStatus = $('#gitea-connection-status');
this.bindEvents();
this.bindServerEvents();
this.subscribeToStore();
this.initialized = true;
}
bindEvents() {
// Modus-Schalter
$$('.gitea-mode-btn').forEach(btn => {
btn.addEventListener('click', (e) => this.handleModeSwitch(e));
});
// Konfiguration speichern
$('#gitea-config-form')?.addEventListener('submit', (e) => this.handleConfigSave(e));
@ -86,6 +109,425 @@ class GiteaManager {
$('#git-commits-list')?.addEventListener('click', (e) => this.handleCommitListClick(e));
}
bindServerEvents() {
// Server Git-Operationen
$('#btn-server-fetch')?.addEventListener('click', () => this.handleServerFetch());
$('#btn-server-pull')?.addEventListener('click', () => this.handleServerPull());
$('#btn-server-push')?.addEventListener('click', () => this.openServerPushModal());
$('#btn-server-commit')?.addEventListener('click', () => this.openServerCommitModal());
// Server Branch wechseln
$('#server-branch-select')?.addEventListener('change', (e) => this.handleServerBranchChange(e));
// Server Commits ausblenden
$('#btn-server-clear-commits')?.addEventListener('click', () => this.clearAllServerCommits());
$('#server-commits-list')?.addEventListener('click', (e) => this.handleServerCommitListClick(e));
}
// Modus-Wechsel Handler
handleModeSwitch(e) {
const btn = e.currentTarget;
const newMode = btn.dataset.mode;
if (newMode === this.currentMode) return;
// Button-Status aktualisieren
$$('.gitea-mode-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.currentMode = newMode;
this.updateModeView();
}
updateModeView() {
if (this.currentMode === 'server') {
// Server-Modus
this.serverModeSection?.classList.remove('hidden');
this.noProjectSection?.classList.add('hidden');
this.configSection?.classList.add('hidden');
this.mainSection?.classList.add('hidden');
this.loadServerData();
} else {
// Projekt-Modus
this.serverModeSection?.classList.add('hidden');
this.loadApplication();
}
}
// =====================
// SERVER-MODUS METHODEN
// =====================
async loadServerData() {
this.showLoading();
try {
// Server-Info laden
const infoResult = await api.getServerGitInfo();
this.serverInfo = infoResult;
if (infoResult.accessible && infoResult.isRepository) {
// Remote-URL anzeigen
if (infoResult.remoteUrl) {
const repoUrl = $('#server-repo-url');
if (repoUrl) {
// Gitea HTML URL generieren (ohne .git)
const htmlUrl = infoResult.remoteUrl.replace(/\.git$/, '');
repoUrl.href = htmlUrl;
repoUrl.textContent = htmlUrl;
repoUrl.classList.remove('hidden');
}
}
// Git-Daten laden
await this.loadServerGitData();
} else {
this.showToast('Server-Verzeichnis ist kein Git-Repository', 'error');
}
} catch (error) {
console.error('[Gitea Server] Fehler beim Laden:', error);
this.showToast('Fehler beim Laden der Server-Daten', 'error');
}
}
async loadServerGitData() {
try {
const [statusResult, branchesResult, commitsResult] = await Promise.allSettled([
api.getServerGitStatus(),
api.getServerGitBranches(),
api.getServerGitCommits(10)
]);
if (statusResult.status === 'fulfilled') {
this.serverStatus = statusResult.value;
} else {
this.serverStatus = null;
console.error('[Gitea Server] Status-Fehler:', statusResult.reason);
}
if (branchesResult.status === 'fulfilled') {
this.serverBranches = branchesResult.value.branches || [];
} else {
this.serverBranches = [];
}
if (commitsResult.status === 'fulfilled') {
this.serverCommits = commitsResult.value.commits || [];
} else {
this.serverCommits = [];
}
this.renderServerStatus();
this.renderServerBranches();
this.renderServerCommits();
this.renderServerChanges();
} catch (error) {
console.error('[Gitea Server] Git-Daten laden fehlgeschlagen:', error);
}
}
renderServerStatus() {
const statusBadge = $('#server-status-indicator');
const changesCount = $('#server-changes-count');
if (!this.serverStatus) {
if (statusBadge) {
statusBadge.textContent = 'Fehler';
statusBadge.className = 'status-badge error';
}
if (changesCount) changesCount.textContent = '-';
return;
}
if (statusBadge) {
if (!this.serverStatus.success) {
statusBadge.textContent = 'Fehler';
statusBadge.className = 'status-badge error';
} else if (this.serverStatus.isClean) {
statusBadge.textContent = 'Sauber';
statusBadge.className = 'status-badge clean';
} else if (this.serverStatus.hasChanges) {
statusBadge.textContent = 'Geändert';
statusBadge.className = 'status-badge dirty';
} else {
statusBadge.textContent = 'OK';
statusBadge.className = 'status-badge clean';
}
}
const changes = this.serverStatus.changes || [];
if (changesCount) changesCount.textContent = changes.length;
}
renderServerBranches() {
const select = $('#server-branch-select');
if (!select) return;
const currentBranch = this.serverStatus?.branch || 'main';
select.innerHTML = '';
const localBranches = this.serverBranches.filter(b => !b.isRemote);
localBranches.forEach(branch => {
const option = document.createElement('option');
option.value = branch.name;
option.textContent = branch.name;
if (branch.name === currentBranch) {
option.selected = true;
}
select.appendChild(option);
});
}
renderServerCommits() {
const listEl = $('#server-commits-list');
const clearBtn = $('#btn-server-clear-commits');
if (!listEl) return;
const visibleCommits = this.serverCommits.filter(commit => {
const hash = commit.hash || commit.sha || commit.shortHash;
return !this.hiddenServerCommits.has(hash);
});
if (clearBtn) {
clearBtn.style.display = visibleCommits.length > 0 ? '' : 'none';
}
if (visibleCommits.length === 0) {
const message = this.serverCommits.length > 0
? 'Alle Commits ausgeblendet'
: 'Keine Commits gefunden';
listEl.innerHTML = `<div class="gitea-empty-state" style="padding: var(--spacing-4);"><p>${message}</p></div>`;
return;
}
listEl.innerHTML = visibleCommits.map(commit => {
const hash = commit.hash || commit.sha || '';
const shortHash = commit.shortHash || hash.substring(0, 7);
return `
<div class="commit-item" data-hash="${escapeHtml(hash)}">
<span class="commit-hash">${escapeHtml(shortHash)}</span>
<div class="commit-info">
<div class="commit-message">${escapeHtml(commit.message?.split('\n')[0] || '')}</div>
<div class="commit-meta">
<span class="author">${escapeHtml(commit.author)}</span> · ${this.formatDate(commit.date)}
</div>
</div>
<button class="commit-delete" title="Ausblenden" data-hash="${escapeHtml(hash)}">
<svg viewBox="0 0 24 24" width="16" height="16"><path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</button>
</div>
`}).join('');
}
renderServerChanges() {
const changesSection = $('#server-changes-section');
const listEl = $('#server-changes-list');
if (!changesSection || !listEl) return;
const changes = this.serverStatus?.changes || [];
if (changes.length === 0) {
changesSection.classList.add('hidden');
return;
}
changesSection.classList.remove('hidden');
listEl.innerHTML = changes.map(change => {
const statusClass = this.getChangeStatusClass(change.status);
const statusLabel = this.getChangeStatusLabel(change.status);
return `
<div class="change-item">
<span class="change-status ${statusClass}" title="${statusLabel}">${change.status}</span>
<span class="change-file">${escapeHtml(change.file)}</span>
</div>
`;
}).join('');
}
// Server Git-Operationen
async handleServerFetch() {
this.setServerOperationLoading('fetch', true);
try {
const result = await api.serverGitFetch();
if (result.success) {
this.showToast('Fetch erfolgreich', 'success');
await this.loadServerGitData();
} else {
this.showToast(result.error || 'Fetch fehlgeschlagen', 'error');
}
} catch (error) {
this.showToast(error.message || 'Fetch fehlgeschlagen', 'error');
} finally {
this.setServerOperationLoading('fetch', false);
}
}
async handleServerPull() {
this.setServerOperationLoading('pull', true);
try {
const branch = $('#server-branch-select')?.value || null;
const result = await api.serverGitPull(branch);
if (result.success) {
this.showToast('Pull erfolgreich', 'success');
await this.loadServerGitData();
} else {
this.showToast(result.error || 'Pull fehlgeschlagen', 'error');
}
} catch (error) {
this.showToast(error.message || 'Pull fehlgeschlagen', 'error');
} finally {
this.setServerOperationLoading('pull', false);
}
}
openServerPushModal() {
const modal = $('#git-push-modal');
if (!modal) return;
const currentBranch = $('#server-branch-select')?.value || this.serverStatus?.branch || 'main';
$('#push-local-branch').textContent = currentBranch;
$('#push-target-branch').value = '';
$('#push-force').checked = false;
// Temporär Attribut setzen um Server-Modus zu markieren
modal.dataset.serverMode = 'true';
modal.classList.remove('hidden');
modal.classList.add('visible');
$('#modal-overlay')?.classList.remove('hidden');
store.openModal('git-push-modal');
}
async executeServerPush(force = false) {
this.setServerOperationLoading('push', true);
try {
const targetBranch = $('#push-target-branch')?.value || null;
const localBranch = $('#server-branch-select')?.value || this.serverStatus?.branch || 'main';
const result = await api.serverGitPush(targetBranch || localBranch, force);
if (result.success) {
const pushedBranch = result.branch || targetBranch || localBranch;
this.showToast(`Push erfolgreich nach Branch "${pushedBranch}"`, 'success');
await this.loadServerGitData();
} else {
this.showToast(result.error || 'Push fehlgeschlagen', 'error');
}
} catch (error) {
this.showToast(error.message || 'Push fehlgeschlagen', 'error');
} finally {
this.setServerOperationLoading('push', false);
}
}
openServerCommitModal() {
const modal = $('#git-commit-modal');
if (!modal) return;
$('#commit-message').value = '';
$('#commit-stage-all').checked = true;
// Temporär Attribut setzen um Server-Modus zu markieren
modal.dataset.serverMode = 'true';
modal.classList.remove('hidden');
modal.classList.add('visible');
$('#modal-overlay')?.classList.remove('hidden');
store.openModal('git-commit-modal');
}
async executeServerCommit(message, stageAll = true) {
try {
const result = await api.serverGitCommit(message, stageAll);
if (result.success) {
this.showToast('Commit erstellt', 'success');
await this.loadServerGitData();
return true;
} else {
this.showToast(result.error || 'Commit fehlgeschlagen', 'error');
return false;
}
} catch (error) {
this.showToast(error.message || 'Commit fehlgeschlagen', 'error');
return false;
}
}
async handleServerBranchChange(e) {
const branch = e.target.value;
if (!branch) return;
try {
const result = await api.serverGitCheckout(branch);
if (result.success) {
this.showToast(`Gewechselt zu ${branch}`, 'success');
await this.loadServerGitData();
} else {
this.showToast(result.error || 'Branch-Wechsel fehlgeschlagen', 'error');
await this.loadServerGitData();
}
} catch (error) {
this.showToast(error.message || 'Branch-Wechsel fehlgeschlagen', 'error');
await this.loadServerGitData();
}
}
handleServerCommitListClick(e) {
const deleteBtn = e.target.closest('.commit-delete');
if (deleteBtn) {
const hash = deleteBtn.dataset.hash;
if (hash) {
this.hideServerCommit(hash);
}
}
}
hideServerCommit(hash) {
this.hiddenServerCommits.add(hash);
this.renderServerCommits();
this.showToast('Commit ausgeblendet', 'info');
}
clearAllServerCommits() {
this.serverCommits.forEach(commit => {
const hash = commit.hash || commit.sha || commit.shortHash;
if (hash) {
this.hiddenServerCommits.add(hash);
}
});
this.renderServerCommits();
this.showToast('Alle Commits ausgeblendet', 'info');
}
setServerOperationLoading(operation, loading) {
const buttonId = `btn-server-${operation}`;
const button = $(`#${buttonId}`);
if (button) {
button.disabled = loading;
if (loading) {
button.classList.add('loading');
} else {
button.classList.remove('loading');
}
}
}
// =====================
// PROJEKT-MODUS METHODEN
// =====================
subscribeToStore() {
// Bei Projektwechsel neu laden
store.subscribe('currentProjectId', async (projectId) => {
@ -444,16 +886,25 @@ class GiteaManager {
async executePush(e) {
e.preventDefault();
const projectId = store.get('currentProjectId');
if (!projectId) return;
const modal = $('#git-push-modal');
const isServerMode = modal?.dataset.serverMode === 'true';
// Modal schließen
const modal = $('#git-push-modal');
modal?.classList.add('hidden');
modal?.classList.remove('visible');
$('#modal-overlay')?.classList.add('hidden');
store.closeModal();
if (isServerMode) {
// Server-Modus: Push für Server-Dateien
delete modal.dataset.serverMode;
const forcePush = $('#push-force')?.checked || false;
await this.executeServerPush(forcePush);
} else {
// Projekt-Modus: Push für Projekt-Repository
const projectId = store.get('currentProjectId');
if (!projectId) return;
this.setOperationLoading('push', true);
try {
@ -498,6 +949,7 @@ class GiteaManager {
this.setOperationLoading('push', false);
}
}
}
openRenameBranchModal() {
const modal = $('#git-rename-branch-modal');
@ -574,8 +1026,8 @@ class GiteaManager {
async handleCommit(e) {
e.preventDefault();
const projectId = store.get('currentProjectId');
if (!projectId) return;
const modal = $('#git-commit-modal');
const isServerMode = modal?.dataset.serverMode === 'true';
const message = $('#commit-message')?.value.trim();
const stageAll = $('#commit-stage-all')?.checked ?? true;
@ -585,6 +1037,18 @@ class GiteaManager {
return;
}
if (isServerMode) {
// Server-Modus: Commit für Server-Dateien
const success = await this.executeServerCommit(message, stageAll);
if (success) {
this.closeModal('git-commit-modal');
delete modal.dataset.serverMode;
}
} else {
// Projekt-Modus: Commit für Projekt-Repository
const projectId = store.get('currentProjectId');
if (!projectId) return;
try {
const result = await api.gitCommit(projectId, message, stageAll);
@ -599,6 +1063,7 @@ class GiteaManager {
this.showToast(error.message || 'Commit fehlgeschlagen', 'error');
}
}
}
async handleBranchChange(e) {
const projectId = store.get('currentProjectId');
@ -976,7 +1441,7 @@ class GiteaManager {
show() {
this.giteaView?.classList.remove('hidden');
this.giteaView?.classList.add('active');
this.loadApplication();
this.updateModeView();
this.startAutoRefresh();
}
@ -990,7 +1455,13 @@ class GiteaManager {
this.stopAutoRefresh();
// Status alle 30 Sekunden aktualisieren
this.refreshInterval = setInterval(() => {
if (this.application?.configured && !this.isLoading) {
if (this.isLoading) return;
if (this.currentMode === 'server') {
// Server-Modus: Server-Daten aktualisieren
this.loadServerGitData();
} else if (this.application?.configured) {
// Projekt-Modus: Projekt-Daten aktualisieren
this.loadGitData();
}
}, 30000);

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '125';
const CACHE_VERSION = '131';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;

10290
logs/app.log

Datei-Diff unterdrückt, da er zu groß ist Diff laden