Portierung aus dem Lokal-Fork (AegisSight-Monitor-Local, Commit d2b9168, per Cherry-Pick übernommen und an den Server-Stand angepasst). - Feste Sätze je Aktion (BILLING_MODE=flat) statt echter Kosten geteilt durch cost_per_credit. Sätze auf die am 23.07.2026 beschlossenen Verkaufswerte gesetzt. Live-Lauf 45, Recherche je Durchlauf 40, Studio-Bausteine 12, Chat/Beschreibung/Globe 1. Per ENV überschreibbar, der alte Modus bleibt als BILLING_MODE=actual erhalten. - Abrechnungsperiode. licenses.credits_period trennt monthly von total, träger Monatsreset bei der nächsten Lizenzprüfung, optionaler Übertrag (credits_rollover, gedeckelt auf ein Monatskontingent). Bestandslizenzen ohne Periodenmarke behalten ihren Verbrauch. - Hard-Stop gegen das verfügbare Monatskontingent (Kontingent plus Übertrag) statt gegen das Lebenszeit-Total. - Warnschwelle budget_warning_percent (Default 80 Prozent) wird erstmals ausgewertet, einmalige Meldung an alle aktiven Nutzer der Organisation. - DB-Migration additiv und idempotent (credits_period, credits_period_start, credits_rollover, credits_carried, budget_warning_sent, unlimited_budget). - /api/auth/me liefert credits_available als Bezugsgröße plus credits_period, das Credits-Widget zeigt "Credits diesen Monat". - Wortlaut überall Credits (nicht Guthaben/Einheiten), docs/ABRECHNUNG.md auf den Online-Stand gebracht. Abweichungen zur Fork-Vorlage. Die Takt-Änderungen (Untergrenze 30 Min, Kostenvorschau im Anlege-Dialog, Fork-Commit 5b0b578) sind bewusst NICHT enthalten, die Sätze stehen auf 45/40 statt der Fork-Defaults 24/33. Getestet gegen eine Kopie der Staging-DB, 20 Prüfungen bestanden. Migration, Flat-Buchung adhoc/research/chat, Warnschwelle einmalig, Hard-Stop, Monatsreset, Übertrag gedeckelt, Bestandslizenz ohne Marke. Die vier Live-Lizenzen stehen auf unlimited_budget und sind unbeeinflusst. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
572 Zeilen
45 KiB
HTML
572 Zeilen
45 KiB
HTML
<!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=20260725a">
|
|
</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>
|
|
|
|
<!-- Gleicher Aufbau wie im klassischen Dashboard: Barrierefreiheit
|
|
(wird von a11y.js hier eingehaengt), Theme, Konto, Ansichtswechsel, Abmelden. -->
|
|
<div class="studio-top-right">
|
|
<div class="theme-switch" id="theme-toggle" onclick="Studio.toggleTheme()" 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 class="credits-divider"></div>
|
|
<button class="header-dropdown-action" type="button" onclick="AIDisclaimer && AIDisclaimer.show()">
|
|
<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"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
|
<span>Über KI-Inhalte</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<a href="/dashboard" class="btn btn-secondary btn-small" id="classic-view-link" title="Zur klassischen Ansicht wechseln" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px;">
|
|
<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="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
|
<span>Klassische Ansicht</span>
|
|
</a>
|
|
<button class="btn btn-secondary btn-small" id="logout-btn" type="button" onclick="Studio.logout()">Abmelden</button>
|
|
</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>
|
|
<!-- Gleicher Umfang wie die Seitenleiste im klassischen Dashboard -->
|
|
<div class="sidebar-filter case-scope">
|
|
<button class="sidebar-filter-btn active" data-scope="all" onclick="Studio.setCaseScope('all')" aria-pressed="true">Alle</button>
|
|
<button class="sidebar-filter-btn" data-scope="mine" onclick="Studio.setCaseScope('mine')" aria-pressed="false">Eigene</button>
|
|
</div>
|
|
<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 ↻</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">➤</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">×</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">×</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 → „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=20260725b"></script>
|
|
<script src="/static/js/ws.js?v=20260316b"></script>
|
|
<script src="/static/js/components.js?v=20260514e"></script>
|
|
<script src="/static/js/a11y.js?v=20260725a"></script>
|
|
<script src="/static/js/ai-disclaimer.js?v=20260725a"></script>
|
|
<script src="/static/js/studio.js?v=20260725a"></script>
|
|
</body>
|
|
</html>
|