Implementierung Wissensmanagement

Dieser Commit ist enthalten in:
HG
2025-12-30 22:49:56 +00:00
committet von Server Deploy
Ursprung 9bf298c26b
Commit 395598c2b0
51 geänderte Dateien mit 7598 neuen und 32 gelöschten Zeilen

Datei anzeigen

@ -14,19 +14,23 @@ class ApiClient {
// Token Management
setToken(token) {
console.log('[API] setToken:', token ? token.substring(0, 20) + '...' : 'NULL');
this.token = token;
if (token) {
localStorage.setItem('auth_token', token);
} else {
this.token = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('current_user');
}
}
getToken() {
if (!this.token) {
this.token = localStorage.getItem('auth_token');
}
return this.token;
// IMMER aus localStorage lesen um Synchronisationsprobleme zu vermeiden
// (z.B. wenn Token nach Login gesetzt wird während andere Requests laufen)
const token = localStorage.getItem('auth_token');
this.token = token; // Cache aktualisieren
return token;
}
setCsrfToken(token) {
@ -39,10 +43,10 @@ class ApiClient {
}
getCsrfToken() {
if (!this.csrfToken) {
this.csrfToken = sessionStorage.getItem('csrf_token');
}
return this.csrfToken;
// IMMER aus sessionStorage lesen um Synchronisationsprobleme zu vermeiden
const token = sessionStorage.getItem('csrf_token');
this.csrfToken = token; // Cache aktualisieren
return token;
}
// Base Request Method
@ -56,6 +60,7 @@ class ApiClient {
// Add auth token
const token = this.getToken();
console.log('[API] Request:', endpoint, 'Token:', token ? token.substring(0, 20) + '...' : 'NULL');
if (token) {
headers['Authorization'] = `Bearer ${token}`;
}
@ -98,8 +103,22 @@ class ApiClient {
// Handle 401 Unauthorized
if (response.status === 401) {
this.setToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
// Token der für diesen Request verwendet wurde
const requestToken = token;
const currentToken = localStorage.getItem('auth_token');
console.log('[API] 401 received for:', endpoint);
console.log('[API] Request token:', requestToken ? requestToken.substring(0, 20) + '...' : 'NULL');
console.log('[API] Current token:', currentToken ? currentToken.substring(0, 20) + '...' : 'NULL');
// Nur ausloggen wenn der Token der gleiche ist (kein neuer Login in der Zwischenzeit)
if (!currentToken || currentToken === requestToken) {
console.log('[API] Token invalid, triggering logout');
this.setToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
} else {
console.log('[API] 401 ignored - new login occurred while request was in flight');
}
throw new ApiError('Sitzung abgelaufen', 401);
}
@ -274,7 +293,9 @@ class ApiClient {
// =====================
async login(username, password) {
console.log('[API] login() called');
const response = await this.post('/auth/login', { username, password });
console.log('[API] login() response:', response ? 'OK' : 'NULL', 'token:', response?.token ? 'EXISTS' : 'MISSING');
this.setToken(response.token);
// Store CSRF token from login response
if (response.csrfToken) {
@ -977,6 +998,79 @@ class ApiClient {
xhr.send(formData);
});
}
// =====================
// KNOWLEDGE ENDPOINTS (Wissensmanagement)
// =====================
// Kategorien
async getKnowledgeCategories() {
return this.get('/knowledge/categories');
}
async createKnowledgeCategory(data) {
return this.post('/knowledge/categories', data);
}
async updateKnowledgeCategory(id, data) {
return this.put(`/knowledge/categories/${id}`, data);
}
async deleteKnowledgeCategory(id) {
return this.delete(`/knowledge/categories/${id}`);
}
async updateKnowledgeCategoryPosition(id, newPosition) {
return this.put(`/knowledge/categories/${id}/position`, { newPosition });
}
// Einträge
async getKnowledgeEntries(categoryId = null) {
const params = categoryId ? `?categoryId=${categoryId}` : '';
return this.get(`/knowledge/entries${params}`);
}
async getKnowledgeEntry(id) {
return this.get(`/knowledge/entries/${id}`);
}
async createKnowledgeEntry(data) {
return this.post('/knowledge/entries', data);
}
async updateKnowledgeEntry(id, data) {
return this.put(`/knowledge/entries/${id}`, data);
}
async deleteKnowledgeEntry(id) {
return this.delete(`/knowledge/entries/${id}`);
}
async updateKnowledgeEntryPosition(id, newPosition, newCategoryId = null) {
return this.put(`/knowledge/entries/${id}/position`, { newPosition, newCategoryId });
}
// Anhänge
async getKnowledgeAttachments(entryId) {
return this.get(`/knowledge/attachments/${entryId}`);
}
async uploadKnowledgeAttachment(entryId, file, onProgress) {
return this.uploadFile(`/knowledge/attachments/${entryId}`, file, onProgress);
}
async deleteKnowledgeAttachment(id) {
return this.delete(`/knowledge/attachments/${id}`);
}
getKnowledgeAttachmentDownloadUrl(id) {
return `${this.baseUrl}/knowledge/attachments/download/${id}`;
}
// Suche
async searchKnowledge(query) {
return this.get(`/knowledge/search?q=${encodeURIComponent(query)}`);
}
}
// Custom API Error Class

Datei anzeigen

@ -20,6 +20,7 @@ import adminManager from './admin.js';
import proposalsManager from './proposals.js';
import notificationManager from './notifications.js';
import giteaManager from './gitea.js';
import knowledgeManager from './knowledge.js';
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
class App {
@ -79,6 +80,9 @@ class App {
// Initialize gitea manager
await giteaManager.init();
// Initialize knowledge manager
await knowledgeManager.init();
// Update UI
this.updateUserMenu();
}
@ -596,6 +600,18 @@ class App {
v.classList.toggle('hidden', !isActive);
});
// Clear search field when switching views
const searchInput = $('#search-input');
if (searchInput && searchInput.value) {
searchInput.value = '';
store.setFilter('search', '');
store.setState({ searchResultIds: [] }, 'CLEAR_SEARCH_RESULTS');
proposalsManager.setSearchQuery('');
knowledgeManager.setSearchQuery('');
$('#search-clear')?.classList.add('hidden');
$('.search-container')?.classList.remove('has-search');
}
// Load proposals when switching to proposals view - reset to active (non-archived)
if (view === 'proposals') {
proposalsManager.resetToActiveView();
@ -607,6 +623,13 @@ class App {
} else {
giteaManager.hide();
}
// Show/hide knowledge manager
if (view === 'knowledge') {
knowledgeManager.show();
} else {
knowledgeManager.hide();
}
}
// =====================
@ -823,8 +846,9 @@ class App {
updateSearchUI('');
searchInput.focus();
// Clear proposals search as well
// Clear view-specific search
proposalsManager.setSearchQuery('');
knowledgeManager.setSearchQuery('');
// Cancel any pending server search
if (searchAbortController) {
@ -897,6 +921,9 @@ class App {
if (currentView === 'proposals') {
// Search proposals only
proposalsManager.setSearchQuery(value);
} else if (currentView === 'knowledge') {
// Search knowledge base
knowledgeManager.setSearchQuery(value);
} else {
// Immediate client-side filtering for tasks
store.setFilter('search', value);

Datei anzeigen

@ -19,11 +19,14 @@ class AuthManager {
// Initialize authentication state
async init() {
const token = api.getToken();
console.log('[Auth] init() - Token exists:', !!token);
if (token) {
try {
// Verify token by making a request
console.log('[Auth] Verifying token...');
const users = await api.getUsers();
console.log('[Auth] Token valid, users loaded');
this.isAuthenticated = true;
// Get current user from stored data
@ -35,11 +38,13 @@ class AuthManager {
return true;
} catch (error) {
// Token invalid
console.log('[Auth] Token invalid, logging out');
this.logout();
return false;
}
}
console.log('[Auth] No token found');
return false;
}
@ -471,8 +476,18 @@ class SessionTimerHandler {
}
}
} else if (response.status === 401) {
// Token ungültig - ausloggen
this.auth.logout();
// Token ungültig - aber nur ausloggen wenn kein neuer Login stattfand
// (Race-Condition: Alter Refresh-Request kann 401 zurückgeben nachdem
// ein neuer Login erfolgreich war)
const currentToken = localStorage.getItem('auth_token');
if (currentToken === token) {
// Gleicher Token, wirklich ungültig, ausloggen
console.log('[Auth] Refresh returned 401, logging out');
this.auth.logout();
} else {
// Token hat sich geändert (neuer Login oder bereits ausgeloggt)
console.log('[Auth] Refresh 401 ignored - token changed (new login occurred)');
}
}
} catch (error) {
console.error('Session refresh error:', error);

1189
frontend/js/knowledge.js Normale Datei

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

Datei anzeigen

@ -21,7 +21,9 @@ class SyncManager {
// Initialize Socket.io connection
async connect() {
if (this.socket?.connected) {
// Verhindere doppelte Verbindungen (auch während des Verbindungsaufbaus)
if (this.socket) {
console.log('[Sync] Socket already exists, skipping connect');
return;
}
@ -102,8 +104,15 @@ class SyncManager {
console.error('[Sync] Socket error:', error);
if (error.type === 'auth') {
// Auth failed, logout
window.dispatchEvent(new CustomEvent('auth:logout'));
// Nur ausloggen wenn wir wirklich nicht eingeloggt sind
// (verhindert Logout durch alte Socket-Verbindungen nach neuem Login)
const currentToken = localStorage.getItem('auth_token');
if (!currentToken) {
console.log('[Sync] Auth error and no token, triggering logout');
window.dispatchEvent(new CustomEvent('auth:logout'));
} else {
console.log('[Sync] Auth error ignored - new login occurred');
}
}
});
@ -546,9 +555,8 @@ class SyncManager {
const syncManager = new SyncManager();
// Listen for auth events
window.addEventListener('auth:login', () => {
syncManager.connect();
});
// Hinweis: syncManager.connect() wird NICHT hier aufgerufen,
// sondern in app.js initializeApp() um doppelte Verbindungen zu vermeiden
window.addEventListener('auth:logout', () => {
syncManager.disconnect();

Datei anzeigen

@ -1321,7 +1321,8 @@ class TaskModalManager {
try {
const projectId = store.get('currentProjectId');
const subtask = await api.createSubtask(projectId, this.taskId, { title });
this.subtasks.push(subtask);
// Neue Subtask an erster Stelle einfügen
this.subtasks.unshift(subtask);
this.renderSubtasks();
input.value = '';
@ -1331,8 +1332,8 @@ class TaskModalManager {
this.showError('Fehler beim Hinzufügen');
}
} else {
// For new tasks, store locally
this.subtasks.push({
// For new tasks, store locally - an erster Stelle
this.subtasks.unshift({
id: generateTempId(),
title,
completed: false
@ -1346,6 +1347,7 @@ class TaskModalManager {
const subtask = this.subtasks.find(s => s.id === subtaskId);
if (!subtask) return;
const wasCompleted = subtask.completed;
subtask.completed = !subtask.completed;
if (this.mode === 'edit' && this.taskId) {
@ -1355,10 +1357,26 @@ class TaskModalManager {
completed: subtask.completed
});
// Wenn abgehakt: ans Ende der Liste verschieben
if (subtask.completed && !wasCompleted) {
const currentIndex = this.subtasks.findIndex(s => s.id === subtaskId);
const lastPosition = this.subtasks.length - 1;
if (currentIndex < lastPosition) {
// Aus aktueller Position entfernen
const [moved] = this.subtasks.splice(currentIndex, 1);
// Ans Ende anfügen
this.subtasks.push(moved);
// API-Call für neue Position
await api.reorderSubtasks(projectId, this.taskId, subtaskId, lastPosition);
}
}
// Update subtask progress in store for immediate board update
this.updateSubtaskProgressInStore();
} catch (error) {
subtask.completed = !subtask.completed;
subtask.completed = wasCompleted;
this.showError('Fehler beim Aktualisieren');
}
}