Dateien
AegisSight-Monitor/src/static/js/api.js
Claude Code 48a60d7579 feat(sources): Review-Queue-UI fuer LLM-Klassifikations-Vorschlaege (Admin)
- Tab-Schalter im Quellen-Modal: "Quellenliste" vs. "Klassifikations-Review"
  (Review-Tab nur fuer org_admin sichtbar, mit Pending-Counter-Badge).
- Review-Karten zeigen Diff aktueller Wert -> LLM-Vorschlag pro Achse,
  Konfidenz-Indikator (gruen/gelb/rot), LLM-Begruendung, Buttons fuer
  Uebernehmen / Verwerfen / Neu klassifizieren.
- Toolbar: Konfidenz-Filter, "Klassifikation starten" (Bulk im Hintergrund),
  "Alle >= 0.85 genehmigen" (Bulk-Approve).
- API-Wrapper in api.js fuer alle 6 neuen Endpoints + erweiterte listSources-Filter.
- Backend-Endpoint POST /api/sources/classification/bulk-approve (Admin-only).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:00:47 +00:00

356 Zeilen
12 KiB
JavaScript

/**
* 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 += `&sections=${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 });
},
};