Gitea:
Push für Serveranwendung in Gitea implementiert
Dieser Commit ist enthalten in:
119
CHANGELOG.txt
119
CHANGELOG.txt
@ -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
|
||||
================================================================================
|
||||
|
||||
65
CLAUDE.md
65
CLAUDE.md
@ -1,25 +1,32 @@
|
||||
# TaskMate - Projektanweisungen
|
||||
# TaskMate - Projektanweisungen
|
||||
|
||||
## Allgemein
|
||||
- Sprache: Deutsch fuer Benutzer-Kommunikation
|
||||
- Aenderungen immer in CHANGELOG.txt dokumentieren nach bisher bekannten Schema in der Datei
|
||||
- Beim Start ANWENDUNGSBESCHREIBUNG.txt lesen
|
||||
- Cache-Version in frontend/sw.js erhoehen nach Aenderungen
|
||||
- Ich bin kein Mensch mit Fachwissen im Bereich Coding, daher musst du sämtliche Aufgaben in der Regel übernehmen.
|
||||
## 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`
|
||||
|
||||
## Technologie
|
||||
- Frontend: Vanilla JavaScript (kein Framework)
|
||||
- Backend: Node.js mit Express
|
||||
- Datenbank: SQLite
|
||||
## Allgemein
|
||||
- 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 erhöhen nach Änderungen
|
||||
- Ich bin kein Mensch mit Fachwissen im Bereich Coding, daher musst du sämtliche Aufgaben in der Regel übernehmen.
|
||||
|
||||
## Konventionen
|
||||
- CSS-Variablen in frontend/css/variables.css
|
||||
- Deutsche Umlaute (ä, ö, ü) in Texten verwenden
|
||||
## Technologie
|
||||
- Frontend: Vanilla JavaScript (kein Framework)
|
||||
- Backend: Node.js mit Express
|
||||
- Datenbank: SQLite
|
||||
|
||||
## Datumsformatierung (WICHTIG)
|
||||
- NIEMALS `toISOString()` für Datumsvergleiche oder -anzeigen verwenden!
|
||||
- `toISOString()` konvertiert in UTC und verursacht Zeitzonenverschiebungen (z.B. 28.12. wird zu 27.12.)
|
||||
- Stattdessen lokale Formatierung verwenden:
|
||||
## Konventionen
|
||||
- CSS-Variablen in frontend/css/variables.css
|
||||
- Deutsche Umlaute (ä, ö, ü) in Texten verwenden
|
||||
|
||||
## Datumsformatierung (WICHTIG)
|
||||
- NIEMALS `toISOString()` für Datumsvergleiche oder -anzeigen verwenden!
|
||||
- `toISOString()` konvertiert in UTC und verursacht Zeitzonenverschiebungen (z.B. 28.12. wird zu 27.12.)
|
||||
- Stattdessen lokale Formatierung verwenden:
|
||||
```javascript
|
||||
// Richtig: Lokale Formatierung
|
||||
const year = date.getFullYear();
|
||||
@ -31,25 +38,23 @@
|
||||
const dateStr = date.toISOString().split('T')[0]; // NICHT VERWENDEN!
|
||||
```
|
||||
|
||||
## Echtzeit-Aktualisierung (KRITISCH)
|
||||
- ALLE Nutzeranpassungen müssen SOFORT und ÜBERALL in der Anwendung sichtbar sein
|
||||
- Der Nutzer darf NIEMALS den Browser aktualisieren müssen (F5), um Änderungen zu sehen
|
||||
- Beispiele für Änderungen, die sofort überall wirken müssen:
|
||||
## Echtzeit-Aktualisierung (KRITISCH)
|
||||
- ALLE Nutzeranpassungen müssen SOFORT und ÜBERALL in der Anwendung sichtbar sein
|
||||
- Der Nutzer darf NIEMALS den Browser aktualisieren müssen (F5), um Änderungen zu sehen
|
||||
- Beispiele für Änderungen, die sofort überall wirken müssen:
|
||||
- Spaltenfarbe ändern → Board, Kalender, Wochenstreifen sofort aktualisieren
|
||||
- Aufgabe erstellen/bearbeiten/löschen → alle Ansichten sofort aktualisieren
|
||||
- Labels, Benutzer, Projekte ändern → überall sofort sichtbar
|
||||
- Technische Umsetzung:
|
||||
- Technische Umsetzung:
|
||||
- `store.subscribe('tasks', callback)` - für Aufgabenänderungen
|
||||
- `store.subscribe('columns', callback)` - für Spaltenänderungen
|
||||
- `store.subscribe('labels', callback)` - für Label-Änderungen
|
||||
- `store.subscribe('users', callback)` - für Benutzeränderungen
|
||||
- `window.addEventListener('app:refresh', callback)` - für allgemeine Aktualisierungen
|
||||
- `window.addEventListener('modal:close', callback)` - nach Modal-Schließung
|
||||
- Bei JEDER neuen Komponente diese Event-Listener einbauen
|
||||
- Bei JEDER Datenänderung prüfen: Welche UI-Bereiche müssen aktualisiert werden?
|
||||
|
||||
## 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.
|
||||
|
||||
- Bei JEDER neuen Komponente diese Event-Listener einbauen
|
||||
- Bei JEDER Datenänderung prüfen: Welche UI-Bereiche müssen aktualisiert werden?
|
||||
|
||||
## Berechtigungen/Aktionen
|
||||
- Du sollst den Dockercontainer eigenständig - sofern erforderlich - neu starten/neu bauen, dass Änderungen wirksam werden
|
||||
- Erreichbarkeit der Anwendung über https://taskmate.aegis-sight.de (keine automatische Browser-Öffnung)
|
||||
|
||||
@ -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.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
BIN
data/taskmate.db
BIN
data/taskmate.db
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
Binäre Datei nicht angezeigt.
@ -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}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 } }
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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
10290
logs/app.log
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
In neuem Issue referenzieren
Einen Benutzer sperren