v403: Bugfix Archivierung + Task-Duplizieren
Zwei verwandte Frontend/Backend-Bugs behoben: 1. Archivierte Aufgaben liessen sich nicht wiederherstellen - Task war nie im Frontend-Store (Board laedt nur archived=0) - store.updateTask() ist no-op fuer unbekannte IDs - task:archived Socket-Event hatte keinen Frontend-Handler Fix: Backend emittiert/retourniert vollen Task, Frontend fuegt ihn via store.addTask ein, schliesst archive-modal, neuer sync.js-Handler haelt andere Clients in sync. 2. Dupliziertes Task verlor abgehakte Subtasks - INSERT INTO subtasks liess completed-Spalte weg -> Default 0 - task_assignees wurden ueberhaupt nicht mitkopiert Fix: subtasks-INSERT um completed erweitert, task_assignees analog zu task_labels mitkopiert. CACHE_VERSION 402 -> 403.
Dieser Commit ist enthalten in:
@@ -1,5 +1,52 @@
|
|||||||
TASKMATE - CHANGELOG
|
TASKMATE - CHANGELOG
|
||||||
====================
|
====================
|
||||||
|
================================================================================
|
||||||
|
18.04.2026 - v403 - Bugfix: Archivierung + Task-Duplizieren
|
||||||
|
================================================================================
|
||||||
|
PROBLEM
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
1. Archivierte Aufgaben konnten nicht wiederhergestellt werden: Backend-Update
|
||||||
|
lief durch, aber das Board zeigte den Task nicht zurueck. Grund: Archivierte
|
||||||
|
Tasks sind nie im Frontend-Store (API filtert per Default archived=0), und
|
||||||
|
store.updateTask() ist ein No-op fuer unbekannte IDs. Ausserdem hatte der
|
||||||
|
Socket-Event 'task:archived' keinen Frontend-Handler, d. h. andere Clients
|
||||||
|
und Ansichten (Kalender, Liste, Mobile) blieben out-of-sync.
|
||||||
|
2. Beim Duplizieren gingen abgehakte Subtasks verloren: INSERT kopierte nur
|
||||||
|
(task_id, title, position) - das 'completed'-Feld fiel auf Schema-Default 0.
|
||||||
|
Zusaetzlich wurden task_assignees (Mehrfachzuweisungen) nicht mitkopiert.
|
||||||
|
|
||||||
|
LOESUNG
|
||||||
|
--------------------------------------------------------------------------------
|
||||||
|
Backend (backend/routes/tasks.js):
|
||||||
|
- Duplicate-Route: INSERT INTO subtasks erweitert um Spalte 'completed'
|
||||||
|
- Duplicate-Route: task_assignees analog zu task_labels mitkopieren
|
||||||
|
- Archive-Route: Vollstaendigen Task im Socket-Event mitschicken (task + userId)
|
||||||
|
- Archive-Route: res.json() liefert jetzt auch den aktualisierten Task zurueck
|
||||||
|
|
||||||
|
Frontend (frontend/js/sync.js):
|
||||||
|
- Neuer Socket-Handler 'task:archived': entfernt Task aus Board bei archiv=true,
|
||||||
|
fuegt ihn bei archiv=false via store.addTask/updateTask wieder ein
|
||||||
|
- Eigene UserID wird wie bei anderen Events ignoriert
|
||||||
|
|
||||||
|
Frontend (frontend/js/app.js, task-modal.js):
|
||||||
|
- app.restoreTask + handleRestore + showColumnSelectDialog: nutzen den vom
|
||||||
|
Backend zurueckgegebenen Task, um ihn in den Store einzupflegen statt nur
|
||||||
|
store.updateTask() zu rufen (welches bei nicht vorhandenen Tasks wirkungslos ist)
|
||||||
|
- handleRestore schliesst jetzt zusaetzlich das archive-modal via
|
||||||
|
'modal:close'-Event, damit der Nutzer den wiederhergestellten Task sofort
|
||||||
|
im Board sieht
|
||||||
|
- Neue Helper-Methode applyRestoredTask() im task-modal
|
||||||
|
|
||||||
|
Service Worker:
|
||||||
|
- CACHE_VERSION: 402 -> 403 (erzwingt Neulade der Frontend-Assets)
|
||||||
|
|
||||||
|
Geaenderte Dateien:
|
||||||
|
- backend/routes/tasks.js
|
||||||
|
- frontend/js/sync.js
|
||||||
|
- frontend/js/app.js
|
||||||
|
- frontend/js/task-modal.js
|
||||||
|
- frontend/sw.js
|
||||||
|
|
||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
19.03.2026 - v402 - Assistent: Umbau auf Claude-Proxy (HTTP/SSE)
|
19.03.2026 - v402 - Assistent: Umbau auf Claude-Proxy (HTTP/SSE)
|
||||||
|
|||||||
@@ -799,10 +799,15 @@ router.post('/:id/duplicate', (req, res) => {
|
|||||||
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
|
const insertLabel = db.prepare('INSERT INTO task_labels (task_id, label_id) VALUES (?, ?)');
|
||||||
taskLabels.forEach(tl => insertLabel.run(newTaskId, tl.label_id));
|
taskLabels.forEach(tl => insertLabel.run(newTaskId, tl.label_id));
|
||||||
|
|
||||||
// Subtasks kopieren
|
// Mehrfachzuweisungen kopieren
|
||||||
|
const assignees = db.prepare('SELECT user_id FROM task_assignees WHERE task_id = ?').all(taskId);
|
||||||
|
const insertAssignee = db.prepare('INSERT INTO task_assignees (task_id, user_id) VALUES (?, ?)');
|
||||||
|
assignees.forEach(a => insertAssignee.run(newTaskId, a.user_id));
|
||||||
|
|
||||||
|
// Subtasks inkl. Abhak-Status kopieren
|
||||||
const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId);
|
const subtasks = db.prepare('SELECT * FROM subtasks WHERE task_id = ? ORDER BY position').all(taskId);
|
||||||
const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, position) VALUES (?, ?, ?)');
|
const insertSubtask = db.prepare('INSERT INTO subtasks (task_id, title, completed, position) VALUES (?, ?, ?, ?)');
|
||||||
subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, idx));
|
subtasks.forEach((st, idx) => insertSubtask.run(newTaskId, st.title, st.completed ? 1 : 0, idx));
|
||||||
|
|
||||||
addHistory(db, newTaskId, req.user.id, 'created', null, null, `Kopie von #${taskId}`);
|
addHistory(db, newTaskId, req.user.id, 'created', null, null, `Kopie von #${taskId}`);
|
||||||
|
|
||||||
@@ -865,16 +870,18 @@ router.put('/:id/archive', (req, res) => {
|
|||||||
|
|
||||||
logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`);
|
logger.info(`Aufgabe ${archived ? 'archiviert' : 'wiederhergestellt'}: ${task.title}`);
|
||||||
|
|
||||||
// WebSocket - vollständige Task-Daten senden
|
// WebSocket - vollständigen Task mitschicken, damit Clients ihn wieder einpflegen können
|
||||||
const io = req.app.get('io');
|
const io = req.app.get('io');
|
||||||
const updatedTask = getFullTask(db, taskId);
|
const updatedTask = getFullTask(db, taskId);
|
||||||
io.to(`project:${task.project_id}`).emit('task:archived', {
|
io.to(`project:${task.project_id}`).emit('task:archived', {
|
||||||
id: taskId,
|
id: taskId,
|
||||||
archived: !!archived,
|
archived: !!archived,
|
||||||
columnId: updatedTask?.columnId
|
columnId: updatedTask?.columnId,
|
||||||
|
task: updatedTask,
|
||||||
|
userId: req.user.id
|
||||||
});
|
});
|
||||||
|
|
||||||
res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt' });
|
res.json({ message: archived ? 'Aufgabe archiviert' : 'Aufgabe wiederhergestellt', task: updatedTask });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Fehler beim Archivieren:', { error: error.message });
|
logger.error('Fehler beim Archivieren:', { error: error.message });
|
||||||
res.status(500).json({ error: 'Interner Serverfehler' });
|
res.status(500).json({ error: 'Interner Serverfehler' });
|
||||||
|
|||||||
@@ -924,8 +924,21 @@ class App {
|
|||||||
async restoreTask(taskId) {
|
async restoreTask(taskId) {
|
||||||
try {
|
try {
|
||||||
const projectId = store.get('currentProjectId');
|
const projectId = store.get('currentProjectId');
|
||||||
await api.restoreTask(projectId, taskId);
|
const response = await api.restoreTask(projectId, taskId);
|
||||||
|
|
||||||
|
// Task in den Store einfuegen (vorher nur in Archivliste, nicht im Store)
|
||||||
|
const restoredTask = response?.task;
|
||||||
|
if (restoredTask) {
|
||||||
|
const existing = store.getTaskById ? store.getTaskById(taskId) : null;
|
||||||
|
if (existing) {
|
||||||
|
store.updateTask(taskId, { ...restoredTask, archived: false });
|
||||||
|
} else {
|
||||||
|
store.addTask({ ...restoredTask, archived: false });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
store.updateTask(taskId, { archived: false });
|
store.updateTask(taskId, { archived: false });
|
||||||
|
}
|
||||||
|
|
||||||
this.showSuccess('Aufgabe wiederhergestellt');
|
this.showSuccess('Aufgabe wiederhergestellt');
|
||||||
|
|
||||||
// Re-render board
|
// Re-render board
|
||||||
|
|||||||
@@ -163,6 +163,10 @@ class SyncManager {
|
|||||||
this.handleTaskMoved(data);
|
this.handleTaskMoved(data);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on('task:archived', (data) => {
|
||||||
|
this.handleTaskArchived(data);
|
||||||
|
});
|
||||||
|
|
||||||
this.socket.on('label:created', (data) => {
|
this.socket.on('label:created', (data) => {
|
||||||
this.handleLabelCreated(data);
|
this.handleLabelCreated(data);
|
||||||
});
|
});
|
||||||
@@ -369,6 +373,27 @@ class SyncManager {
|
|||||||
store.moveTask(taskId, columnId, position);
|
store.moveTask(taskId, columnId, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleTaskArchived(data) {
|
||||||
|
const { id, archived, task, userId } = data;
|
||||||
|
|
||||||
|
if (userId && userId === store.get('currentUser')?.id) return;
|
||||||
|
|
||||||
|
if (archived) {
|
||||||
|
// Archivieren: aus Board-Sicht entfernen (Task bleibt in DB)
|
||||||
|
store.removeTask(id);
|
||||||
|
} else {
|
||||||
|
// Wiederherstellen: Task zurück in den Store bringen
|
||||||
|
if (task) {
|
||||||
|
const existing = store.getTaskById ? store.getTaskById(id) : null;
|
||||||
|
if (existing) {
|
||||||
|
store.updateTask(id, { ...task, archived: false });
|
||||||
|
} else {
|
||||||
|
store.addTask({ ...task, archived: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleLabelCreated(data) {
|
handleLabelCreated(data) {
|
||||||
const { label, userId } = data;
|
const { label, userId } = data;
|
||||||
|
|
||||||
|
|||||||
@@ -588,9 +588,10 @@ class TaskModalManager {
|
|||||||
async handleRestore() {
|
async handleRestore() {
|
||||||
try {
|
try {
|
||||||
const projectId = store.get('currentProjectId');
|
const projectId = store.get('currentProjectId');
|
||||||
await api.restoreTask(projectId, this.taskId);
|
const response = await api.restoreTask(projectId, this.taskId);
|
||||||
store.updateTask(this.taskId, { archived: false });
|
this.applyRestoredTask(response?.task);
|
||||||
this.close();
|
this.close();
|
||||||
|
window.dispatchEvent(new CustomEvent('modal:close', { detail: { modalId: 'archive-modal' } }));
|
||||||
this.showSuccess('Aufgabe wiederhergestellt');
|
this.showSuccess('Aufgabe wiederhergestellt');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Prüfen ob Spaltenauswahl erforderlich ist
|
// Prüfen ob Spaltenauswahl erforderlich ist
|
||||||
@@ -602,6 +603,19 @@ class TaskModalManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applyRestoredTask(restoredTask) {
|
||||||
|
if (restoredTask) {
|
||||||
|
const existing = store.getTaskById ? store.getTaskById(this.taskId) : null;
|
||||||
|
if (existing) {
|
||||||
|
store.updateTask(this.taskId, { ...restoredTask, archived: false });
|
||||||
|
} else {
|
||||||
|
store.addTask({ ...restoredTask, archived: false });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
store.updateTask(this.taskId, { archived: false });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
handleTaskToAssistant() {
|
handleTaskToAssistant() {
|
||||||
if (!this.originalTask) return;
|
if (!this.originalTask) return;
|
||||||
|
|
||||||
@@ -640,9 +654,10 @@ class TaskModalManager {
|
|||||||
onSelect: async (columnId) => {
|
onSelect: async (columnId) => {
|
||||||
try {
|
try {
|
||||||
const projectId = store.get('currentProjectId');
|
const projectId = store.get('currentProjectId');
|
||||||
await api.restoreTask(projectId, this.taskId, columnId);
|
const response = await api.restoreTask(projectId, this.taskId, columnId);
|
||||||
store.updateTask(this.taskId, { archived: false, columnId: columnId });
|
this.applyRestoredTask(response?.task);
|
||||||
this.close();
|
this.close();
|
||||||
|
window.dispatchEvent(new CustomEvent('modal:close', { detail: { modalId: 'archive-modal' } }));
|
||||||
this.showSuccess('Aufgabe wiederhergestellt');
|
this.showSuccess('Aufgabe wiederhergestellt');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.showError('Fehler beim Wiederherstellen');
|
this.showError('Fehler beim Wiederherstellen');
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* Offline support and caching
|
* Offline support and caching
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const CACHE_VERSION = '402';
|
const CACHE_VERSION = '403';
|
||||||
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
|
||||||
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
|
||||||
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren