Promote develop → main (2026-07-25 13:03 UTC) #52

Zusammengeführt
IntelSight_Admin hat 8 Commits von develop nach main 2026-07-25 15:03:06 +02:00 zusammengeführt
7 geänderte Dateien mit 3999 neuen und 0 gelöschten Zeilen
Nur Änderungen aus Commit 4bc842ca1c werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -490,6 +490,18 @@ async def dashboard():
return FileResponse(os.path.join(STATIC_DIR, "dashboard.html"))
@app.get("/studio")
async def studio():
"""Studio-Ansicht (experimentelle 3-Spalten-UI) ausliefern.
Vorerst nur fuer info@aegis-sight.de gedacht. Bearer-Auth greift bei einer
Seiten-Navigation nicht (Token liegt im localStorage, nicht im Cookie), daher
erfolgt das Gating clientseitig: der Header-Button erscheint nur fuer info@,
und studio.js leitet fremde Logins auf /dashboard um.
"""
return FileResponse(os.path.join(STATIC_DIR, "studio.html"))
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=8891)

1363
src/static/css/studio.css Normale Datei

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

Datei anzeigen

@@ -80,6 +80,7 @@
</div>
</div>
<div class="header-license-warning" id="header-license-warning"></div>
<a href="/studio" class="btn btn-secondary btn-small" id="studio-link" style="display:none;" title="Experimentelle Studio-Ansicht">Studio</a>
<button class="btn btn-secondary btn-small" id="logout-btn" data-i18n="header.logout">Abmelden</button>
</div>
</header>

Datei anzeigen

@@ -217,6 +217,78 @@ const API = {
return this._request('GET', `/incidents/${incidentId}/refresh-log?limit=${limit}`);
},
// === Studio: Ereignis-Timeline, RAG-Chat, modulare Bausteine, Uploads, Faktencheck-Verlauf ===
// Backend-Endpunkte folgen phasenweise; fehlende liefern vorerst 404 (studio.js faengt das ab).
getEvents(incidentId, limit = 250) {
return this._request('GET', `/incidents/${incidentId}/events?limit=${encodeURIComponent(limit)}`);
},
// RAG-Chat: inhaltliche Frage über eine konkrete Lage (Studio-UI)
askIncident(incidentId, message, { conversation_id = null, source_filter = null, scope = 'fall' } = {}) {
return this._request('POST', `/incidents/${incidentId}/ask`, {
message,
conversation_id,
source_filter,
scope,
});
},
// Multimodaler Quellen-Ingest (Upload/URL -> Artikel)
createUploads(incidentId, { files = [], url = null } = {}) {
const fd = new FormData();
(files || []).forEach(f => fd.append('files', f));
if (url) fd.append('url', url);
return this.upload(`/incidents/${incidentId}/uploads`, fd);
},
listUploads(incidentId) {
return this._request('GET', `/incidents/${incidentId}/uploads`);
},
deleteUpload(incidentId, uploadId) {
return this._request('DELETE', `/incidents/${incidentId}/uploads/${uploadId}`);
},
async fetchUploadBlobUrl(incidentId, uploadId) {
const token = localStorage.getItem('osint_token');
const headers = {};
if (token) headers['Authorization'] = `Bearer ${token}`;
const res = await fetch(`${this.baseUrl}/incidents/${incidentId}/uploads/${uploadId}/file`, { headers });
if (!res.ok) throw new Error(`Datei konnte nicht geladen werden (${res.status})`);
const blob = await res.blob();
return URL.createObjectURL(blob);
},
// Modulare Pipeline-Bausteine (Studio)
runStage(incidentId, stage) {
return this._request('POST', `/incidents/${incidentId}/run/${stage}`);
},
getRunStatus(incidentId) {
return this._request('GET', `/incidents/${incidentId}/run-status`);
},
// Datenstand je Artefakt (erzeugt? wie alt? wie viele neue Artikel seither?)
getFreshness(incidentId) {
return this._request('GET', `/incidents/${incidentId}/freshness`);
},
// Kann die Websuche gerade Treffer liefern? (online via Claude-WebSearch, Stub)
getSearchStatus() {
return this._request('GET', '/system/search-status');
},
listFactcheckRuns(incidentId) {
return this._request('GET', `/incidents/${incidentId}/factcheck-runs`);
},
getFactcheckRun(incidentId, runId) {
return this._request('GET', `/incidents/${incidentId}/factcheck-runs/${runId}`);
},
// X-Zugänge (Studio; online-Router folgt in spaeterer Phase)
listXAccounts() {
return this._request('GET', '/x/accounts');
},
addXAccount(data) {
return this._request('POST', '/x/accounts', data);
},
deleteXAccount(username) {
return this._request('DELETE', `/x/accounts/${encodeURIComponent(username)}`);
},
// Sources (Quellenverwaltung)
listSources(params = {}) {
const query = new URLSearchParams();

Datei anzeigen

@@ -453,6 +453,12 @@ const App = {
this.user = user;
this._currentUsername = user.email;
// Studio-Button vorerst nur fuer info@aegis-sight.de sichtbar (experimentelle Ansicht)
if (user.email === 'info@aegis-sight.de') {
const studioLink = document.getElementById('studio-link');
if (studioLink) studioLink.style.display = '';
}
// i18n: Sprache anhand der Org laden (default 'de') und DOM uebersetzen
if (window.I18N) {
const targetLang = user.output_language || 'de';

2023
src/static/js/studio.js Normale Datei

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

522
src/static/studio.html Normale Datei
Datei anzeigen

@@ -0,0 +1,522 @@
<!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">
<title>AegisSight Studio</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&family=Inter:wght@400;500;600&display=swap" 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=20260501h">
<link rel="stylesheet" href="/static/css/studio.css?v=20260714b">
</head>
<body>
<div class="studio">
<!-- Kopfzeile -->
<header class="studio-top">
<div class="studio-brand">AegisSight <span>Studio</span></div>
<!-- Die Fall-Auswahl liegt jetzt links im Reiter "Fälle"; hier steht der
Titel des offenen Falls und der Knopf zum Anlegen eines neuen. -->
<button class="studio-btn studio-new-btn" id="new-incident-btn" type="button" onclick="Studio.openNewIncident()" title="Neuen Fall anlegen">
<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" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Neuer Fall
</button>
<span class="studio-incident-title" id="incident-title"></span>
<span class="studio-type-badge" id="type-badge" style="display:none;"></span>
<div class="studio-status" id="live-status">
<span class="mini-spinner"></span>
<span class="studio-status-text" id="live-status-text">Aktualisierung läuft …</span>
<span class="studio-status-timer" id="live-status-timer"></span>
</div>
<div class="studio-top-right">
<button class="studio-theme-toggle" onclick="Studio.toggleTheme()" title="Hell/Dunkel" aria-label="Theme wechseln">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></svg>
</button>
<a href="/dashboard">Klassische Ansicht</a>
</div>
</header>
<!-- 3 Spalten. Die linke Spalte bleibt IMMER sichtbar - ohne sie koennte man
keinen Fall waehlen. Ohne offenen Fall stehen rechts davon nur Hinweise. -->
<main class="studio-cols" id="studio-cols">
<!-- Spalte 1: Quellen -->
<section class="studio-col studio-col-sources">
<!-- Zwei Reiter: die Fall-Auswahl (frueher das Dropdown oben) und die Quellen -->
<div class="left-tabs" role="tablist">
<button class="left-tab" id="lt-cases" role="tab" aria-selected="false" onclick="Studio.leftTab('cases')">
Fälle <span class="lt-count" id="cases-count"></span>
</button>
<button class="left-tab active" id="lt-sources" role="tab" aria-selected="true" onclick="Studio.leftTab('sources')">
Quellen <span class="lt-count" id="sources-count"></span>
</button>
</div>
<!-- Reiter: Fälle -->
<div class="left-pane" id="pane-cases" hidden>
<div class="src-search">
<span class="src-search-icon"><svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
<input type="search" id="case-search-input" placeholder="Fälle durchsuchen …" aria-label="Fälle durchsuchen" oninput="Studio.filterCases(this.value)">
</div>
<!-- Erst im Auswahlmodus: "Alle" nimmt genau das, was der Filter zeigt -->
<div class="col-actions" id="case-select-row" hidden>
<label title="Alle gerade sichtbaren Fälle auswählen">
<input type="checkbox" id="case-select-all" onchange="Studio.selectAllCases(this.checked)"> Alle
</label>
<span class="spacer"></span>
<span class="case-sel-hint" id="case-sel-hint"></span>
</div>
<div class="col-body" id="cases-list"></div>
<!-- Standardleiste: startet den Auswahlmodus (ohne Haken in der Liste) -->
<div class="bulk-bar" id="case-tools">
<div class="bulk-actions">
<button class="btn-bulk" onclick="Studio.startCaseMode('archive')" title="Mehrere Fälle archivieren oder reaktivieren">Archivieren</button>
<button class="btn-bulk danger" onclick="Studio.startCaseMode('delete')" title="Mehrere Fälle löschen">Löschen</button>
</div>
</div>
<!-- Auswahlmodus: Haken sind sichtbar, hier wird bestätigt -->
<div class="bulk-bar" id="bulk-bar" hidden>
<span class="bulk-count" id="bulk-count"></span>
<div class="bulk-actions">
<button class="btn-bulk" id="bulk-archive" onclick="Studio.bulkStatus('archived')">Archivieren</button>
<button class="btn-bulk" id="bulk-activate" onclick="Studio.bulkStatus('active')">Aktivieren</button>
<button class="btn-bulk danger" id="bulk-delete" onclick="Studio.bulkDelete()">Löschen</button>
<button class="btn-bulk ghost" onclick="Studio.endCaseMode()" title="Auswahl verwerfen">Abbrechen</button>
</div>
</div>
</div>
<!-- Reiter: Quellen -->
<div class="left-pane" id="pane-sources">
<div class="src-search">
<span class="src-search-icon"><svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
<input type="search" id="src-search-input" placeholder="Quellen durchsuchen …" aria-label="Quellen durchsuchen" oninput="Studio.filterSources(this.value)">
</div>
<div class="col-actions">
<label><input type="checkbox" id="src-select-all" checked onchange="Studio.toggleAllSources(this.checked)"> Alle</label>
<span class="spacer"></span>
<!-- "Sammeln" steht hier, weil sein Ergebnis hier landet: beide Knoepfe
fuellen dieselbe Quellenliste - einmal per Web-Suche, einmal von Hand. -->
<button class="studio-btn collect-btn" id="collect-btn" data-stage="collect" type="button" disabled onclick="Studio.runStage('collect')" title="Neue Quellen sammeln (Crawler, Web-Suche, RSS)">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
<span class="cb-label">Sammeln</span>
</button>
<button class="studio-btn studio-btn-ghost ingest-add-btn" id="ingest-add-btn" type="button" onclick="Studio.toggleIngest()" title="Dokumente, Bilder, Sprachnachrichten oder Links hinzufügen">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
Quelle
</button>
</div>
<div class="ingest-panel" id="ingest-panel" hidden>
<div class="dropzone" id="dropzone" ondragover="Studio.dzOver(event)" ondragleave="Studio.dzLeave(event)" ondrop="Studio.dzDrop(event)" onclick="document.getElementById('ingest-file').click()">
<input type="file" id="ingest-file" multiple hidden accept=".pdf,.docx,.doc,.txt,.json,.csv,.md,.log,.png,.jpg,.jpeg,.gif,.bmp,.webp,.tif,.tiff,.mp3,.wav,.m4a,.ogg,.oga,.opus,.flac,.aac,.amr,.mp4,.weba,.3gp" onchange="Studio.dzFiles(this.files); this.value='';">
<svg class="dz-icon" xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg>
<div class="dz-hint">Dateien hierher ziehen oder <strong>klicken</strong></div>
<div class="dz-sub">PDF · Word · JSON/TXT · Bilder · Sprachnachrichten</div>
</div>
<div class="ingest-url">
<input type="url" id="ingest-url" placeholder="oder Link einfügen (https://…)" onkeydown="Studio.urlKey(event)">
<button class="studio-btn" type="button" onclick="Studio.addUrl()">Hinzufügen</button>
</div>
</div>
<div class="ingest-jobs" id="ingest-jobs"></div>
<div class="col-body" id="sources-list"></div>
</div>
</section>
<!-- Spalte 2: Studio-Tabs (oben) + Konversation (unten) -->
<section class="studio-col studio-col-center" id="studio-center">
<!-- Frisch angelegter Fall: der Nutzer entscheidet aktiv, was passieren soll.
Frueher blieb er hier vor einer leeren Oberflaeche stehen. -->
<div class="start-panel" id="start-panel" hidden>
<div class="sp-inner">
<div class="sp-head">
<div class="sp-title">Dieser Fall ist noch leer</div>
<div class="sp-sub">Wie soll es weitergehen?</div>
</div>
<div class="sp-warn" id="sp-warn" hidden>
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
<span id="sp-warn-text"></span>
</div>
<div class="sp-options">
<button class="sp-opt" onclick="Studio.startChoice('full')">
<span class="sp-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/></svg></span>
<span class="sp-text">
<span class="sp-name">Recherche jetzt starten</span>
<span class="sp-desc" id="sp-full-desc">Sammeln, Analyse, Faktencheck und Karte nacheinander. Am Ende steht ein fertiges Lagebild.</span>
</span>
</button>
<button class="sp-opt" onclick="Studio.startChoice('collect')">
<span class="sp-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></span>
<span class="sp-text">
<span class="sp-name">Nur Quellen sammeln</span>
<span class="sp-desc">Erst zusammentragen, was es zum Thema gibt. Analyse und Faktencheck startest du danach selbst, wenn sich das Material lohnt.</span>
</span>
</button>
<button class="sp-opt" onclick="Studio.startChoice('ingest')">
<span class="sp-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="12" y1="18" x2="12" y2="12"/><polyline points="9 15 12 12 15 15"/></svg></span>
<span class="sp-text">
<span class="sp-name">Eigene Dokumente zuerst</span>
<span class="sp-desc">PDFs, Bilder, Sprachnachrichten oder Links hinzufügen und den Fall auf eigenem Material aufbauen. Ohne Websuche.</span>
</span>
</button>
<button class="sp-opt" onclick="Studio.startChoice('auto')">
<span class="sp-ico"><svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></span>
<span class="sp-text">
<span class="sp-name">Automatisch aktualisieren</span>
<span class="sp-desc" id="sp-auto-desc">Der Fall läuft ab jetzt selbstständig im eingestellten Intervall. Der erste Lauf startet sofort.</span>
</span>
</button>
</div>
</div>
</div>
<!-- Tab-Panel: nur sichtbar, wenn mindestens ein Studio-Teil geoeffnet ist -->
<div class="center-tabs">
<div class="center-tabbar" id="center-tabbar" role="tablist"></div>
<div class="center-tab-content" id="center-tab-content">
<div class="tab-panel" data-art="summary"><div id="art-summary-body"></div></div>
<div class="tab-panel" data-art="latest"><div id="art-latest-body"></div></div>
<div class="tab-panel" data-art="factcheck"><div id="art-fc-body"></div></div>
<div class="tab-panel" data-art="map">
<div id="map-empty">Noch keine Orte erkannt.</div>
<div id="map-container"></div>
</div>
<div class="tab-panel" data-art="timeline"><div id="art-tl-body"></div></div>
<div class="tab-panel" data-art="snapshots"><div id="art-snap-body"></div></div>
<div class="tab-panel" data-art="export">
<div class="export-row">
<select id="export-format">
<option value="pdf">PDF</option>
<option value="docx">Word (DOCX)</option>
</select>
<button class="studio-btn" onclick="Studio.doExport()">Bericht erzeugen</button>
</div>
<div class="empty-hint" style="margin-top:8px;">Enthält Zusammenfassung, Bericht, Faktencheck und Quellen.</div>
</div>
</div>
</div>
<!-- Verschiebbare Trennung (Tab-Panel <-> Konversation) -->
<div class="center-divider" id="center-divider" title="Ziehen, um die Aufteilung zu ändern" role="separator" aria-orientation="horizontal" aria-label="Aufteilung ändern"></div>
<!-- Konversation (immer sichtbar, unten) -->
<div class="chat-region">
<div class="col-head"><span>Konversation</span>
<button class="col-count" style="background:none;border:none;cursor:pointer;color:var(--text-disabled);" onclick="Studio.resetChat()" title="Neuer Chat">Neu &#x21bb;</button>
</div>
<div class="chat-messages" id="chat-messages"></div>
<div class="chat-suggestions" id="chat-suggestions"></div>
<label class="chat-scope" id="chat-scope" title="Antwort nicht nur aus diesem Fall, sondern fallübergreifend aus allen Fällen ziehen">
<input type="checkbox" id="chat-scope-all" onchange="Studio.toggleScope(this.checked)">
<span>Über alle Fälle suchen</span>
</label>
<div class="chat-input-row">
<textarea id="chat-input" rows="1" placeholder="Frage zum Fall stellen …" oninput="Studio.autoGrow(this)" onkeydown="Studio.chatKey(event)"></textarea>
<button class="chat-send" id="chat-send" onclick="Studio.sendChat()" aria-label="Senden">&#10148;</button>
</div>
</div>
</section>
<!-- Spalte 3: Studio-Menue (oeffnet Tabs in der Mitte) -->
<section class="studio-col studio-col-artifacts">
<div class="col-head"><span>Studio</span></div>
<!-- Artefakt-Karten: die Karte IST das Ergebnis. Klick oeffnet es in der
Mitte, das Symbol rechts rechnet es neu. Die Hinweiszeile darunter
meldet selbst, wenn das Ergebnis veraltet ist (siehe /freshness). -->
<div class="col-body studio-menu" id="studio-menu">
<!-- Kompletter Lauf orchestriert alles -> steht eigenstaendig ueber den Karten -->
<button class="run-all" data-stage="full" disabled onclick="Studio.runStage('full')" title="Alle Schritte nacheinander (Sammeln, Analyse, Faktencheck, Geoparsing)">
<span class="ra-icon"><svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/></svg></span>
<span class="ra-label">Kompletter Lauf</span>
<span class="ra-state"></span>
</button>
<!-- Lagebild (Erzeuger: Analyse) -->
<div class="art-card" data-card="analyze">
<div class="art-row">
<button class="studio-menu-item art-head" data-art="summary" onclick="Studio.openTab('summary')">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg></span>
<span class="mi-text">
<span class="mi-title" id="art-summary-title">Lagebild</span>
<span class="mi-meta" id="art-summary-meta"></span>
</span>
</button>
<button class="art-run" data-stage="analyze" disabled onclick="Studio.runStage('analyze')" title="Analyse komplett neu rechnen - der bisherige Bericht bleibt im Verlauf">
<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" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/></svg>
</button>
</div>
<div class="art-note" data-note="analyze"></div>
<div class="art-subs">
<button class="studio-menu-item art-sub" data-art="latest" onclick="Studio.openTab('latest')">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg></span>
<span class="mi-title" id="art-latest-title">Neueste Entwicklungen</span>
<span class="mi-meta"></span>
</button>
<button class="studio-menu-item art-sub" data-art="snapshots" onclick="Studio.openTab('snapshots')">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/><path d="M3 3v5h5"/><path d="M12 7v5l4 2"/></svg></span>
<span class="mi-title">Frühere Lageberichte</span>
<span class="mi-meta" id="art-snap-meta"></span>
</button>
</div>
</div>
<!-- Faktencheck (Verlauf frueherer Laeufe steckt im Faktencheck-Tab selbst) -->
<div class="art-card" data-card="factcheck">
<div class="art-row">
<button class="studio-menu-item art-head" data-art="factcheck" onclick="Studio.openTab('factcheck')">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3.85 8.62a4 4 0 0 1 4.78-4.77 4 4 0 0 1 6.74 0 4 4 0 0 1 4.78 4.78 4 4 0 0 1 0 6.74 4 4 0 0 1-4.77 4.78 4 4 0 0 1-6.75 0 4 4 0 0 1-4.78-4.77 4 4 0 0 1 0-6.76Z"/><path d="m9 12 2 2 4-4"/></svg></span>
<span class="mi-text">
<span class="mi-title">Faktencheck</span>
<span class="mi-meta" id="art-fc-meta"></span>
</span>
</button>
<button class="art-run" data-stage="factcheck" disabled onclick="Studio.runStage('factcheck')" title="Faktencheck komplett neu - der bisherige Stand wird als Lauf archiviert">
<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" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/></svg>
</button>
</div>
<div class="art-note" data-note="factcheck"></div>
</div>
<!-- Karte (Erzeuger: Geoparsing) -->
<div class="art-card" data-card="geoparse">
<div class="art-row">
<button class="studio-menu-item art-head" data-art="map" onclick="Studio.openTab('map')">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg></span>
<span class="mi-text">
<span class="mi-title">Geografische Verteilung</span>
<span class="mi-meta map-stats" id="map-stats"></span>
</span>
</button>
<button class="art-run" data-stage="geoparse" disabled onclick="Studio.runStage('geoparse')" title="Orte aus noch nicht geprüften Artikeln erkennen">
<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" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/></svg>
</button>
</div>
<div class="art-note" data-note="geoparse"></div>
</div>
<!-- Netzwerk: Erzeuger fehlt noch im Backend, daher ohne Ansicht -->
<div class="art-card art-card-soon" data-card="network">
<div class="art-row">
<div class="art-head art-head-static">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.6" y1="10.5" x2="15.4" y2="6.5"/><line x1="8.6" y1="13.5" x2="15.4" y2="17.5"/></svg></span>
<span class="mi-text">
<span class="mi-title">Netzwerkanalyse</span>
<span class="mi-meta">Akteure und Beziehungen</span>
</span>
</div>
<button class="art-run" data-stage="network" disabled title="Baustein folgt">
<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" aria-hidden="true"><path d="M3 12a9 9 0 0 1 15.5-6.4L21 8"/><polyline points="21 3 21 8 16 8"/><path d="M21 12a9 9 0 0 1-15.5 6.4L3 16"/><polyline points="3 21 3 16 8 16"/></svg>
</button>
</div>
<div class="art-note" data-note="network"></div>
</div>
<!-- Ereignis-Timeline: hat keinen Erzeuger, entsteht nebenbei -->
<div class="art-card" data-card="timeline">
<div class="art-row">
<button class="studio-menu-item art-head" data-art="timeline" onclick="Studio.openTab('timeline')">
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v18h18"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg></span>
<span class="mi-text">
<span class="mi-title">Ereignis-Timeline</span>
<span class="mi-meta" id="art-tl-meta"></span>
</span>
</button>
</div>
<div class="art-note" data-note="timeline"></div>
</div>
<!-- Export ist eine Ausgabe-Aktion, kein Artefakt -> Fusszeile -->
<div class="art-foot">
<button class="studio-menu-item art-export" data-art="export" onclick="Studio.openTab('export')">
<span class="mi-icon"><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" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg></span>
<span class="mi-title">Bericht exportieren</span>
</button>
</div>
</div>
</section>
<!-- Ohne offenen Fall: Mitte und rechte Spalte bleiben stehen, sind aber
ausgegraut und nicht bedienbar. Dieser Hinweis legt sich darueber. -->
<div class="studio-empty" id="studio-empty" style="display:none;">
<div class="se-box">
<svg xmlns="http://www.w3.org/2000/svg" width="30" height="30" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 20h16a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13c0 1.1.9 2 2 2Z"/></svg>
<div>Wähle links einen Fall aus oder lege oben einen neuen an.</div>
</div>
</div>
</main>
</div>
<!-- Modal: Rueckfrage (ersetzt das Browser-confirm) -->
<div class="modal-overlay" id="modal-confirm" role="dialog" aria-modal="true" aria-labelledby="confirm-title">
<div class="modal modal-confirm">
<div class="modal-header">
<div class="modal-title" id="confirm-title">Sicher?</div>
<button class="modal-close" type="button" onclick="Studio._confirmClose(false)" aria-label="Schließen">&times;</button>
</div>
<div class="modal-body">
<div id="confirm-body"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="confirm-cancel" onclick="Studio._confirmClose(false)">Abbrechen</button>
<button type="button" class="btn btn-danger" id="confirm-ok" onclick="Studio._confirmClose(true)">Löschen</button>
</div>
</div>
</div>
<!-- Modal: Neuen Fall anlegen (gleicher Funktionsumfang wie im Dashboard) -->
<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" type="button" onclick="Studio.closeNewIncident()" aria-label="Schließen">&times;</button>
</div>
<form id="new-incident-form" onsubmit="Studio.submitIncident(event)">
<div class="modal-body">
<div class="form-group">
<label for="inc-type">Art der Lage</label>
<select id="inc-type" onchange="Studio.incTypeDefaults()">
<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 for="inc-title">Titel des Vorfalls</label>
<input type="text" id="inc-title" required aria-required="true" placeholder="z.B. Explosion in Madrid" oninput="Studio.incTitleChanged()">
</div>
<div class="form-group">
<div class="description-label-row">
<label for="inc-description">Beschreibung / Kontext</label>
<button type="button" class="btn btn-secondary btn-small" id="btn-enhance-description" onclick="Studio.generateDescription()" disabled>
<span id="enhance-btn-text">Beschreibung generieren</span>
<span id="enhance-spinner" class="spinner-inline" style="display:none;"></span>
</button>
</div>
<textarea id="inc-description" placeholder="Weitere Details zum Vorfall (optional)"></textarea>
</div>
<div class="form-group">
<label>Quellen</label>
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="inc-international">
<span class="toggle-switch"></span>
<span class="toggle-text">Internationale Quellen einbeziehen</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>
</label>
</div>
<div class="toggle-group" style="margin-top: 8px;">
<label class="toggle-label">
<input type="checkbox" id="inc-x">
<span class="toggle-switch"></span>
<span class="toggle-text">X (Twitter) einbeziehen</span>
</label>
<div class="form-hint" id="inc-x-hint" style="display:none;">Erst einen X-Zugang hinterlegen (klassische Ansicht &rarr; „X-Zugänge").</div>
</div>
</div>
<div class="form-group">
<label>Sichtbarkeit</label>
<div class="toggle-group">
<label class="toggle-label">
<input type="checkbox" id="inc-visibility" checked onchange="Studio.incVisibilityHint()">
<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="Studio.incRefreshToggle()">
<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="Studio.incIntervalMin()">
<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 conditional-field" id="refresh-starttime-field">
<label for="inc-refresh-starttime">Erste Aktualisierung um</label>
<input type="time" id="inc-refresh-starttime" value="07:00">
</div>
<div class="form-group">
<label for="inc-retention">Aufbewahrung (Tage)</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="Studio.closeNewIncident()">Abbrechen</button>
<button type="submit" class="btn btn-primary" id="modal-new-submit">Lage anlegen</button>
</div>
</form>
</div>
</div>
<!-- Toasts -->
<div class="toast-container" id="toast-container" aria-live="polite" aria-atomic="true"></div>
<!-- Vendor + geteilte Module -->
<script src="/static/vendor/leaflet.js"></script>
<script src="/static/vendor/leaflet.markercluster.js"></script>
<script src="/static/js/i18n.js?v=20260513a"></script>
<script src="/static/js/api.js?v=20260714a"></script>
<script src="/static/js/ws.js?v=20260316b"></script>
<script src="/static/js/components.js?v=20260514e"></script>
<script src="/static/js/studio.js?v=20260714b"></script>
</body>
</html>