/** * API-Client für den OSINT Lagemonitor. */ class ApiError extends Error { constructor(status, detail) { super(detail || `Fehler ${status}`); this.name = 'ApiError'; this.status = status; this.detail = detail; } } const API = { baseUrl: '/api', _getHeaders() { const token = localStorage.getItem('osint_token'); return { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', }; }, async _request(method, path, body = null, externalSignal = null) { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 30000); // Externen Abort weiterleiten an internen Controller if (externalSignal) { externalSignal.addEventListener('abort', () => controller.abort(), { once: true }); } const options = { method, headers: this._getHeaders(), signal: controller.signal, }; if (body) { options.body = JSON.stringify(body); } let response; try { response = await fetch(`${this.baseUrl}${path}`, options); } catch (err) { clearTimeout(timeout); if (err.name === 'AbortError') { throw new Error('Zeitüberschreitung bei der Anfrage'); } throw err; } clearTimeout(timeout); if (response.status === 401) { localStorage.removeItem('osint_token'); localStorage.removeItem('osint_username'); window.location.href = '/'; return; } if (!response.ok) { const data = await response.json().catch(() => ({})); let detail = data.detail; if (Array.isArray(detail)) { detail = detail.map(e => e.msg || JSON.stringify(e)).join('; '); } else if (typeof detail === 'object' && detail !== null) { detail = JSON.stringify(detail); } // Lizenz-Status aus Header auslesen (vom Backend gesetzt bei 403) const licStatus = response.headers.get('X-License-Status'); if (response.status === 403 && licStatus && typeof App !== 'undefined') { if (!App.user) App.user = {}; App.user.read_only = true; App.user.read_only_reason = licStatus; const warningEl = document.getElementById('header-license-warning'); if (warningEl) { let text = 'Nur Lesezugriff'; if (licStatus === 'budget_exceeded') text = 'Token-Budget aufgebraucht – nur Lesezugriff. Bitte Verwaltung kontaktieren.'; else if (licStatus === 'expired') text = 'Lizenz abgelaufen – nur Lesezugriff'; else if (licStatus === 'no_license') text = 'Keine aktive Lizenz – nur Lesezugriff'; else if (licStatus === 'org_disabled') text = 'Organisation deaktiviert – nur Lesezugriff'; warningEl.textContent = text; warningEl.classList.add('visible'); } if (typeof App._updateRefreshButton === 'function') App._updateRefreshButton(false); if (typeof UI !== 'undefined' && UI.showToast) { UI.showToast(detail || 'Lizenz-Beschränkung – nur Lesezugriff', 'error'); } } throw new ApiError(response.status, detail); } if (response.status === 204) return null; return response.json(); }, // Auth getMe() { return this._request('GET', '/auth/me'); }, // Incidents listIncidents(statusFilter = null) { const query = statusFilter ? `?status_filter=${statusFilter}` : ''; return this._request('GET', `/incidents${query}`); }, enhanceDescription(title, description, type, signal = null) { return this._request('POST', '/incidents/enhance-description', { title, description, type }, signal); }, createIncident(data) { return this._request('POST', '/incidents', data); }, getRefreshingIncidents() { return this._request('GET', '/incidents/refreshing'); }, getIncident(id) { return this._request('GET', `/incidents/${id}`); }, getIncidentSources(id) { return this._request('GET', `/incidents/${id}/sources`); }, updateIncident(id, data) { return this._request('PUT', `/incidents/${id}`, data); }, deleteIncident(id) { return this._request('DELETE', `/incidents/${id}`); }, getArticles(incidentId, { limit = 500, offset = 0, search = null } = {}) { const params = new URLSearchParams(); params.set('limit', String(limit)); params.set('offset', String(offset)); if (search) params.set('search', search); return this._request('GET', `/incidents/${incidentId}/articles?${params.toString()}`); }, getArticlesSourcesSummary(incidentId) { return this._request('GET', `/incidents/${incidentId}/articles/sources-summary`); }, getArticlesTimelineBuckets(incidentId, granularity = 'day') { return this._request('GET', `/incidents/${incidentId}/articles/timeline-buckets?granularity=${encodeURIComponent(granularity)}`); }, getFactChecks(incidentId) { return this._request('GET', `/incidents/${incidentId}/factchecks`); }, getPipeline(incidentId) { return this._request('GET', `/incidents/${incidentId}/pipeline`); }, getSnapshots(incidentId) { return this._request('GET', `/incidents/${incidentId}/snapshots`); }, getSnapshot(incidentId, snapshotId) { return this._request('GET', `/incidents/${incidentId}/snapshots/${snapshotId}`); }, searchSnapshots(incidentId, query) { return this._request('GET', `/incidents/${incidentId}/snapshots/search?q=${encodeURIComponent(query)}`); }, getLocations(incidentId) { return this._request('GET', `/incidents/${incidentId}/locations`); }, triggerGeoparse(incidentId) { return this._request('POST', `/incidents/${incidentId}/geoparse`); }, getGeoparseStatus(incidentId) { return this._request('GET', `/incidents/${incidentId}/geoparse-status`); }, refreshIncident(id) { return this._request('POST', `/incidents/${id}/refresh`); }, getRefreshLog(incidentId, limit = 20) { return this._request('GET', `/incidents/${incidentId}/refresh-log?limit=${limit}`); }, // Sources (Quellenverwaltung) listSources(params = {}) { const query = new URLSearchParams(); if (params.source_type) query.set('source_type', params.source_type); if (params.category) query.set('category', params.category); if (params.source_status) query.set('source_status', params.source_status); if (params.political_orientation) query.set('political_orientation', params.political_orientation); if (params.media_type) query.set('media_type', params.media_type); if (params.reliability) query.set('reliability', params.reliability); if (params.alignment) query.set('alignment', params.alignment); if (params.state_affiliated !== undefined && params.state_affiliated !== null) { query.set('state_affiliated', String(params.state_affiliated)); } const qs = query.toString(); return this._request('GET', `/sources${qs ? '?' + qs : ''}`); }, // Sources: Klassifikations-Review (LLM) getClassificationStats() { return this._request('GET', '/sources/classification/stats'); }, getClassificationQueue(limit = 50, minConfidence = 0.0) { const qs = new URLSearchParams({ limit: String(limit), min_confidence: String(minConfidence) }).toString(); return this._request('GET', `/sources/classification/queue?${qs}`); }, approveClassification(id) { return this._request('POST', `/sources/${id}/classification/approve`); }, rejectClassification(id) { return this._request('POST', `/sources/${id}/classification/reject`); }, reclassifySource(id) { return this._request('POST', `/sources/${id}/classification/reclassify`); }, triggerBulkClassify(limit = 50, onlyUnclassified = true) { const qs = new URLSearchParams({ limit: String(limit), only_unclassified: String(onlyUnclassified) }).toString(); return this._request('POST', `/sources/classification/bulk-classify?${qs}`); }, bulkApproveClassifications(minConfidence = 0.85) { const qs = new URLSearchParams({ min_confidence: String(minConfidence) }).toString(); return this._request('POST', `/sources/classification/bulk-approve?${qs}`); }, createSource(data) { return this._request('POST', '/sources', data); }, updateSource(id, data) { return this._request('PUT', `/sources/${id}`, data); }, deleteSource(id) { return this._request('DELETE', `/sources/${id}`); }, getSourceStats() { return this._request('GET', '/sources/stats'); }, discoverMulti(url) { return this._request('POST', '/sources/discover-multi', { url }); }, getMyExclusions() { return this._request('GET', '/sources/my-exclusions'); }, blockDomain(domain, notes) { return this._request('POST', '/sources/block-domain', { domain, notes }); }, unblockDomain(domain) { return this._request('POST', '/sources/unblock-domain', { domain }); }, deleteDomain(domain) { return this._request('DELETE', `/sources/domain/${encodeURIComponent(domain)}`); }, cancelRefresh(id) { return this._request('POST', `/incidents/${id}/cancel-refresh`); }, // Notifications listNotifications(limit = 50) { return this._request('GET', `/notifications?limit=${limit}`); }, markNotificationsRead(ids = null) { return this._request('PUT', '/notifications/mark-read', { notification_ids: ids }); }, // Subscriptions (E-Mail-Benachrichtigungen) getSubscription(incidentId) { return this._request('GET', '/incidents/' + incidentId + '/subscription'); }, updateSubscription(incidentId, data) { return this._request('PUT', '/incidents/' + incidentId + '/subscription', data); }, // Feedback sendFeedback(data) { return this._request('POST', '/feedback', data); }, async sendFeedbackForm(formData) { const token = localStorage.getItem('osint_token'); const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 60000); const resp = await fetch(this.baseUrl + '/feedback', { method: 'POST', headers: { 'Authorization': token ? 'Bearer ' + token : '' }, body: formData, signal: controller.signal, }); clearTimeout(timeout); if (!resp.ok) { const err = await resp.json().catch(() => ({})); throw new Error(err.detail || 'Fehler ' + resp.status); } }, // Export // Tutorial-Fortschritt getTutorialState() { return this._request('GET', '/tutorial/state'); }, saveTutorialState(data) { return this._request('PUT', '/tutorial/state', data); }, resetTutorialState() { return this._request('DELETE', '/tutorial/state'); }, exportReport(id, format, scope, sections) { const token = localStorage.getItem('osint_token'); let url = `${this.baseUrl}/incidents/${id}/export?format=${format}`; if (sections && sections.length > 0) { url += `§ions=${sections.join(',')}`; } else if (scope) { url += `&scope=${scope}`; } return fetch(url, { headers: { 'Authorization': `Bearer ${token}` }, }); }, // --- Global Admin: Org-Wechsel (herausnehmbar) --- listOrganizations() { return this._request('GET', '/auth/organizations'); }, switchOrg(organizationId) { return this._request('POST', '/auth/switch-org', { organization_id: organizationId }); }, };