Export: Klassifizierung (offen/dienstgebrauch/vertraulich) komplett entfernt
Dieser Commit ist enthalten in:
@@ -680,14 +680,6 @@
|
||||
<label class="export-radio"><input type="radio" name="export-format" value="pdf" checked><span>PDF</span></label>
|
||||
<label class="export-radio"><input type="radio" name="export-format" value="docx"><span>Word (DOCX)</span></label>
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Klassifizierung</label>
|
||||
<select id="export-classification" class="form-control" style="width:100%;padding:8px 12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:13px;">
|
||||
<option value="offen">Offen</option>
|
||||
<option value="dienstgebrauch">Nur für den Dienstgebrauch</option>
|
||||
<option value="vertraulich" selected>Vertraulich</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-export')">Abbrechen</button>
|
||||
|
||||
700
src/static/dashboard.html.bak
Normale Datei
700
src/static/dashboard.html.bak
Normale Datei
@@ -0,0 +1,700 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script>(function(){var t=localStorage.getItem('osint_theme');if(t)document.documentElement.setAttribute('data-theme',t);try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<link rel="apple-touch-icon" href="/static/favicon.svg">
|
||||
<title>AegisSight Monitor</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack.min.css" rel="stylesheet">
|
||||
<link rel="stylesheet" href="/static/vendor/leaflet.css">
|
||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260316k">
|
||||
<style>
|
||||
/* Export Modal Radio */
|
||||
.export-radio { display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:pointer; border-radius:var(--radius-sm); transition:background 0.15s; border:1px solid transparent; margin-bottom:4px; }
|
||||
.export-radio:hover { background:var(--bg-secondary); }
|
||||
.export-radio input[type="radio"] { accent-color:var(--accent); width:16px; height:16px; cursor:pointer; flex-shrink:0; }
|
||||
.export-radio input[type="radio"]:checked ~ span:first-of-type { color:var(--accent); font-weight:600; }
|
||||
.export-radio span:first-of-type { font-size:13px; }
|
||||
.export-radio-desc { font-size:11px; color:var(--text-tertiary); margin-left:auto; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<a href="#main-content" class="skip-link">Zum Hauptinhalt springen</a>
|
||||
<div class="dashboard">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="header-left">
|
||||
<div class="header-logo">Aegis<span>Sight</span> Monitor</div>
|
||||
<h1 class="sr-only">AegisSight Monitor Dashboard</h1>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="theme-switch" id="theme-toggle" onclick="ThemeManager.toggle()" role="switch" aria-checked="true" aria-label="Dark Mode" title="Theme wechseln">
|
||||
<span class="theme-switch-icon theme-switch-sun">☀︎</span>
|
||||
<div class="theme-switch-track">
|
||||
<div class="theme-switch-knob"></div>
|
||||
</div>
|
||||
<span class="theme-switch-icon theme-switch-moon">☽</span>
|
||||
</div>
|
||||
<div class="header-user-info">
|
||||
<button class="header-user-btn" id="header-user-btn" aria-expanded="false" aria-haspopup="true">
|
||||
<span class="header-user" id="header-user"></span>
|
||||
<span class="header-user-chevron" aria-hidden="true">▾</span>
|
||||
</button>
|
||||
<div class="header-user-dropdown" id="header-user-dropdown" role="menu">
|
||||
<div class="header-dropdown-row">
|
||||
<span class="header-dropdown-label">Organisation</span>
|
||||
<span class="header-dropdown-value" id="header-org-name">-</span>
|
||||
</div>
|
||||
<div class="header-dropdown-row">
|
||||
<span class="header-dropdown-label">Lizenz</span>
|
||||
<span class="header-dropdown-value" id="header-license-info">-</span>
|
||||
</div>
|
||||
<div id="credits-section" class="credits-section" style="display: none;">
|
||||
<div class="credits-divider"></div>
|
||||
<div class="credits-label">Credits</div>
|
||||
<div class="credits-bar-container">
|
||||
<div id="credits-bar" class="credits-bar"></div>
|
||||
</div>
|
||||
<div class="credits-info">
|
||||
<span><span id="credits-remaining">0</span> von <span id="credits-total">0</span></span>
|
||||
<span class="credits-percent" id="credits-percent"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-license-warning" id="header-license-warning"></div>
|
||||
<button class="btn btn-secondary btn-small" id="logout-btn">Abmelden</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Sidebar -->
|
||||
<nav class="sidebar" aria-label="Seitenleiste">
|
||||
<div class="sidebar-section">
|
||||
<button class="btn btn-primary btn-full btn-small" id="new-incident-btn" style="margin-bottom:6px;">+ Neuer Fall</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-filter">
|
||||
<button class="sidebar-filter-btn active" data-filter="all" onclick="App.setSidebarFilter('all')" aria-pressed="true">Alle</button>
|
||||
<button class="sidebar-filter-btn" data-filter="mine" onclick="App.setSidebarFilter('mine')" aria-pressed="false">Eigene</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-incidents')" role="button" tabindex="0" aria-expanded="true">
|
||||
<span class="sidebar-chevron" id="chevron-active-incidents" aria-hidden="true">▾</span>
|
||||
Live-Monitoring
|
||||
<span class="sidebar-section-count" id="count-active-incidents"></span>
|
||||
</h2>
|
||||
<div id="active-incidents" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('active-research')" role="button" tabindex="0" aria-expanded="true">
|
||||
<span class="sidebar-chevron" id="chevron-active-research" aria-hidden="true">▾</span>
|
||||
Recherchen
|
||||
<span class="sidebar-section-count" id="count-active-research"></span>
|
||||
</h2>
|
||||
<div id="active-research" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sidebar-section">
|
||||
<h2 class="sidebar-section-title collapsible" onclick="App.toggleSidebarSection('archived-incidents')" role="button" tabindex="0" aria-expanded="false">
|
||||
<span class="sidebar-chevron" id="chevron-archived-incidents" aria-hidden="true">▾</span>
|
||||
Archiv
|
||||
<span class="sidebar-section-count" id="count-archived-incidents"></span>
|
||||
</h2>
|
||||
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
|
||||
</div>
|
||||
<div class="sidebar-sources-link">
|
||||
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()">Quellen verwalten</button>
|
||||
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()">Feedback senden</button>
|
||||
<button class="btn btn-secondary btn-full btn-small" onclick="Tutorial.start()" title="Interaktiven Rundgang starten">Rundgang starten</button>
|
||||
<div class="sidebar-stats-mini">
|
||||
<span id="stat-sources-count">0 Quellen</span> · <span id="stat-articles-count">0 Artikel</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main class="main-content" id="main-content">
|
||||
<div class="empty-state" id="empty-state">
|
||||
<div class="empty-state-icon">☉</div>
|
||||
<div class="empty-state-title">Kein Vorfall ausgewählt</div>
|
||||
<div class="empty-state-text">Erstelle einen neuen Fall oder wähle einen bestehenden aus der Seitenleiste.</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Netzwerkanalyse View (hidden by default) -->
|
||||
|
||||
|
||||
<!-- Lagebild (hidden by default) -->
|
||||
<div id="incident-view" style="display:none;">
|
||||
<!-- Header Strip -->
|
||||
<div class="incident-header-strip" id="incident-header-strip">
|
||||
<div class="incident-header-row0">
|
||||
<span class="incident-type-badge" id="incident-type-badge"></span>
|
||||
<span class="auto-refresh-indicator" id="meta-refresh-mode"></span>
|
||||
</div>
|
||||
<div class="incident-header-row1">
|
||||
<div class="incident-header-left">
|
||||
<h2 class="incident-header-title" id="incident-title"></h2>
|
||||
</div>
|
||||
<div class="incident-header-actions">
|
||||
<button class="btn btn-primary btn-small" id="refresh-btn">Aktualisieren</button>
|
||||
<button class="btn btn-secondary btn-small" id="edit-incident-btn">Bearbeiten</button>
|
||||
<button class="btn btn-secondary btn-small" onclick="App.openExportModal()">Bericht exportieren</button>
|
||||
<button class="btn btn-secondary btn-small" id="archive-incident-btn">Archivieren</button>
|
||||
<button class="btn btn-danger btn-small" id="delete-incident-btn">Löschen</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="incident-header-row2">
|
||||
<div class="incident-header-row2-left">
|
||||
<span class="incident-creator-badge">von <strong id="incident-creator"></strong></span>
|
||||
<span class="intl-badge" id="intl-badge"></span>
|
||||
<span id="incident-description" class="incident-description-text"></span>
|
||||
</div>
|
||||
<div class="incident-header-row2-right">
|
||||
<div class="summary-meta" id="summary-meta">
|
||||
<span id="meta-updated" class="meta-updated-link" role="button" tabindex="0" onclick="App.toggleRefreshHistory()" onkeydown="if(event.key==='Enter')App.toggleRefreshHistory()"></span>
|
||||
</div>
|
||||
<div class="refresh-history-popover" id="refresh-history-popover" style="display:none;">
|
||||
<div class="refresh-history-header">
|
||||
<span class="refresh-history-title">Refresh-Verlauf</span>
|
||||
<button class="refresh-history-close" onclick="App.closeRefreshHistory()">×</button>
|
||||
</div>
|
||||
<div class="refresh-history-list" id="refresh-history-list">
|
||||
<div style="padding:12px;color:var(--text-disabled);font-size:12px;">Lade...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Fortschrittsanzeige -->
|
||||
<div class="progress-bar" id="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" aria-label="Verarbeitungsfortschritt" style="display:none;">
|
||||
<div class="progress-steps">
|
||||
<div class="progress-step" id="step-researching">
|
||||
<div class="progress-step-dot"></div>
|
||||
<span>Recherche</span>
|
||||
</div>
|
||||
<div class="progress-step" id="step-analyzing">
|
||||
<div class="progress-step-dot"></div>
|
||||
<span>Analyse</span>
|
||||
</div>
|
||||
<div class="progress-step" id="step-factchecking">
|
||||
<div class="progress-step-dot"></div>
|
||||
<span>Faktencheck</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" id="progress-fill"></div>
|
||||
</div>
|
||||
<div class="progress-label-container">
|
||||
<span id="progress-label" class="progress-label">Warte auf Start...</span>
|
||||
<span id="progress-timer" class="progress-timer"></span>
|
||||
</div>
|
||||
<button id="progress-cancel-btn" class="progress-cancel-btn" onclick="App.cancelRefresh()">Abbrechen</button>
|
||||
</div>
|
||||
|
||||
<!-- Layout-Toolbar -->
|
||||
<div class="layout-toolbar" id="layout-toolbar" style="display:none;">
|
||||
<div class="layout-toggles">
|
||||
<button class="layout-toggle-btn active" data-tile="lagebild" onclick="LayoutManager.toggleTile('lagebild')" aria-pressed="true">Lagebild</button>
|
||||
<button class="layout-toggle-btn active" data-tile="faktencheck" onclick="LayoutManager.toggleTile('faktencheck')" aria-pressed="true">Faktencheck</button>
|
||||
<button class="layout-toggle-btn active" data-tile="quellen" onclick="LayoutManager.toggleTile('quellen')" aria-pressed="true">Quellen</button>
|
||||
<button class="layout-toggle-btn active" data-tile="timeline" onclick="LayoutManager.toggleTile('timeline')" aria-pressed="true">Timeline</button>
|
||||
<button class="layout-toggle-btn active" data-tile="karte" onclick="LayoutManager.toggleTile('karte')" aria-pressed="true">Karte</button>
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-small" onclick="LayoutManager.reset()">Layout zurücksetzen</button>
|
||||
</div>
|
||||
|
||||
<!-- gridstack Dashboard-Grid -->
|
||||
<div class="grid-stack">
|
||||
<div class="grid-stack-item" gs-id="lagebild" gs-x="0" gs-y="0" gs-w="6" gs-h="4" gs-min-w="4" gs-min-h="4">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="card incident-analysis-summary">
|
||||
<div class="card-header">
|
||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Lagebild', 'summary-content')">Lagebild</div>
|
||||
<span class="lagebild-timestamp" id="lagebild-timestamp"></span>
|
||||
</div>
|
||||
<div id="summary-content">
|
||||
<div id="summary-text" class="summary-text"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-stack-item" gs-id="faktencheck" gs-x="6" gs-y="0" gs-w="6" gs-h="4" gs-min-w="4" gs-min-h="4">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="card incident-analysis-factcheck" id="factcheck-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Faktencheck', 'factcheck-list')">Faktencheck <span class="info-icon" data-tooltip="Gesichert/Bestätigt = durch mehrere unabhängige Quellen belegt. Unbestätigt/Ungeprüft = noch nicht ausreichend verifiziert. Widerlegt/Umstritten = Quellen widersprechen sich."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></div>
|
||||
<div class="fc-filter-bar" id="fc-filters"></div>
|
||||
</div>
|
||||
<div class="factcheck-list" id="factcheck-list">
|
||||
<div class="empty-state" style="padding:20px;">
|
||||
<div class="empty-state-text">Noch keine Fakten geprüft</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-stack-item" gs-id="quellen" gs-x="0" gs-y="4" gs-w="12" gs-h="2" gs-min-w="6" gs-min-h="2">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="card source-overview-card">
|
||||
<div class="card-header source-overview-header-toggle" onclick="App.toggleSourceOverview()" role="button" tabindex="0" aria-expanded="false">
|
||||
<span class="source-overview-chevron" id="source-overview-chevron" title="Aufklappen" aria-hidden="true">▸</span>
|
||||
<div class="card-title clickable">Quellenübersicht</div>
|
||||
<button class="btn btn-secondary btn-small source-detail-btn" onclick="event.stopPropagation(); openContentModal('Quellenübersicht', 'source-overview-content')">Detailansicht</button>
|
||||
</div>
|
||||
<div class="source-overview-subheader" onclick="App.toggleSourceOverview()" role="button">
|
||||
<span class="source-overview-header-stats" id="source-overview-header-stats"></span>
|
||||
</div>
|
||||
<div id="source-overview-content" style="display:none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-stack-item" gs-id="timeline" gs-x="0" gs-y="5" gs-w="12" gs-h="4" gs-min-w="6" gs-min-h="4">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="card timeline-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title clickable" role="button" tabindex="0" onclick="openContentModal('Ereignis-Timeline', 'timeline')">Ereignis-Timeline</div>
|
||||
<div class="ht-controls">
|
||||
<div class="ht-filter-group">
|
||||
<button class="ht-filter-btn active" data-filter="all" onclick="App.setTimelineFilter('all')" aria-pressed="true">Alle</button>
|
||||
<button class="ht-filter-btn" data-filter="articles" onclick="App.setTimelineFilter('articles')" aria-pressed="false">Meldungen</button>
|
||||
<button class="ht-filter-btn" data-filter="snapshots" onclick="App.setTimelineFilter('snapshots')" aria-pressed="false">Lageberichte</button>
|
||||
</div>
|
||||
<span class="ht-count" id="article-count"></span>
|
||||
<div class="ht-range-group">
|
||||
<button class="ht-range-btn" data-range="24h" onclick="App.setTimelineRange('24h')" aria-pressed="false">24h</button>
|
||||
<button class="ht-range-btn" data-range="7d" onclick="App.setTimelineRange('7d')" aria-pressed="false">7T</button>
|
||||
<button class="ht-range-btn active" data-range="all" onclick="App.setTimelineRange('all')" aria-pressed="true">Alles</button>
|
||||
</div>
|
||||
<label for="timeline-search" class="sr-only">Timeline durchsuchen</label>
|
||||
<input type="text" id="timeline-search" class="timeline-filter-input" placeholder="Suche..." oninput="App.debouncedRerenderTimeline()">
|
||||
</div>
|
||||
</div>
|
||||
<div id="timeline" class="ht-timeline-container">
|
||||
<div class="ht-empty">Noch keine Meldungen</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid-stack-item" gs-id="karte" gs-x="0" gs-y="9" gs-w="12" gs-h="8" gs-min-w="6" gs-min-h="3">
|
||||
<div class="grid-stack-item-content">
|
||||
<div class="card map-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">Geografische Verteilung</div>
|
||||
<span class="map-stats" id="map-stats"></span>
|
||||
<div class="card-header-actions">
|
||||
<button class="btn btn-secondary btn-small" id="geoparse-btn" onclick="App.triggerGeoparse()" title="Orte aus Artikeln einlesen">Orte einlesen</button>
|
||||
<button class="btn btn-secondary btn-small map-expand-btn" id="map-expand-btn" onclick="UI.toggleMapFullscreen()" title="Vollbild" aria-label="Karte im Vollbild anzeigen">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-container" id="map-container">
|
||||
<div class="map-empty" id="map-empty">Keine Orte erkannt</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Parkplatz für ausgeblendete Kacheln -->
|
||||
<div id="tile-parking" style="display:none;"></div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Neue Lage -->
|
||||
<div class="modal-overlay" id="modal-new" role="dialog" aria-modal="true" aria-labelledby="modal-new-title">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="modal-new-title">Neuen Fall anlegen</div>
|
||||
<button class="modal-close" onclick="closeModal('modal-new')" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<form id="new-incident-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="inc-title">Titel des Vorfalls</label>
|
||||
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inc-description">Beschreibung / Kontext</label>
|
||||
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inc-type">Art der Lage</label>
|
||||
<select id="inc-type" onchange="toggleTypeDefaults()">
|
||||
<option value="adhoc">Live-Monitoring — Ereignis beobachten</option>
|
||||
<option value="research">Recherche — Thema analysieren</option>
|
||||
</select>
|
||||
<div class="form-hint" id="type-hint">
|
||||
Durchsucht laufend hunderte Nachrichtenquellen nach neuen Meldungen. Empfohlen: Automatische Aktualisierung.
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Quellen</label>
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="inc-international" checked>
|
||||
<span class="toggle-switch"></span>
|
||||
<span class="toggle-text">Internationale Quellen einbeziehen <span class="info-icon tooltip-below" data-tooltip="Aktiviert: Sucht auch in englischsprachigen und internationalen Medien. Deaktiviert: Nur deutschsprachige Quellen."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toggle-group" style="margin-top: 8px;">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="inc-telegram">
|
||||
<span class="toggle-switch"></span>
|
||||
<span class="toggle-text">Telegram-Kanäle einbeziehen <span class="info-icon tooltip-below" data-tooltip="Bezieht OSINT-relevante Telegram-Kanäle als zusätzliche Quelle ein. Kann die Aktualität erhöhen, aber auch unbestätigte Informationen liefern."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></span>
|
||||
</label>
|
||||
</div> </div>
|
||||
<div class="form-group">
|
||||
<label>Sichtbarkeit <span class="info-icon tooltip-below" data-tooltip="Öffentlich: Alle Nutzer der Organisation sehen diese Lage. Privat: Nur für dich sichtbar."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></label>
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="inc-visibility" checked>
|
||||
<span class="toggle-switch"></span>
|
||||
<span class="toggle-text" id="visibility-text">Öffentlich — für alle Nutzer sichtbar</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inc-refresh-mode">Aktualisierung</label>
|
||||
<select id="inc-refresh-mode" onchange="toggleRefreshInterval()">
|
||||
<option value="manual">Manuell</option>
|
||||
<option value="auto">Automatisch</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group conditional-field" id="refresh-interval-field">
|
||||
<label for="inc-refresh-value">Intervall</label>
|
||||
<div class="interval-input-group">
|
||||
<input type="number" id="inc-refresh-value" min="10" value="15">
|
||||
<select id="inc-refresh-unit" onchange="updateIntervalMin()">
|
||||
<option value="1" selected>Minuten</option>
|
||||
<option value="60">Stunden</option>
|
||||
<option value="1440">Tage</option>
|
||||
<option value="10080">Wochen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="inc-retention">Aufbewahrung (Tage) <span class="info-icon tooltip-below" data-tooltip="Nach Ablauf wird die Lage automatisch archiviert. 0 = unbegrenzt aufbewahren."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></label>
|
||||
<input type="number" id="inc-retention" min="0" max="999" value="30" placeholder="0 = Unbegrenzt">
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 8px;">
|
||||
<label>E-Mail-Benachrichtigungen</label>
|
||||
<div class="form-hint" style="margin-bottom: 8px;">Per E-Mail benachrichtigen bei:</div>
|
||||
<div class="toggle-group">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="inc-notify-summary">
|
||||
<span class="toggle-switch"></span>
|
||||
<span class="toggle-text">Neues Lagebild</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toggle-group" style="margin-top: 8px;">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="inc-notify-new-articles">
|
||||
<span class="toggle-switch"></span>
|
||||
<span class="toggle-text">Neue Artikel</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="toggle-group" style="margin-top: 8px;">
|
||||
<label class="toggle-label">
|
||||
<input type="checkbox" id="inc-notify-status-change">
|
||||
<span class="toggle-switch"></span>
|
||||
<span class="toggle-text">Statusänderung Faktencheck</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-new')">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary" id="modal-new-submit">Lage anlegen</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Quellenverwaltung -->
|
||||
<div class="modal-overlay" id="modal-sources" role="dialog" aria-modal="true" aria-labelledby="modal-sources-title">
|
||||
<div class="modal modal-wide">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="modal-sources-title">Quellenverwaltung</div>
|
||||
<button class="modal-close" onclick="closeModal('modal-sources')" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body sources-modal-body">
|
||||
<!-- Stats-Leiste -->
|
||||
<div class="sources-stats-bar" id="sources-stats-bar"></div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="sources-toolbar">
|
||||
<div class="sources-filters">
|
||||
<label for="sources-filter-type" class="sr-only">Quellentyp filtern</label>
|
||||
<select id="sources-filter-type" class="timeline-filter-select" onchange="App.filterSources()">
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="rss_feed">RSS-Feed</option>
|
||||
<option value="web_source">Web-Quelle</option>
|
||||
<option value="telegram_channel">Telegram</option>
|
||||
<option value="excluded">Von mir ausgeschlossen</option>
|
||||
</select>
|
||||
<label for="sources-filter-category" class="sr-only">Kategorie filtern</label>
|
||||
<select id="sources-filter-category" class="timeline-filter-select" onchange="App.filterSources()">
|
||||
<option value="">Alle Kategorien</option>
|
||||
<option value="nachrichtenagentur">Nachrichtenagentur</option>
|
||||
<option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option>
|
||||
<option value="qualitaetszeitung">Qualitätszeitung</option>
|
||||
<option value="behoerde">Behörde</option>
|
||||
<option value="fachmedien">Fachmedien</option>
|
||||
<option value="think-tank">Think Tank</option>
|
||||
<option value="international">International</option>
|
||||
<option value="regional">Regional</option>
|
||||
<option value="boulevard">Boulevard</option>
|
||||
<option value="sonstige">Sonstige</option>
|
||||
</select>
|
||||
<label for="sources-search" class="sr-only">Quellen durchsuchen</label>
|
||||
<input type="text" id="sources-search" class="timeline-filter-input sources-search-input" placeholder="Suche..." oninput="App.filterSources()">
|
||||
</div>
|
||||
<div class="sources-toolbar-actions">
|
||||
<button class="btn btn-primary btn-small" onclick="App.toggleSourceForm()">+ Quelle</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Inline-Formular: Quelle hinzufügen (ein-/ausklappbar) -->
|
||||
<div class="sources-add-form" id="sources-add-form" style="display:none;">
|
||||
<div class="sources-form-row">
|
||||
<div class="form-group flex-1">
|
||||
<label for="src-discover-url">URL oder Domain</label>
|
||||
<input type="text" id="src-discover-url" placeholder="z.B. netzpolitik.org oder t.me/kanalname">
|
||||
</div>
|
||||
<button class="btn btn-secondary btn-small" id="src-discover-btn" onclick="App.discoverSource()">Erkennen</button>
|
||||
</div>
|
||||
|
||||
<!-- Ergebnis-Anzeige (nach Discovery) -->
|
||||
<div id="src-discovery-result" class="sources-discovery-result" style="display:none;">
|
||||
<div class="sources-add-form-grid">
|
||||
<div class="form-group">
|
||||
<label for="src-name">Name</label>
|
||||
<input type="text" id="src-name" placeholder="Wird erkannt...">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="src-category">Kategorie</label>
|
||||
<select id="src-category">
|
||||
<option value="nachrichtenagentur">Nachrichtenagentur</option>
|
||||
<option value="oeffentlich-rechtlich">Öffentlich-Rechtlich</option>
|
||||
<option value="qualitaetszeitung">Qualitätszeitung</option>
|
||||
<option value="behoerde">Behörde</option>
|
||||
<option value="fachmedien">Fachmedien</option>
|
||||
<option value="think-tank">Think Tank</option>
|
||||
<option value="international">International</option>
|
||||
<option value="regional">Regional</option>
|
||||
<option value="boulevard">Boulevard</option>
|
||||
<option value="sonstige" selected>Sonstige</option>
|
||||
<option value="ukraine-russland-krieg">Ukraine-Russland-Krieg</option>
|
||||
<option value="irankonflikt">Irankonflikt</option>
|
||||
<option value="osint-international">OSINT International</option>
|
||||
<option value="extremismus-deutschland">Extremismus Deutschland</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Typ</label>
|
||||
<input type="text" id="src-type-display" class="input-readonly" readonly>
|
||||
<select id="src-type-select" style="display:none">
|
||||
<option value="rss_feed">RSS-Feed</option>
|
||||
<option value="web_source">Web-Quelle</option>
|
||||
<option value="telegram_channel">Telegram-Kanal</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="src-rss-url-group">
|
||||
<label>RSS-Feed URL</label>
|
||||
<input type="text" id="src-rss-url" class="input-readonly" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Domain</label>
|
||||
<input type="text" id="src-domain" class="input-readonly" readonly>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="src-notes">Notizen</label>
|
||||
<input type="text" id="src-notes" placeholder="Optional">
|
||||
</div>
|
||||
</div>
|
||||
<div class="sources-discovery-actions">
|
||||
<button class="btn btn-primary btn-small" onclick="App.saveSource()">Speichern</button>
|
||||
<button class="btn btn-secondary btn-small" onclick="App.toggleSourceForm(false)">Abbrechen</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Quellen-Liste (gruppiert) -->
|
||||
<div class="sources-list" id="sources-list">
|
||||
<div class="empty-state-text" style="padding:var(--sp-3xl);text-align:center;">Lade Quellen...</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Content-Viewer (wiederverwendbar für Lagebild, Faktencheck, Quellenübersicht, Timeline) -->
|
||||
<div class="modal-overlay" id="modal-content-viewer" role="dialog" aria-modal="true" aria-labelledby="content-viewer-title">
|
||||
<div class="modal modal-content-viewer">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="content-viewer-title"></div>
|
||||
<div class="modal-header-extra" id="content-viewer-header-extra"></div>
|
||||
<button class="modal-close" onclick="closeModal('modal-content-viewer')" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<div class="modal-body" id="content-viewer-body"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal: Feedback -->
|
||||
<div class="modal-overlay" id="modal-feedback" role="dialog" aria-modal="true" aria-labelledby="modal-feedback-title">
|
||||
<div class="modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title" id="modal-feedback-title">Feedback senden</div>
|
||||
<button class="modal-close" onclick="closeModal('modal-feedback')" aria-label="Schließen">×</button>
|
||||
</div>
|
||||
<form id="feedback-form">
|
||||
<div class="modal-body">
|
||||
<div class="form-group">
|
||||
<label for="fb-category">Kategorie</label>
|
||||
<select id="fb-category">
|
||||
<option value="bug">Fehlerbericht</option>
|
||||
<option value="feature">Feature-Wunsch</option>
|
||||
<option value="question">Frage</option>
|
||||
<option value="other">Sonstiges</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fb-message">Nachricht</label>
|
||||
<textarea id="fb-message" required aria-required="true" minlength="10" maxlength="5000" rows="6" placeholder="Beschreibe dein Anliegen (mind. 10 Zeichen)..."></textarea>
|
||||
<div class="form-hint"><span id="fb-char-count">0</span> / 5.000 Zeichen</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="fb-files">Bilder anhaengen (optional)</label>
|
||||
<input type="file" id="fb-files" accept="image/jpeg,image/png" multiple style="font-size:13px;">
|
||||
<div class="form-hint">Max. 3 Bilder (JPEG/PNG, je max. 5 MB)</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" onclick="closeModal('modal-feedback')">Abbrechen</button>
|
||||
<button type="submit" class="btn btn-primary" id="fb-submit-btn">Absenden</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat-Assistent Widget -->
|
||||
<button class="chat-toggle-btn" id="chat-toggle-btn" title="Chat-Assistent" aria-label="Chat-Assistent oeffnen">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H5.2L4 17.2V4h16v12z"/></svg>
|
||||
</button>
|
||||
<div class="chat-window" id="chat-window">
|
||||
<div class="chat-header">
|
||||
<span class="chat-header-title">AegisSight Assistent</span>
|
||||
<div class="chat-header-actions">
|
||||
<button class="chat-header-btn chat-reset-btn" id="chat-reset-btn" title="Neuer Chat" aria-label="Neuen Chat starten" style="display:none">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15"><path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="chat-header-btn" id="chat-fullscreen-btn" title="Vollbild" aria-label="Vollbild umschalten">
|
||||
<svg viewBox="0 0 24 24" width="15" height="15"><path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z" fill="currentColor"/></svg>
|
||||
</button>
|
||||
<button class="chat-header-btn chat-header-close" id="chat-close-btn" title="Schließen" aria-label="Chat schließen">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-messages" id="chat-messages"></div>
|
||||
<form class="chat-input-area" id="chat-form" autocomplete="off">
|
||||
<textarea id="chat-input" rows="1" placeholder="Frage stellen..." maxlength="2000"></textarea>
|
||||
<button type="submit" class="chat-send-btn" title="Senden" aria-label="Nachricht senden">
|
||||
<svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Modal: Neue Netzwerkanalyse -->
|
||||
<!-- Tutorial -->
|
||||
<div class="tutorial-overlay" id="tutorial-overlay">
|
||||
<div class="tutorial-spotlight" id="tutorial-spotlight"></div>
|
||||
</div>
|
||||
<div class="tutorial-bubble" id="tutorial-bubble"></div>
|
||||
<div class="tutorial-cursor" id="tutorial-cursor"></div>
|
||||
|
||||
<!-- Toast Container -->
|
||||
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gridstack@12/dist/gridstack-all.js"></script>
|
||||
<script src="/static/vendor/leaflet.js"></script>
|
||||
<script src="/static/vendor/leaflet.markercluster.js"></script>
|
||||
<script src="/static/js/api.js?v=20260316c"></script>
|
||||
<script src="/static/js/ws.js?v=20260316b"></script>
|
||||
<script src="/static/js/components.js?v=20260316d"></script>
|
||||
<script src="/static/js/layout.js?v=20260316b"></script>
|
||||
<script src="/static/js/app.js?v=20260316b"></script>
|
||||
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
||||
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
||||
<script src="/static/js/chat.js?v=20260316i"></script>
|
||||
<script>document.addEventListener("DOMContentLoaded",function(){Chat.init();Tutorial.init()});</script>
|
||||
|
||||
<!-- Map Fullscreen Overlay -->
|
||||
<div class="map-fullscreen-overlay" id="map-fullscreen-overlay">
|
||||
<div class="map-fullscreen-header">
|
||||
<div class="map-fullscreen-title">Geografische Verteilung</div>
|
||||
<span class="map-stats map-fullscreen-stats" id="map-fullscreen-stats"></span>
|
||||
<button class="btn btn-secondary btn-small" onclick="UI.toggleMapFullscreen()" title="Vollbild beenden" aria-label="Vollbild beenden">
|
||||
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="4 14 10 14 10 20"/><polyline points="20 10 14 10 14 4"/><line x1="14" y1="10" x2="21" y2="3"/><line x1="3" y1="21" x2="10" y2="14"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="map-fullscreen-container" id="map-fullscreen-container"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Export Modal -->
|
||||
<div class="modal-overlay" id="modal-export" role="dialog" aria-modal="true">
|
||||
<div class="modal" style="max-width:420px;">
|
||||
<div class="modal-header">
|
||||
<h3>Bericht exportieren</h3>
|
||||
<button class="modal-close" onclick="closeModal('modal-export')">×</button>
|
||||
</div>
|
||||
<div class="modal-body" style="padding:20px;">
|
||||
<div style="margin-bottom:16px;">
|
||||
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Umfang</label>
|
||||
<label class="export-radio"><input type="radio" name="export-scope" value="summary" checked><span>Executive Summary</span><span class="export-radio-desc">1-2 Seiten, Kernpunkte</span></label>
|
||||
<label class="export-radio"><input type="radio" name="export-scope" value="report"><span>Lagebericht</span><span class="export-radio-desc">Lagebild, Faktencheck, Quellen</span></label>
|
||||
<label class="export-radio"><input type="radio" name="export-scope" value="full"><span>Vollständiger Bericht</span><span class="export-radio-desc">+ Timeline, Artikelverzeichnis</span></label>
|
||||
</div>
|
||||
<div style="margin-bottom:16px;">
|
||||
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Format</label>
|
||||
<label class="export-radio"><input type="radio" name="export-format" value="pdf" checked><span>PDF</span></label>
|
||||
<label class="export-radio"><input type="radio" name="export-format" value="docx"><span>Word (DOCX)</span></label>
|
||||
</div>
|
||||
<div style="margin-bottom:8px;">
|
||||
<label style="font-size:11px;text-transform:uppercase;letter-spacing:1px;color:var(--text-secondary);display:block;margin-bottom:8px;">Klassifizierung</label>
|
||||
<select id="export-classification" class="form-control" style="width:100%;padding:8px 12px;background:var(--bg-secondary);border:1px solid var(--border);border-radius:var(--radius-sm);color:var(--text-primary);font-size:13px;">
|
||||
<option value="offen">Offen</option>
|
||||
<option value="dienstgebrauch">Nur für den Dienstgebrauch</option>
|
||||
<option value="vertraulich" selected>Vertraulich</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding:12px 20px;display:flex;justify-content:flex-end;gap:8px;border-top:1px solid var(--border);">
|
||||
<button class="btn btn-secondary" onclick="closeModal('modal-export')">Abbrechen</button>
|
||||
<button class="btn btn-primary" id="export-submit-btn" onclick="App.submitExport()">Exportieren</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -228,9 +228,9 @@ const API = {
|
||||
resetTutorialState() {
|
||||
return this._request('DELETE', '/tutorial/state');
|
||||
},
|
||||
exportReport(id, format, scope, classification) {
|
||||
exportReport(id, format, scope) {
|
||||
const token = localStorage.getItem('osint_token');
|
||||
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}&classification=${classification}`, {
|
||||
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
},
|
||||
|
||||
237
src/static/js/api.js.bak
Normale Datei
237
src/static/js/api.js.bak
Normale Datei
@@ -0,0 +1,237 @@
|
||||
/**
|
||||
* API-Client für den OSINT Lagemonitor.
|
||||
*/
|
||||
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) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 30000);
|
||||
|
||||
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);
|
||||
}
|
||||
throw new Error(detail || `Fehler ${response.status}`);
|
||||
}
|
||||
|
||||
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}`);
|
||||
},
|
||||
|
||||
createIncident(data) {
|
||||
return this._request('POST', '/incidents', data);
|
||||
},
|
||||
|
||||
getRefreshingIncidents() {
|
||||
return this._request('GET', '/incidents/refreshing');
|
||||
},
|
||||
|
||||
getIncident(id) {
|
||||
return this._request('GET', `/incidents/${id}`);
|
||||
},
|
||||
|
||||
updateIncident(id, data) {
|
||||
return this._request('PUT', `/incidents/${id}`, data);
|
||||
},
|
||||
|
||||
deleteIncident(id) {
|
||||
return this._request('DELETE', `/incidents/${id}`);
|
||||
},
|
||||
|
||||
getArticles(incidentId) {
|
||||
return this._request('GET', `/incidents/${incidentId}/articles`);
|
||||
},
|
||||
|
||||
getFactChecks(incidentId) {
|
||||
return this._request('GET', `/incidents/${incidentId}/factchecks`);
|
||||
},
|
||||
|
||||
getSnapshots(incidentId) {
|
||||
return this._request('GET', `/incidents/${incidentId}/snapshots`);
|
||||
},
|
||||
|
||||
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);
|
||||
const qs = query.toString();
|
||||
return this._request('GET', `/sources${qs ? '?' + 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, classification) {
|
||||
const token = localStorage.getItem('osint_token');
|
||||
return fetch(`${this.baseUrl}/incidents/${id}/export?format=${format}&scope=${scope}&classification=${classification}`, {
|
||||
headers: { 'Authorization': `Bearer ${token}` },
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -2140,7 +2140,6 @@ const App = {
|
||||
if (!this.currentIncidentId) return;
|
||||
const scope = document.querySelector('input[name="export-scope"]:checked').value;
|
||||
const format = document.querySelector('input[name="export-format"]:checked').value;
|
||||
const classification = document.getElementById('export-classification').value;
|
||||
|
||||
const btn = document.getElementById('export-submit-btn');
|
||||
const origText = btn.textContent;
|
||||
@@ -2148,7 +2147,7 @@ const App = {
|
||||
btn.textContent = scope === 'summary' ? 'KI generiert Executive Summary...' : 'Wird erstellt...';
|
||||
|
||||
try {
|
||||
const response = await API.exportReport(this.currentIncidentId, format, scope, classification);
|
||||
const response = await API.exportReport(this.currentIncidentId, format, scope);
|
||||
if (!response.ok) {
|
||||
const err = await response.json().catch(() => ({}));
|
||||
throw new Error(err.detail || 'Fehler ' + response.status);
|
||||
|
||||
3298
src/static/js/app.js.bak
Normale Datei
3298
src/static/js/app.js.bak
Normale Datei
Datei-Diff unterdrückt, da er zu groß ist
Diff laden
In neuem Issue referenzieren
Einen Benutzer sperren