Datei Upload und Download fix

Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-10 20:54:24 +00:00
committet von Server Deploy
Ursprung 5b1f8b1cfe
Commit 671aaadc26
7 geänderte Dateien mit 186 neuen und 6 gelöschten Zeilen

Datei anzeigen

@ -1,6 +1,63 @@
TASKMATE - CHANGELOG
====================
================================================================================
10.01.2026 - BUGFIX: Download von Dateianhängen funktioniert wieder
================================================================================
## PROBLEM
404-Fehler beim Herunterladen von Dateianhängen aus Aufgaben
## URSACHE
Fehlende /api Präfix in Frontend API-Call
## LÖSUNG
✅ api.js korrigiert: Doppeltes /api Präfix entfernt
✅ downloadFile nutzt bereits baseUrl (/api), daher nur /files/download nötig
✅ Download-Funktionalität wiederhergestellt
✅ Cache-Version erhöht auf 300
================================================================================
10.01.2026 - BUGFIX: Wissenseinträge erstellen - "active" Spalte entfernt
================================================================================
## PROBLEM
500 Internal Server Error beim Erstellen neuer Wissenseinträge
## URSACHE
SQL-Query suchte nach nicht existierender "active" Spalte in users-Tabelle
## LÖSUNG
✅ WHERE-Klausel vereinfacht - nur noch id != ? ohne active = 1
✅ backend/routes/knowledge.js korrigiert
================================================================================
10.01.2026 - Feature: Benachrichtigungen bei neuen Wissenseinträgen
================================================================================
## NEUE FUNKTION
Alle Nutzer werden benachrichtigt, wenn ein neuer Wissenseintrag erstellt wird
## IMPLEMENTIERUNG
- Neuer Benachrichtigungstyp: 'knowledge:new_entry'
- Benachrichtigung zeigt Titel und Kategorie des neuen Eintrags
- Klick auf Benachrichtigung führt direkt zum Eintrag
## DETAILS
- Backend sendet Benachrichtigung an alle aktiven Nutzer (außer Ersteller)
- Socket.io Event 'knowledge:created' für Echtzeit-Updates
- Frontend navigiert zu Wissen-Tab, wählt Kategorie und expandiert Eintrag
- Eintrag wird kurz hervorgehoben (highlight animation)
- Service erweitert um "data" Feld für zusätzliche Informationen
## DATEIEN
✅ backend/routes/knowledge.js - Benachrichtigungen bei neuem Eintrag
✅ backend/services/notificationService.js - Neuer Typ, data-Feld Support
✅ frontend/js/notifications.js - Icon und Click-Handler für Knowledge
✅ frontend/js/app.js - Navigation zu Wissenseinträgen
✅ frontend/css/knowledge.css - Highlight-Animation
✅ frontend/sw.js - Cache-Version 298
================================================================================
10.01.2026 - UI: Favicon ohne schwarzen Hintergrund
================================================================================

Datei anzeigen

@ -12,6 +12,7 @@ const multer = require('multer');
const { getDb } = require('../database');
const logger = require('../utils/logger');
const { validators, stripHtml } = require('../middleware/validation');
const notificationService = require('../services/notificationService');
// Upload-Konfiguration für Knowledge-Anhänge
const UPLOAD_DIR = path.join(__dirname, '..', 'uploads', 'knowledge');
@ -498,6 +499,53 @@ router.post('/entries', (req, res) => {
logger.info(`Knowledge-Eintrag erstellt: ${title}`);
// Benachrichtigung an alle Nutzer senden
const io = req.app.get('io');
if (io) {
// Alle Nutzer abrufen (außer dem Ersteller)
const users = db.prepare(`
SELECT id FROM users
WHERE id != ?
`).all(req.user.id);
// Benachrichtigung für jeden Nutzer erstellen
users.forEach(user => {
notificationService.create(
user.id,
'knowledge:new_entry',
{
entryId: entry.id,
entryTitle: entry.title,
categoryName: category.name,
categoryId: category.id,
projectId: null,
actorId: req.user.id
},
io,
false // nicht persistent
);
});
// Socket.io Event für Echtzeit-Update senden
io.emit('knowledge:created', {
entry: {
id: entry.id,
categoryId: entry.category_id,
categoryName: category.name,
categoryColor: category.color,
title: entry.title,
url: entry.url,
notes: entry.notes,
position: entry.position,
attachmentCount: 0,
createdBy: entry.created_by,
creatorName: req.user.display_name,
createdAt: entry.created_at,
updatedAt: entry.updated_at
}
});
}
res.status(201).json({
id: entry.id,
categoryId: entry.category_id,

Datei anzeigen

@ -58,6 +58,10 @@ const NOTIFICATION_TYPES = {
'approval:rejected': {
title: (data) => 'Genehmigung abgelehnt',
message: (data) => `"${data.proposalTitle}" wurde abgelehnt`
},
'knowledge:new_entry': {
title: (data) => 'Neuer Wissenseintrag',
message: (data) => `Neuer Eintrag: "${data.entryTitle}" in ${data.categoryName}`
}
};
@ -84,8 +88,8 @@ const notificationService = {
const message = typeConfig.message(data);
const result = db.prepare(`
INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO notifications (user_id, type, title, message, task_id, project_id, proposal_id, actor_id, is_persistent, data)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
userId,
type,
@ -95,7 +99,8 @@ const notificationService = {
data.projectId || null,
data.proposalId || null,
data.actorId || null,
persistent ? 1 : 0
persistent ? 1 : 0,
JSON.stringify(data) // Zusätzliche Daten als JSON speichern
);
const notification = db.prepare(`
@ -260,6 +265,16 @@ const notificationService = {
* Benachrichtigung formatieren für Frontend
*/
formatNotification(notification) {
// Parse zusätzliche Daten wenn vorhanden
let additionalData = {};
if (notification.data) {
try {
additionalData = JSON.parse(notification.data);
} catch (e) {
// Ignore parse errors
}
}
return {
id: notification.id,
userId: notification.user_id,
@ -274,7 +289,10 @@ const notificationService = {
actorColor: notification.actor_color,
isRead: notification.is_read === 1,
isPersistent: notification.is_persistent === 1,
createdAt: notification.created_at
createdAt: notification.created_at,
// Zusätzliche Daten für Knowledge-Einträge
entryId: additionalData.entryId || null,
categoryId: additionalData.categoryId || null
};
},

Datei anzeigen

@ -278,6 +278,22 @@
margin-top: -2px;
}
.knowledge-entry-item.highlight {
animation: highlightPulse 2s ease-in-out;
}
@keyframes highlightPulse {
0%, 100% {
background-color: transparent;
}
25%, 75% {
background-color: var(--primary-100, rgba(59, 130, 246, 0.1));
}
50% {
background-color: var(--primary-200, rgba(59, 130, 246, 0.2));
}
}
/* Header (immer sichtbar) */
.knowledge-entry-header {
display: flex;

Datei anzeigen

@ -335,6 +335,30 @@ class App {
}
});
// Notification navigation - open knowledge entry from inbox
window.addEventListener('notification:open-knowledge', (e) => {
const { entryId, categoryId } = e.detail;
if (entryId && categoryId) {
// Switch to knowledge view
this.switchView('knowledge');
// Select category and expand entry after view is loaded
setTimeout(async () => {
await knowledgeManager.selectCategory(categoryId);
// Expand the specific entry
knowledgeManager.expandedEntries.add(entryId);
const entryElement = document.querySelector(`[data-entry-id="${entryId}"]`);
if (entryElement) {
entryElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
entryElement.classList.add('highlight');
// Remove highlight after animation
setTimeout(() => {
entryElement.classList.remove('highlight');
}, 2000);
}
}, 300);
}
});
// Online/Offline
window.addEventListener('online', () => this.handleOnline());
window.addEventListener('offline', () => this.handleOffline());

Datei anzeigen

@ -167,7 +167,9 @@ class NotificationManager {
<div class="notification-item ${notification.isRead ? '' : 'unread'} ${notification.isPersistent ? 'persistent' : ''}"
data-id="${notification.id}"
data-task-id="${notification.taskId || ''}"
data-proposal-id="${notification.proposalId || ''}">
data-proposal-id="${notification.proposalId || ''}"
data-entry-id="${notification.entryId || ''}"
data-category-id="${notification.categoryId || ''}">
<div class="notification-type-icon ${iconClass}">
${icon}
</div>
@ -209,6 +211,9 @@ class NotificationManager {
if (type.startsWith('approval:')) {
return 'approval';
}
if (type.startsWith('knowledge:')) {
return 'knowledge';
}
return 'task';
}
@ -246,6 +251,9 @@ class NotificationManager {
if (type.startsWith('approval:rejected')) {
return '<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" fill="none"/><path d="M15 9l-6 6M9 9l6 6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>';
}
if (type.startsWith('knowledge:')) {
return '<svg viewBox="0 0 24 24"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z" stroke="currentColor" stroke-width="2" fill="none" stroke-linejoin="round"/></svg>';
}
// Default
return '<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9" stroke="currentColor" stroke-width="2" fill="none"/></svg>';
}
@ -428,6 +436,8 @@ class NotificationManager {
handleItemClick(item) {
const taskId = item.dataset.taskId;
const proposalId = item.dataset.proposalId;
const entryId = item.dataset.entryId;
const categoryId = item.dataset.categoryId;
if (taskId) {
// Zur Aufgabe navigieren
@ -437,6 +447,13 @@ class NotificationManager {
// Zum Genehmigung-Tab wechseln
this.closeDropdown();
window.dispatchEvent(new CustomEvent('notification:open-proposal', { detail: { proposalId: parseInt(proposalId) } }));
} else if (entryId && categoryId) {
// Zum Wissenseintrag navigieren
this.closeDropdown();
window.dispatchEvent(new CustomEvent('notification:open-knowledge', { detail: {
entryId: parseInt(entryId),
categoryId: parseInt(categoryId)
}}));
}
}

Datei anzeigen

@ -4,7 +4,7 @@
* Offline support and caching
*/
const CACHE_VERSION = '297';
const CACHE_VERSION = '300';
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;