refactor(studio): ein Startpunkt statt sechs, Schritte werden zur Anzeige
Die Bausteinspalte mischte vier Dinge in einem Kartenlayout. Ergebnisse zum Anschauen, Erzeuger zum Starten, ein Artefakt ohne Erzeuger und reine Aktionen. In jeder Karte lagen Öffnen und Neurechnen wenige Pixel auseinander, beide mit demselben Kreispfeil-Symbol. Dazu kamen sechs Stellen, an denen ein Lauf beginnen konnte, verteilt über zwei Spalten. Jetzt drei getrennte Bereiche. Oben wird gearbeitet, in der Mitte werden Ergebnisse angesehen, unten stehen die Fall-Aktionen. Gearbeitet wird über genau einen Knopf, dessen Beschriftung sich am Zustand ausrichtet. Beim leeren Fall Lauf starten, bei einer Recherche Recherche starten, bei veralteten Ergebnissen Jetzt aktualisieren, sonst Erneut durchlaufen, während eines abbrechbaren Laufs Abbrechen. Darüber steht, was gerade zu tun ist, etwa die Zahl der neuen Artikel seit dem letzten Bericht. Darunter die vier Schritte als Kette mit Zustand. Sie starten nichts, Aufklappen erklärt nur, was ein Schritt tut. Damit fällt auch eine Kostenfalle weg. Ein einzeln gestartetes Sammeln buchte denselben Satz wie der komplette Lauf, wer danach Lagebild und Faktencheck einzeln nachzog, zahlte 69 statt 45 Credits, ohne dass die Oberfläche darauf hinwies. Benannt wird ab jetzt das Ergebnis statt des Erzeugers. Artikel holen, Lagebild schreiben, Fakten prüfen, Orte erkennen. Bei Recherche-Lagen heißt der zweite Schritt Recherchebericht schreiben. Die Wörter Baustein, Geoparsing und Analyse verschwinden aus der Oberfläche. Weiter entfernt sind die Karte Netzwerkanalyse, solange es den Baustein im Server nicht gibt, der Sammeln-Knopf aus der Quellenspalte und die Wahl Nur Quellen sammeln im Startdialog. Der Server behält seine Schnittstelle für einzelne Bausteine. Die Anzeige eines so gestarteten Laufs bleibt erhalten, er kann aus der klassischen Ansicht kommen. Ein solcher Lauf wird beim Abbruch auch als der Schritt abgebrochen, der wirklich läuft, nicht pauschal als kompletter Lauf. 51 tote CSS-Regeln entfernt. Prüfungen ohne Browser, 59 neue Fälle für die Lauf-Anzeige plus 12 in der Strukturprüfung, die festhalten, dass es im ganzen Dokument genau ein Element gibt, das einen Lauf starten kann. Die Anzeige der Credits am Knopf kommt später, sie ist bewusst nicht Teil dieser Änderung. Cache-Buster auf 20260802c. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -136,7 +136,7 @@ src/:
|
||||
static/:
|
||||
index.html: "Login-Seite (Magic-Link), JS inline"
|
||||
dashboard.html: "Hauptdashboard (Sidebar + Reiter-Ansicht + 8 Modals + Chat-Widget)"
|
||||
studio.html: "Studio (3-Spalten-Werkstatt, online nur fuer info@ sichtbar - Gating rein clientseitig). Reiter Einstellungen ersetzt das Bearbeiten-Modal der klassischen Ansicht und enthaelt die Wahl des KI-Wegs je Fall."
|
||||
studio.html: "Studio (3-Spalten-Werkstatt, online nur fuer info@ sichtbar - Gating rein clientseitig). Reiter Einstellungen ersetzt das Bearbeiten-Modal der klassischen Ansicht und enthaelt die Wahl des KI-Wegs je Fall. Spalte 3 hat genau EINEN Startpunkt (#ls-btn), darunter stehen die vier Schritte als reine Anzeige. Einzelne Bausteine lassen sich aus der Oberflaeche nicht mehr starten, der Server kann es weiterhin."
|
||||
css/:
|
||||
style.css: "AegisSight Design System (Dark/Light Theme, alle Komponenten)"
|
||||
studio.css: "Studio-spezifische Styles"
|
||||
@@ -144,7 +144,7 @@ src/:
|
||||
api.js: "REST-API-Client (fetch, Auth-Header, 30s Timeout, 403 -> Nur-Lese-Modus)"
|
||||
app.js: "Hauptlogik: ThemeManager, NotificationCenter, App-Objekt (181 KB)"
|
||||
components.js: "UI-Rendering: Sidebar, Faktenchecks, Toasts, Progress, Karte (geteilt mit Studio)"
|
||||
studio.js: "Kompletter Studio-Controller (116 KB), enthaelt auch den Einstellungen-Reiter je Fall"
|
||||
studio.js: "Kompletter Studio-Controller, enthaelt den Einstellungen-Reiter je Fall und die Lauf-Anzeige (_renderLauf, _schrittZustaende, laufKnopf)"
|
||||
chat.js: "Chat-Assistent Widget (Bedienfragen)"
|
||||
pipeline.js: "Grafische Analysepipeline (11 Schritte, Live-Animation)"
|
||||
layout.js: "Reiter-Umschalter je Lage (merkt letzten Reiter; enthaelt noch gridstack-Legacy-Stubs)"
|
||||
@@ -171,6 +171,7 @@ tests/:
|
||||
test_sammelaktionen.js: "Sammelaktionen der Seitenleiste im klassischen Dashboard, Auswahlmodus, Filterbeachtung, Loeschbestaetigung (Aufruf mit node)"
|
||||
test_falleinstellungen.js: "Einstellungen-Reiter im Studio, Formular fuellen, nur Geaendertes speichern, KI-Weg umstellen samt Warnhinweis, Abo nachladen (Aufruf mit node)"
|
||||
test_studio_struktur.py: "Strukturpruefung beider Oberflaechen ohne Browser, eindeutige IDs, jedes onclick zeigt auf eine vorhandene Methode, Cache-Buster, Design-Tokens"
|
||||
test_lauf.js: "Lauf-Anzeige der Studio-Spalte, Zustand des Startknopfs, Kette, Schrittzustaende aus /freshness, Nachweis dass kein Schritt etwas startet (Aufruf mit node)"
|
||||
test_quellenausgabe.py: "Ausgabetreue des Quellenverzeichnisses: Nummern aus dem Lagebild statt Zeilennummern, keine Kappung, ein Verlag ein Name, Statistik deckungsgleich mit dem Verzeichnis"
|
||||
```
|
||||
|
||||
|
||||
@@ -1014,180 +1014,6 @@
|
||||
es neu, die Zeile darunter meldet den Datenstand (aktuell / veraltet / laeuft). */
|
||||
@keyframes st-spin { to { transform: rotate(360deg); } }
|
||||
|
||||
/* Kompletter Lauf: orchestriert alles, daher als Primaerknopf ueber den Karten */
|
||||
.run-all {
|
||||
display: flex; align-items: center; justify-content: center; gap: 8px;
|
||||
width: 100%;
|
||||
padding: 9px 12px;
|
||||
margin-bottom: 12px;
|
||||
border-radius: var(--radius-md, 6px);
|
||||
border: 1px solid var(--accent);
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-family: var(--font-title);
|
||||
font-weight: 600;
|
||||
font-size: 12.5px;
|
||||
cursor: pointer;
|
||||
transition: background .15s, opacity .15s;
|
||||
}
|
||||
.run-all:hover:not(:disabled) { background: var(--accent-hover); }
|
||||
.run-all:disabled { opacity: .45; cursor: not-allowed; }
|
||||
/* display:flex wuerde das hidden-Attribut ueberstimmen - explizit ausblenden */
|
||||
.run-all[hidden] { display: none; }
|
||||
.run-all .ra-icon { display: inline-flex; }
|
||||
.run-all .ra-state { display: none; }
|
||||
.run-all.running .ra-icon { display: none; }
|
||||
.run-all.running .ra-state {
|
||||
display: inline-flex; align-items: center; gap: 6px; font-weight: 400; font-size: 11.5px;
|
||||
}
|
||||
.run-all.running .ra-state::before {
|
||||
content: ''; width: 11px; height: 11px; flex-shrink: 0;
|
||||
border: 2px solid currentColor; border-right-color: transparent;
|
||||
border-radius: 50%; animation: st-spin .8s linear infinite;
|
||||
}
|
||||
|
||||
/* Die Karte selbst */
|
||||
.art-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
margin-bottom: 8px;
|
||||
overflow: hidden;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
.art-card:hover { border-color: var(--accent); }
|
||||
.art-card:has(.studio-menu-item.active) { border-color: var(--accent); }
|
||||
.art-card.busy { border-color: var(--accent); }
|
||||
.art-card-soon { opacity: .6; }
|
||||
.art-row { display: flex; align-items: stretch; }
|
||||
|
||||
/* Kartenkopf: uebernimmt die Rolle des alten Menue-Eintrags, aber ohne eigenen Rahmen */
|
||||
.studio-menu .art-head {
|
||||
flex: 1; min-width: 0;
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 10px var(--sp-lg);
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
.studio-menu .art-head:hover { background: var(--bg-hover); }
|
||||
.studio-menu .art-head.active { background: var(--tint-accent-faint); }
|
||||
.studio-menu .art-head-static { cursor: default; }
|
||||
.studio-menu .art-head-static:hover { background: none; }
|
||||
.art-head .mi-icon { flex-shrink: 0; width: 20px; height: 20px; display: inline-flex; align-items: center; justify-content: center; color: var(--accent); }
|
||||
.art-head .mi-text { flex: 1; min-width: 0; }
|
||||
.art-head .mi-title {
|
||||
display: block;
|
||||
font-family: var(--font-title);
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
line-height: 1.25;
|
||||
white-space: normal;
|
||||
overflow: visible;
|
||||
text-overflow: clip;
|
||||
}
|
||||
.art-head .mi-meta { display: block; margin-top: 1px; font-size: 10.5px; color: var(--text-disabled); }
|
||||
|
||||
/* Erzeuger-Aktion (rechnet das Ergebnis neu) */
|
||||
.art-run {
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: none;
|
||||
border: none;
|
||||
border-left: 1px solid var(--border);
|
||||
color: var(--text-disabled);
|
||||
cursor: pointer;
|
||||
transition: color .15s, background .15s;
|
||||
}
|
||||
.art-run:hover:not(:disabled) { color: var(--accent); background: var(--bg-hover); }
|
||||
.art-run:disabled { opacity: .35; cursor: not-allowed; }
|
||||
|
||||
/* Datenstand-Zeile */
|
||||
.art-note {
|
||||
display: none;
|
||||
align-items: center;
|
||||
gap: var(--sp-md);
|
||||
padding: 7px var(--sp-lg);
|
||||
border-top: 1px solid var(--border);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.art-note.show { display: flex; }
|
||||
.art-note.fresh { background: var(--tint-success); color: var(--success); }
|
||||
.art-note.stale { background: var(--tint-warning); color: var(--warning); }
|
||||
.art-note.blocked { background: var(--bg-elevated); color: var(--text-disabled); }
|
||||
.art-note.running { background: var(--tint-accent); color: var(--accent); }
|
||||
.art-note .an-spin {
|
||||
width: 11px; height: 11px; flex-shrink: 0;
|
||||
border: 2px solid currentColor; border-right-color: transparent;
|
||||
border-radius: 50%; animation: st-spin .8s linear infinite;
|
||||
}
|
||||
.art-note button {
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
padding: 3px 9px;
|
||||
font-size: 10.5px;
|
||||
font-weight: 600;
|
||||
border-radius: var(--radius-md, 6px);
|
||||
border: 1px solid currentColor;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
.art-note button:hover { background: var(--tint-hover-subtle); }
|
||||
|
||||
/* Unter-Ansichten: gehoeren zum selben Erzeuger (Analyse) */
|
||||
.art-subs { border-top: 1px solid var(--border); }
|
||||
.studio-menu .art-sub {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
width: 100%;
|
||||
padding: 6px var(--sp-lg);
|
||||
margin: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
color: var(--text-secondary);
|
||||
font-family: var(--font-body);
|
||||
font-size: 11.5px;
|
||||
}
|
||||
.studio-menu .art-sub:hover { background: var(--bg-hover); color: var(--text-primary); }
|
||||
.studio-menu .art-sub.active { background: var(--tint-accent-faint); color: var(--accent); }
|
||||
.art-sub .mi-icon { flex-shrink: 0; color: var(--text-disabled); display: inline-flex; }
|
||||
.art-sub.active .mi-icon { color: var(--accent); }
|
||||
.art-sub .mi-title {
|
||||
flex: 1; min-width: 0;
|
||||
font-family: var(--font-body);
|
||||
font-weight: 500;
|
||||
font-size: 11.5px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.art-sub .mi-meta { flex-shrink: 0; font-size: 10px; color: var(--text-disabled); }
|
||||
|
||||
/* Export: Ausgabe-Aktion, kein Artefakt -> Fusszeile */
|
||||
.art-foot { margin-top: var(--sp-lg); padding-top: var(--sp-lg); border-top: 1px solid var(--border); }
|
||||
.studio-menu .art-export { justify-content: center; gap: 8px; margin-bottom: 0; padding: 8px; font-size: 12px; }
|
||||
.art-export .mi-title { font-family: var(--font-body); font-weight: 600; font-size: 12px; flex: 0 0 auto; }
|
||||
.art-export .mi-icon { color: var(--accent); display: inline-flex; }
|
||||
|
||||
/* "Sammeln" in der Quellenspalte (sein Ergebnis landet dort) */
|
||||
.collect-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.collect-btn.running .cb-label { display: none; }
|
||||
.collect-btn.running::after {
|
||||
content: 'Sammelt …';
|
||||
display: inline-flex; align-items: center;
|
||||
}
|
||||
|
||||
/* Karten-Container fuellt den Karte-Tab */
|
||||
#map-container {
|
||||
width: 100%;
|
||||
@@ -1486,3 +1312,221 @@
|
||||
.case-ai-tag.ai-anthropic {
|
||||
color: var(--text-disabled);
|
||||
}
|
||||
|
||||
/* === Der Lauf (Spalte 3) ============================================== */
|
||||
/* Genau ein Startpunkt. Die vier Schritte darunter sind Anzeige, sie starten
|
||||
nichts. Frueher standen hier Artefakt-Karten, in denen ein Klick links das
|
||||
Ergebnis oeffnete und einer rechts es neu rechnete. Das lag zu nah
|
||||
beieinander und kostete im falschen Fall Geld. */
|
||||
|
||||
.lauf-stand {
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-card);
|
||||
}
|
||||
|
||||
/* Vollstaendig umlaufender Rahmen, kein einseitiger Streifen */
|
||||
.lauf-stand.betont {
|
||||
border: 2px solid var(--sidebar-active, #C8A851);
|
||||
padding: 11px;
|
||||
}
|
||||
|
||||
.ls-haupt {
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ls-haupt.warn { color: var(--warning); }
|
||||
|
||||
.ls-neben {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.ls-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--sidebar-active, #C8A851);
|
||||
background: var(--sidebar-active, #C8A851);
|
||||
color: #1A1A1A;
|
||||
font-family: var(--font-title, inherit);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: filter .15s, opacity .15s;
|
||||
}
|
||||
|
||||
.ls-btn:hover:not(:disabled) { filter: brightness(1.08); }
|
||||
|
||||
.ls-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
|
||||
.ls-btn.abbrechen {
|
||||
background: transparent;
|
||||
border-color: var(--error);
|
||||
color: var(--error);
|
||||
}
|
||||
|
||||
.ls-icon { display: inline-flex; flex-shrink: 0; }
|
||||
|
||||
.ls-hinweis {
|
||||
margin-top: 8px;
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ls-hinweis[hidden] { display: none; }
|
||||
|
||||
/* --- Die Kette --- */
|
||||
.lauf-kette {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 4px 6px 8px;
|
||||
}
|
||||
|
||||
.kn {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--border);
|
||||
background: var(--bg-elevated);
|
||||
}
|
||||
|
||||
.kn-fertig { background: var(--success); border-color: var(--success); }
|
||||
.kn-veraltet { background: var(--warning); border-color: var(--warning); }
|
||||
.kn-laeuft {
|
||||
background: var(--sidebar-active, #C8A851);
|
||||
border-color: var(--sidebar-active, #C8A851);
|
||||
animation: kn-puls 1.4s ease-in-out infinite;
|
||||
}
|
||||
.kn-gesperrt { opacity: .45; }
|
||||
|
||||
.kn-linie { flex: 1; height: 2px; background: var(--border); }
|
||||
.kn-linie.aktiv { background: var(--success); }
|
||||
|
||||
@keyframes kn-puls { 0%, 100% { opacity: 1; } 50% { opacity: .35; } }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.kn-laeuft { animation: none; }
|
||||
}
|
||||
|
||||
.lauf-fortschritt {
|
||||
font-size: 11px;
|
||||
color: var(--text-tertiary);
|
||||
text-align: center;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* --- Die Schritte --- */
|
||||
.lauf-schritte { margin-bottom: 4px; }
|
||||
|
||||
.schritt {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
background: var(--bg-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.schritt-laeuft, .schritt-veraltet {
|
||||
border: 2px solid var(--sidebar-active, #C8A851);
|
||||
}
|
||||
|
||||
.schritt-gesperrt { opacity: .55; }
|
||||
|
||||
.schritt-kopf {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-primary);
|
||||
font-family: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.schritt-kopf:hover { background: var(--tint-hover-subtle, rgba(255,255,255,.03)); }
|
||||
|
||||
.schritt-nr {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--bg-elevated);
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.schritt-fertig .schritt-nr { background: var(--tint-success); color: var(--success); }
|
||||
.schritt-veraltet .schritt-nr { background: var(--tint-warning); color: var(--warning); }
|
||||
.schritt-laeuft .schritt-nr { background: var(--tint-accent); color: var(--sidebar-active, #C8A851); }
|
||||
|
||||
.schritt-txt { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||||
.schritt-name { font-size: 12px; }
|
||||
.schritt-stand { font-size: 11px; color: var(--text-tertiary); }
|
||||
.schritt-fertig .schritt-stand { color: var(--success); }
|
||||
.schritt-veraltet .schritt-stand { color: var(--warning); }
|
||||
|
||||
.schritt-pfeil {
|
||||
flex-shrink: 0;
|
||||
color: var(--text-tertiary);
|
||||
display: inline-flex;
|
||||
transition: transform .18s;
|
||||
}
|
||||
|
||||
.schritt.offen .schritt-pfeil { transform: rotate(180deg); }
|
||||
|
||||
.schritt-mehr {
|
||||
display: none;
|
||||
padding: 0 10px 10px 40px;
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.schritt.offen .schritt-mehr { display: block; }
|
||||
|
||||
/* --- Bereichstrenner im Menue --- */
|
||||
.menu-bereich {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 20px 0 6px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: .08em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.menu-bereich::after {
|
||||
content: "";
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
}
|
||||
|
||||
/* Untergeordnete Ansicht, gehoert zum Eintrag darueber */
|
||||
.studio-menu-item.mi-unter {
|
||||
padding-left: 30px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.studio-menu-item.mi-unter .mi-title { font-size: 12px; }
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<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=20260802b">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260802c">
|
||||
<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; }
|
||||
@@ -858,11 +858,11 @@
|
||||
<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=20260802b"></script>
|
||||
<script src="/static/js/components.js?v=20260802c"></script>
|
||||
<script src="/static/js/layout.js?v=20260513f"></script>
|
||||
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
||||
<script src="/static/js/a11y.js?v=20260725a"></script>
|
||||
<script src="/static/js/app.js?v=20260802b"></script>
|
||||
<script src="/static/js/app.js?v=20260802c"></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=20260514e"></script>
|
||||
|
||||
@@ -327,10 +327,12 @@ const Studio = {
|
||||
const zeigen = !!leer && !this._runningStage && !schonGewaehlt;
|
||||
panel.hidden = !zeigen;
|
||||
|
||||
// "Kompletter Lauf" rechts und die Startfrage sind dieselbe Aktion - nie beide
|
||||
// gleichzeitig zeigen. Wird die Frage nicht (mehr) gestellt, ist der Knopf wieder
|
||||
// da; sonst stünde man nach einem ergebnislosen Lauf ohne Startmoeglichkeit da.
|
||||
const runAll = document.querySelector('.run-all');
|
||||
// Der Stand-Kasten und die Startfrage sind dieselbe Aktion, nie beide
|
||||
// gleichzeitig zeigen. Wird die Frage nicht mehr gestellt, ist der Kasten
|
||||
// wieder da, sonst stuende man nach einem ergebnislosen Lauf ohne
|
||||
// Startmoeglichkeit da. Die Kette darunter bleibt sichtbar, sie erklaert,
|
||||
// was der Startdialog anbietet.
|
||||
const runAll = document.getElementById('lauf-stand');
|
||||
if (runAll) runAll.hidden = zeigen;
|
||||
|
||||
if (!zeigen) return;
|
||||
@@ -342,7 +344,7 @@ const Studio = {
|
||||
if (full) {
|
||||
full.textContent = research
|
||||
? 'Tiefenrecherche in drei Durchläufen (Breite Erfassung, Vertiefung, Konsolidierung), danach Analyse und Faktencheck. Dauert deutlich länger als ein einzelner Lauf.'
|
||||
: 'Sammeln, Analyse, Faktencheck und Karte nacheinander. Am Ende steht ein fertiges Lagebild.';
|
||||
: 'Artikel holen, Lagebild schreiben, Fakten prüfen und Orte erkennen, nacheinander. Am Ende steht ein fertiges Lagebild.';
|
||||
}
|
||||
const auto = document.getElementById('sp-auto-desc');
|
||||
if (auto) {
|
||||
@@ -378,7 +380,7 @@ const Studio = {
|
||||
// kommt zurück (beide sind dieselbe Aktion).
|
||||
localStorage.setItem('studio_start_' + inc.id, kind);
|
||||
panel.hidden = true;
|
||||
const runAll = document.querySelector('.run-all');
|
||||
const runAll = document.getElementById('lauf-stand');
|
||||
if (runAll) runAll.hidden = false;
|
||||
|
||||
if (kind === 'ingest') {
|
||||
@@ -401,8 +403,8 @@ const Studio = {
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 'full' (kompletter Lauf) oder 'collect' (nur sammeln)
|
||||
this.runStage(kind);
|
||||
// Bleibt 'full'. Einzelne Schritte lassen sich nicht mehr starten.
|
||||
this.runStage('full');
|
||||
},
|
||||
|
||||
// === Auswahlmodus + Massenaktionen ===
|
||||
@@ -1812,7 +1814,7 @@ const Studio = {
|
||||
card.innerHTML = `<div class="chat-offer-title">Gezielte Recherche abgeschlossen.</div>
|
||||
<div class="chat-offer-text">${n} neue Meldung${n === 1 ? '' : 'en'} zur Frage erfasst. Starte Analyse oder Faktencheck neu und stelle die Frage erneut.</div>`;
|
||||
this._tlLoaded = false;
|
||||
try { this.fresh = await API.getFreshness(incId); this._renderFreshness(); } catch (e) {}
|
||||
try { this.fresh = await API.getFreshness(incId); this._renderLauf(); } catch (e) {}
|
||||
} else if (st && st.status === 'error') {
|
||||
if (btn) { btn.disabled = false; btn.textContent = 'Erneut versuchen'; }
|
||||
if (textEl) textEl.textContent = 'Die Recherche ist fehlgeschlagen. Bitte erneut versuchen.';
|
||||
@@ -1823,65 +1825,88 @@ const Studio = {
|
||||
setTimeout(tick, 3000);
|
||||
},
|
||||
|
||||
// === Bausteine (Kacheln in Spalte 3) ===
|
||||
// Ein Klick startet den Baustein sofort. Läuft er, zeigt die Kachel Spinner +
|
||||
// Fortschritt; bei den Orchestrator-Bausteinen (Sammeln, Kompletter Lauf) bricht
|
||||
// ein zweiter Klick ab. Analyse/Faktencheck/Geoparsing haben backendseitig keinen
|
||||
// Abbruch - ihre Kachel ist während des Laufs gesperrt statt einen toten Knopf zu zeigen.
|
||||
// === Der Lauf (Spalte 3) =============================================
|
||||
// Es gibt genau einen Startpunkt. Die vier Schritte darunter sind Anzeige,
|
||||
// sie starten nichts. Aufklappen erklaert nur, was ein Schritt tut.
|
||||
//
|
||||
// Der Server kann einzelne Bausteine weiterhin starten (POST .../stage).
|
||||
// Die Oberflaeche nutzt das nicht mehr, zeigt einen so gestarteten Lauf
|
||||
// aber korrekt an, denn er kann aus der klassischen Ansicht kommen.
|
||||
_CANCELLABLE: ['collect', 'full'],
|
||||
_STAGE_LABELS: {
|
||||
collect: 'Sammeln', analyze: 'Analyse', factcheck: 'Faktencheck',
|
||||
geoparse: 'Geoparsing', network: 'Netzwerkanalyse', full: 'Kompletter Lauf',
|
||||
collect: 'Artikel holen', analyze: 'Lagebild schreiben',
|
||||
factcheck: 'Fakten prüfen', geoparse: 'Orte erkennen',
|
||||
network: 'Netzwerkanalyse', full: 'Kompletter Lauf',
|
||||
},
|
||||
|
||||
// "Aktualisieren" von außen (Tastatur/WS-Resync) = kompletter Lauf
|
||||
_SCHRITT_ICONS: {
|
||||
collect: '<path d="M22 12h-6l-2 3h-4l-2-3H2"/><path d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"/>',
|
||||
analyze: '<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"/>',
|
||||
factcheck: '<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"/>',
|
||||
geoparse: '<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"/>',
|
||||
},
|
||||
|
||||
_SCHRITT_WAS: {
|
||||
collect: 'Durchsucht die ausgewählten Quellen nach neuen Meldungen. Ohne diesen Schritt haben die folgenden nichts, womit sie arbeiten können.',
|
||||
analyze: 'Wertet alle Artikel aus und schreibt daraus den zusammenhängenden Bericht. Der bisherige Bericht bleibt im Verlauf erhalten.',
|
||||
factcheck: 'Zieht die Tatsachenbehauptungen aus dem Bericht und sucht nach Belegen und Widersprüchen. Der bisherige Stand wird als Lauf archiviert.',
|
||||
geoparse: 'Findet Ortsangaben in den Artikeln und setzt sie auf die Karte. Läuft nur über Artikel, die noch nicht geprüft wurden.',
|
||||
},
|
||||
|
||||
_schrittOffen: null,
|
||||
|
||||
/** Von aussen (Tastatur, WebSocket-Resync) heisst Aktualisieren kompletter Lauf. */
|
||||
async refresh() { return this.runStage('full'); },
|
||||
|
||||
// Alle Start-Elemente tragen data-stage: die ↻-Knoepfe in den Karten, "Kompletter
|
||||
// Lauf" und der Sammeln-Knopf in der Quellenspalte.
|
||||
/**
|
||||
* Der eine Startknopf. Laeuft gerade ein abbrechbarer Schritt, bricht er ab,
|
||||
* sonst startet er den kompletten Lauf.
|
||||
*
|
||||
* Wichtig ist der laufende Schritt, nicht 'full'. Ein Lauf kann aus der
|
||||
* klassischen Ansicht als 'collect' gestartet worden sein. Ein Abbruch auf
|
||||
* 'full' wuerde ihn dann nicht treffen, obwohl der Knopf Abbrechen anzeigt.
|
||||
*/
|
||||
async laufKnopf() {
|
||||
const run = this._runningStage;
|
||||
if (run && this._CANCELLABLE.includes(run)) return this.runStage(run);
|
||||
if (run) {
|
||||
UI.showToast('Dieser Schritt lässt sich nicht abbrechen.', 'info');
|
||||
return;
|
||||
}
|
||||
return this.runStage('full');
|
||||
},
|
||||
|
||||
_stageEls() { return Array.from(document.querySelectorAll('[data-stage]')); },
|
||||
_stageEl(stage) { return document.querySelector(`[data-stage="${CSS.escape(stage)}"]`); },
|
||||
|
||||
_stagesIdle() {
|
||||
this._runningStage = null;
|
||||
// Ohne Artikel können Analyse, Faktencheck und Geoparsing nicht laufen
|
||||
// (das Backend wirft "Keine Artikel vorhanden") - erst gar nicht anbieten.
|
||||
const ohneArtikel = !(this.articles || []).length;
|
||||
const brauchtArtikel = ['analyze', 'factcheck', 'geoparse'];
|
||||
this._stageEls().forEach(b => {
|
||||
b.classList.remove('running');
|
||||
const s = b.dataset.stage;
|
||||
// Netzwerkanalyse bleibt gesperrt (Baustein folgt im Backend)
|
||||
b.disabled = (s === 'network') || !this.incident
|
||||
|| (ohneArtikel && brauchtArtikel.includes(s));
|
||||
b.disabled = !this.incident;
|
||||
});
|
||||
document.querySelectorAll('.art-card').forEach(c => c.classList.remove('busy'));
|
||||
const ra = document.querySelector('.run-all .ra-state');
|
||||
if (ra) ra.textContent = '';
|
||||
this._renderFreshness();
|
||||
this._renderLauf();
|
||||
},
|
||||
|
||||
_stageRunning(stage, text) {
|
||||
if (!stage) return;
|
||||
this._runningStage = stage;
|
||||
// Sobald etwas läuft, ist die Startfrage beantwortet - auch wenn der Lauf
|
||||
// über den Sammeln-Knopf links statt über das Panel ausgelöst wurde.
|
||||
// Sobald etwas läuft, ist die Startfrage beantwortet.
|
||||
const panel = document.getElementById('start-panel');
|
||||
if (panel) panel.hidden = true;
|
||||
if (this.incident) localStorage.setItem('studio_start_' + this.incident.id, 'run');
|
||||
const cancellable = this._CANCELLABLE.includes(stage);
|
||||
this._stageEls().forEach(b => {
|
||||
const isThis = b.dataset.stage === stage;
|
||||
b.classList.toggle('running', isThis);
|
||||
// Die laufende Aktion bleibt nur klickbar, wenn sie abbrechbar ist
|
||||
b.disabled = isThis ? !cancellable : true;
|
||||
b.classList.add('running');
|
||||
// Der Startknopf bleibt nur klickbar, wenn sich der Lauf abbrechen lässt
|
||||
b.disabled = !cancellable;
|
||||
});
|
||||
document.querySelectorAll('.art-card').forEach(c => {
|
||||
c.classList.toggle('busy', c.dataset.card === stage);
|
||||
});
|
||||
const ra = document.querySelector('.run-all .ra-state');
|
||||
if (ra) ra.textContent = stage === 'full' ? (text || 'läuft …') + ' · Abbrechen' : '';
|
||||
this._renderFreshness(text);
|
||||
this._renderLauf(text);
|
||||
},
|
||||
|
||||
klappSchritt(i) {
|
||||
this._schrittOffen = (this._schrittOffen === i) ? null : i;
|
||||
this._renderLauf();
|
||||
},
|
||||
|
||||
// App-Zeitstempel stehen bereits in lokaler Zeit -> nur umformatieren, NICHT als UTC lesen
|
||||
@@ -1890,102 +1915,218 @@ const Studio = {
|
||||
return m ? `${m[3]}.${m[2]}. ${m[4]}:${m[5]}` : '';
|
||||
},
|
||||
|
||||
_setNote(card, cls, html) {
|
||||
const el = document.querySelector(`.art-note[data-note="${card}"]`);
|
||||
if (!el) return;
|
||||
el.className = 'art-note show ' + cls;
|
||||
el.innerHTML = html;
|
||||
_svg(pfad, groesse) {
|
||||
return `<svg xmlns="http://www.w3.org/2000/svg" width="${groesse}" height="${groesse}" `
|
||||
+ 'viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" '
|
||||
+ `stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${pfad}</svg>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Datenstand je Karte anzeigen. Die Karte meldet selbst, ob ihr Ergebnis noch
|
||||
* zur Datenlage passt - so muss niemand die Pipeline im Kopf haben.
|
||||
* Zustand der vier Schritte aus /freshness ableiten.
|
||||
* 'fertig' | 'veraltet' | 'offen' | 'gesperrt' | 'laeuft' | 'wartet'
|
||||
*/
|
||||
_renderFreshness(runText) {
|
||||
const f = this.fresh;
|
||||
_schrittZustaende() {
|
||||
const f = this.fresh || {};
|
||||
const run = this._runningStage;
|
||||
const spin = '<span class="an-spin"></span>';
|
||||
const btn = (stage, label) =>
|
||||
`<button type="button" onclick="Studio.runStage('${stage}')">${label}</button>`;
|
||||
|
||||
// Sammeln-Knopf spiegelt den Lauf mit
|
||||
const cb = document.getElementById('collect-btn');
|
||||
if (cb) cb.classList.toggle('running', run === 'collect');
|
||||
|
||||
const running = (card, label) =>
|
||||
this._setNote(card, 'running', `${spin} ${label} läuft …`);
|
||||
const waiting = (card) =>
|
||||
this._setNote(card, 'blocked',
|
||||
`wartet, bis ${this._STAGE_LABELS[run] || 'der Baustein'} fertig ist`);
|
||||
|
||||
// Ohne Freshness-Daten (alte Lage frisch geladen / Endpunkt weg): Karten neutral lassen
|
||||
const cards = ['analyze', 'factcheck', 'geoparse'];
|
||||
if (run) {
|
||||
cards.forEach(c => (run === c ? running(c, this._STAGE_LABELS[c]) : waiting(c)));
|
||||
if (run === 'collect' || run === 'full') {
|
||||
cards.forEach(c => waiting(c));
|
||||
}
|
||||
}
|
||||
this._setNote('network', 'blocked', 'Baustein folgt');
|
||||
this._setNote('timeline', 'fresh', 'entsteht automatisch aus den Artikeln');
|
||||
if (run || !f) return;
|
||||
|
||||
const arts = f.articles || 0;
|
||||
|
||||
// --- Lagebild (Analyse) ---
|
||||
const s = f.summary || {};
|
||||
const sumMeta = document.getElementById('art-summary-meta');
|
||||
if (sumMeta) {
|
||||
sumMeta.textContent = s.exists
|
||||
? `${arts} Artikel ausgewertet · ${this._fmtStamp(s.last)}`
|
||||
: 'noch nicht erzeugt';
|
||||
}
|
||||
if (!arts) {
|
||||
this._setNote('analyze', 'blocked', 'Erst sammeln - ohne Artikel geht es nicht');
|
||||
} else if (!s.exists) {
|
||||
this._setNote('analyze', 'blocked', 'noch nicht erzeugt' + btn('analyze', 'Erzeugen'));
|
||||
} else if (s.pending > 0) {
|
||||
this._setNote('analyze', 'stale',
|
||||
`${s.pending} neue Artikel seit dem letzten Bericht` + btn('analyze', 'Aktualisieren'));
|
||||
} else {
|
||||
this._setNote('analyze', 'fresh', 'aktuell');
|
||||
}
|
||||
|
||||
// --- Faktencheck ---
|
||||
const fc = f.factcheck || {};
|
||||
const fcMeta = document.getElementById('art-fc-meta');
|
||||
if (fcMeta) {
|
||||
fcMeta.textContent = fc.exists
|
||||
? `${fc.facts} Fakten · ${this._fmtStamp(fc.last)}`
|
||||
: 'noch nicht erzeugt';
|
||||
}
|
||||
if (!arts) {
|
||||
this._setNote('factcheck', 'blocked', 'Erst sammeln - ohne Artikel geht es nicht');
|
||||
} else if (!fc.exists) {
|
||||
this._setNote('factcheck', 'blocked', 'noch nicht erzeugt' + btn('factcheck', 'Erzeugen'));
|
||||
} else if (fc.pending > 0) {
|
||||
this._setNote('factcheck', 'stale',
|
||||
`${fc.pending} neue Artikel seit dem letzten Faktencheck` + btn('factcheck', 'Aktualisieren'));
|
||||
} else {
|
||||
this._setNote('factcheck', 'fresh', 'aktuell');
|
||||
}
|
||||
|
||||
// --- Karte (Geoparsing) --- offen ist hier nicht "neu seit", sondern "noch nicht geprüft"
|
||||
const geo = f.geoparse || {};
|
||||
if (!arts) {
|
||||
this._setNote('geoparse', 'blocked', 'Erst sammeln - ohne Artikel geht es nicht');
|
||||
} else if (geo.pending > 0) {
|
||||
this._setNote('geoparse', 'stale',
|
||||
`${geo.pending} Artikel noch nicht verortet` + btn('geoparse', 'Verorten'));
|
||||
|
||||
const zust = (key, ohneLauf) => {
|
||||
if (run === key) return 'laeuft';
|
||||
// Der komplette Lauf und das Sammeln arbeiten alle Schritte ab
|
||||
if (run === 'full' || run === 'collect') return 'wartet';
|
||||
if (run) return 'wartet';
|
||||
return ohneLauf();
|
||||
};
|
||||
|
||||
return [
|
||||
{
|
||||
key: 'collect',
|
||||
zustand: zust('collect', () => (arts ? 'fertig' : 'offen')),
|
||||
stand: run === 'collect' ? 'läuft gerade'
|
||||
: arts ? `${arts} Artikel${f.last_collect ? ', ' + this._fmtStamp(f.last_collect) : ''}`
|
||||
: 'noch nicht gelaufen',
|
||||
},
|
||||
{
|
||||
key: 'analyze',
|
||||
zustand: zust('analyze', () => !arts ? 'gesperrt'
|
||||
: !s.exists ? 'offen'
|
||||
: s.pending > 0 ? 'veraltet' : 'fertig'),
|
||||
stand: run === 'analyze' ? 'läuft gerade'
|
||||
: !arts ? 'wartet auf Artikel'
|
||||
: !s.exists ? 'noch nicht erzeugt'
|
||||
: s.pending > 0 ? `${s.pending} neue Artikel seit dem Bericht`
|
||||
: `aktuell, ${this._fmtStamp(s.last)}`,
|
||||
},
|
||||
{
|
||||
key: 'factcheck',
|
||||
zustand: zust('factcheck', () => !arts ? 'gesperrt'
|
||||
: !fc.exists ? 'offen'
|
||||
: fc.pending > 0 ? 'veraltet' : 'fertig'),
|
||||
stand: run === 'factcheck' ? 'läuft gerade'
|
||||
: !arts ? 'wartet auf Artikel'
|
||||
: !fc.exists ? 'noch nicht erzeugt'
|
||||
: fc.pending > 0 ? `${fc.pending} neue Artikel seit der Prüfung`
|
||||
: `${fc.facts} Fakten geprüft`,
|
||||
},
|
||||
{
|
||||
key: 'geoparse',
|
||||
zustand: zust('geoparse', () => !arts ? 'gesperrt'
|
||||
: geo.pending > 0 ? 'veraltet' : 'fertig'),
|
||||
stand: run === 'geoparse' ? 'läuft gerade'
|
||||
: !arts ? 'wartet auf Artikel'
|
||||
: geo.pending > 0 ? `${geo.pending} Artikel noch nicht verortet`
|
||||
: 'alle Artikel verortet',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
/** Stand-Kasten, Kette und Schrittliste zeichnen. */
|
||||
_renderLauf(runText) {
|
||||
const kasten = document.getElementById('lauf-stand');
|
||||
if (!kasten) return;
|
||||
const inc = this.incident;
|
||||
const f = this.fresh || {};
|
||||
const run = this._runningStage;
|
||||
const arts = f.articles || 0;
|
||||
const research = inc && inc.type === 'research';
|
||||
|
||||
const haupt = document.getElementById('ls-haupt');
|
||||
const neben = document.getElementById('ls-neben');
|
||||
const knopf = document.getElementById('ls-btn');
|
||||
const text = knopf && knopf.querySelector('.ls-text');
|
||||
const iconEl = knopf && knopf.querySelector('.ls-icon');
|
||||
const hinweis = document.getElementById('ls-hinweis');
|
||||
|
||||
const ICON_LAUF = '<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"/>';
|
||||
const ICON_STOPP = '<rect width="12" height="12" x="6" y="6" rx="1"/>';
|
||||
|
||||
// --- Stand-Kasten ---
|
||||
let ueberschrift = '';
|
||||
let knopfText = 'Aktualisieren';
|
||||
let abbruch = false;
|
||||
let betont = false;
|
||||
|
||||
if (!inc) {
|
||||
ueberschrift = 'Kein Fall geöffnet';
|
||||
} else if (run) {
|
||||
ueberschrift = (this._STAGE_LABELS[run] || 'Ein Schritt') + ' läuft gerade';
|
||||
abbruch = this._CANCELLABLE.includes(run);
|
||||
knopfText = abbruch ? 'Abbrechen' : (runText || 'läuft …');
|
||||
} else if (!arts) {
|
||||
ueberschrift = 'Noch keine Artikel gesammelt';
|
||||
knopfText = research ? 'Recherche starten' : 'Lauf starten';
|
||||
betont = true;
|
||||
} else {
|
||||
this._setNote('geoparse', 'fresh', 'alle Artikel verortet');
|
||||
const offen = this._schrittZustaende()
|
||||
.filter(s => s.zustand === 'veraltet' || s.zustand === 'offen');
|
||||
if (offen.length) {
|
||||
const s = f.summary || {};
|
||||
ueberschrift = s.pending > 0
|
||||
? `${s.pending} neue Artikel seit dem letzten Bericht`
|
||||
: 'Ergebnisse sind nicht auf dem neuesten Stand';
|
||||
knopfText = 'Jetzt aktualisieren';
|
||||
betont = true;
|
||||
} else {
|
||||
ueberschrift = 'Alle Ergebnisse auf dem neuesten Stand';
|
||||
knopfText = 'Erneut durchlaufen';
|
||||
}
|
||||
}
|
||||
|
||||
// --- Nebenangaben ---
|
||||
const snapMeta = document.getElementById('art-snap-meta');
|
||||
if (snapMeta && !snapMeta.textContent) snapMeta.textContent = f.snapshots ? String(f.snapshots) : '';
|
||||
const tlMeta = document.getElementById('art-tl-meta');
|
||||
if (tlMeta && !tlMeta.textContent && f.events) tlMeta.textContent = `${f.events} Ereignisse`;
|
||||
kasten.classList.toggle('betont', betont);
|
||||
if (haupt) {
|
||||
haupt.textContent = ueberschrift;
|
||||
haupt.classList.toggle('warn', betont && !!arts);
|
||||
}
|
||||
if (neben) {
|
||||
neben.textContent = !inc ? ''
|
||||
: arts ? `${arts} Artikel im Bestand`
|
||||
: 'Noch kein Bestand';
|
||||
}
|
||||
if (text) text.textContent = knopfText;
|
||||
if (iconEl) iconEl.innerHTML = this._svg(abbruch ? ICON_STOPP : ICON_LAUF, 16);
|
||||
if (knopf) {
|
||||
knopf.classList.toggle('abbrechen', abbruch);
|
||||
knopf.disabled = !inc || (!!run && !abbruch);
|
||||
knopf.title = abbruch ? 'Laufenden Schritt abbrechen'
|
||||
: research ? 'Alle Schritte nacheinander, am Ende steht ein fertiger Recherchebericht'
|
||||
: 'Alle Schritte nacheinander, am Ende steht ein fertiges Lagebild';
|
||||
}
|
||||
if (hinweis) {
|
||||
hinweis.textContent = (!inc || run || arts) ? ''
|
||||
: 'Danach entstehen Bericht, Faktencheck und Karte von selbst.';
|
||||
hinweis.hidden = !hinweis.textContent;
|
||||
}
|
||||
|
||||
// --- Kette und Schritte ---
|
||||
const schritte = inc ? this._schrittZustaende() : [];
|
||||
const kette = document.getElementById('lauf-kette');
|
||||
const fortschritt = document.getElementById('lauf-fortschritt');
|
||||
const liste = document.getElementById('lauf-schritte');
|
||||
|
||||
if (kette) {
|
||||
kette.innerHTML = schritte.map((s, i) => {
|
||||
const kn = `<span class="kn kn-${s.zustand}"></span>`;
|
||||
if (i === schritte.length - 1) return kn;
|
||||
const voll = s.zustand === 'fertig' && schritte[i + 1].zustand === 'fertig';
|
||||
return kn + `<span class="kn-linie${voll ? ' aktiv' : ''}"></span>`;
|
||||
}).join('');
|
||||
}
|
||||
if (fortschritt) {
|
||||
const fertig = schritte.filter(s => s.zustand === 'fertig').length;
|
||||
fortschritt.textContent = inc
|
||||
? `${fertig} von ${schritte.length} Schritten auf dem neuesten Stand` : '';
|
||||
}
|
||||
|
||||
if (liste) {
|
||||
liste.innerHTML = schritte.map((s, i) => {
|
||||
const auf = this._schrittOffen === i;
|
||||
const name = (s.key === 'analyze' && research)
|
||||
? 'Recherchebericht schreiben' : this._STAGE_LABELS[s.key];
|
||||
const marke = s.zustand === 'fertig'
|
||||
? this._svg('<path d="M20 6 9 17l-5-5"/>', 12) : String(i + 1);
|
||||
return `<div class="schritt schritt-${s.zustand}${auf ? ' offen' : ''}">
|
||||
<button class="schritt-kopf" type="button" onclick="Studio.klappSchritt(${i})"
|
||||
aria-expanded="${auf}">
|
||||
<span class="schritt-nr">${marke}</span>
|
||||
<span class="schritt-txt">
|
||||
<span class="schritt-name">${UI.escape(name)}</span>
|
||||
<span class="schritt-stand">${UI.escape(s.stand)}</span>
|
||||
</span>
|
||||
<span class="schritt-pfeil">${this._svg('<polyline points="6 9 12 15 18 9"/>', 13)}</span>
|
||||
</button>
|
||||
<div class="schritt-mehr">${UI.escape(this._SCHRITT_WAS[s.key] || '')}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// --- Nebenangaben an den Ansichten ---
|
||||
this._renderMeta();
|
||||
},
|
||||
|
||||
/** Zusatzangaben an den Ansichtsknoepfen, aus /freshness. */
|
||||
_renderMeta() {
|
||||
const f = this.fresh;
|
||||
if (!f) return;
|
||||
const arts = f.articles || 0;
|
||||
const s = f.summary || {};
|
||||
const fc = f.factcheck || {};
|
||||
|
||||
const setz = (id, wert) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.textContent = wert || '';
|
||||
};
|
||||
setz('art-summary-meta', s.exists
|
||||
? `${arts} Artikel ausgewertet, ${this._fmtStamp(s.last)}` : 'noch nicht erzeugt');
|
||||
setz('art-fc-meta', fc.exists
|
||||
? `${fc.facts} Fakten, ${this._fmtStamp(fc.last)}` : 'noch nicht erzeugt');
|
||||
|
||||
const snap = document.getElementById('art-snap-meta');
|
||||
if (snap && !snap.textContent) snap.textContent = f.snapshots ? String(f.snapshots) : '';
|
||||
const tl = document.getElementById('art-tl-meta');
|
||||
if (tl && !tl.textContent && f.events) tl.textContent = `${f.events} Ereignisse`;
|
||||
},
|
||||
|
||||
async runStage(stage) {
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<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=20260802b">
|
||||
<link rel="stylesheet" href="/static/css/studio.css?v=20260802b">
|
||||
<link rel="stylesheet" href="/static/css/style.css?v=20260802c">
|
||||
<link rel="stylesheet" href="/static/css/studio.css?v=20260802c">
|
||||
</head>
|
||||
<body>
|
||||
<div class="studio">
|
||||
@@ -153,12 +153,6 @@
|
||||
<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
|
||||
@@ -206,14 +200,6 @@
|
||||
</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">
|
||||
@@ -401,122 +387,90 @@
|
||||
<!-- 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). -->
|
||||
<!-- Drei getrennte Bereiche. Oben wird gearbeitet (ein Startpunkt,
|
||||
darunter die Kette der vier Schritte als Anzeige), in der Mitte
|
||||
werden Ergebnisse angesehen, unten stehen die Fall-Aktionen.
|
||||
Ein Klick auf eine Ansicht oeffnet sie und rechnet nichts neu. -->
|
||||
<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>
|
||||
<!-- Der Lauf. Genau ein Startpunkt, damit nicht zwei Wege zum
|
||||
selben Ziel nebeneinander stehen. Die Beschriftung des Knopfes
|
||||
richtet sich nach dem Zustand des Falls, siehe _renderLauf. -->
|
||||
<div class="lauf-stand" id="lauf-stand">
|
||||
<div class="ls-haupt" id="ls-haupt"></div>
|
||||
<div class="ls-neben" id="ls-neben"></div>
|
||||
<button class="ls-btn" id="ls-btn" data-stage="full" disabled
|
||||
onclick="Studio.laufKnopf()">
|
||||
<span class="ls-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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="ls-text">Aktualisieren</span>
|
||||
</button>
|
||||
<div class="ls-hinweis" id="ls-hinweis"></div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- Die vier Schritte als Kette. Reine Anzeige, sie starten nichts.
|
||||
Aufklappen erklaert nur, was der Schritt tut. -->
|
||||
<div class="lauf-kette" id="lauf-kette" aria-hidden="true"></div>
|
||||
<div class="lauf-fortschritt" id="lauf-fortschritt"></div>
|
||||
<div class="lauf-schritte" id="lauf-schritte"></div>
|
||||
|
||||
<div class="menu-bereich">Ansehen</div>
|
||||
<button class="studio-menu-item" data-art="summary" onclick="Studio.openTab('summary')">
|
||||
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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')">
|
||||
<button class="studio-menu-item mi-unter" 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-text">
|
||||
<span class="mi-title" id="art-latest-title">Neueste Entwicklungen</span>
|
||||
<span class="mi-meta"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="studio-menu-item art-sub" data-art="snapshots" onclick="Studio.openTab('snapshots')">
|
||||
<button class="studio-menu-item mi-unter" 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-text">
|
||||
<span class="mi-title">Frühere Berichte</span>
|
||||
<span class="mi-meta" id="art-snap-meta"></span>
|
||||
</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>
|
||||
<button class="studio-menu-item" data-art="factcheck" onclick="Studio.openTab('factcheck')">
|
||||
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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>
|
||||
<button class="studio-menu-item" data-art="map" onclick="Studio.openTab('map')">
|
||||
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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 class="mi-title">Karte</span>
|
||||
<span class="mi-meta" 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>
|
||||
<button class="studio-menu-item" data-art="timeline" onclick="Studio.openTab('timeline')">
|
||||
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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">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-title">Zeitstrahl</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="settings" onclick="Studio.openTab('settings')">
|
||||
<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="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></span>
|
||||
<span class="mi-title">Einstellungen</span>
|
||||
</button>
|
||||
<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>
|
||||
<div class="menu-bereich">Fall</div>
|
||||
<button class="studio-menu-item" data-art="export" onclick="Studio.openTab('export')">
|
||||
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" 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-text">
|
||||
<span class="mi-title">Bericht exportieren</span>
|
||||
<span class="mi-meta"></span>
|
||||
</span>
|
||||
</button>
|
||||
<button class="studio-menu-item" data-art="settings" onclick="Studio.openTab('settings')">
|
||||
<span class="mi-icon"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg></span>
|
||||
<span class="mi-text">
|
||||
<span class="mi-title">Einstellungen</span>
|
||||
<span class="mi-meta"></span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -694,9 +648,9 @@
|
||||
<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=20260802b"></script>
|
||||
<script src="/static/js/components.js?v=20260802c"></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=20260802b"></script>
|
||||
<script src="/static/js/studio.js?v=20260802c"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
314
tests/test_lauf.js
Normale Datei
314
tests/test_lauf.js
Normale Datei
@@ -0,0 +1,314 @@
|
||||
/**
|
||||
* Testet die Lauf-Anzeige der Bausteinspalte ohne Browser.
|
||||
*
|
||||
* Die Methoden werden aus studio.js herausgeloest und gegen ein schlankes
|
||||
* DOM-Double laufen gelassen. Kein Netzzugriff, keine Kosten.
|
||||
*
|
||||
* Aufruf aus dem Projektstamm:
|
||||
* node tests/test_lauf.js
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const vm = require('vm');
|
||||
|
||||
let ok = 0;
|
||||
let fail = 0;
|
||||
|
||||
function pruefe(name, bedingung, extra) {
|
||||
if (bedingung) {
|
||||
ok++;
|
||||
console.log(' OK ' + name);
|
||||
} else {
|
||||
fail++;
|
||||
console.log(' FEHL ' + name + ' '
|
||||
+ (extra === undefined ? '' : JSON.stringify(extra)));
|
||||
}
|
||||
}
|
||||
|
||||
// --- DOM-Double ------------------------------------------------------------
|
||||
function element(id) {
|
||||
const klassen = new Set();
|
||||
const el = {
|
||||
id: id,
|
||||
innerHTML: '',
|
||||
textContent: '',
|
||||
title: '',
|
||||
hidden: false,
|
||||
disabled: false,
|
||||
classList: {
|
||||
add: (n) => klassen.add(n),
|
||||
remove: (n) => klassen.delete(n),
|
||||
toggle: (n, an) => { if (an) klassen.add(n); else klassen.delete(n); },
|
||||
contains: (n) => klassen.has(n),
|
||||
},
|
||||
_kinder: {},
|
||||
querySelector(sel) {
|
||||
const k = sel.replace('.', '');
|
||||
if (!this._kinder[k]) this._kinder[k] = element(k);
|
||||
return this._kinder[k];
|
||||
},
|
||||
};
|
||||
return el;
|
||||
}
|
||||
|
||||
const IDS = ['lauf-stand', 'ls-haupt', 'ls-neben', 'ls-btn', 'ls-hinweis',
|
||||
'lauf-kette', 'lauf-fortschritt', 'lauf-schritte', 'start-panel',
|
||||
'art-summary-meta', 'art-fc-meta', 'art-snap-meta', 'art-tl-meta'];
|
||||
const elemente = {};
|
||||
const meldungen = [];
|
||||
const aufrufe = [];
|
||||
|
||||
const umgebung = {
|
||||
document: {
|
||||
getElementById: (id) => elemente[id] || null,
|
||||
querySelectorAll: () => [],
|
||||
},
|
||||
UI: {
|
||||
showToast: (t, a) => meldungen.push({ t, a }),
|
||||
escape: (s) => String(s === undefined || s === null ? '' : s)
|
||||
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>'),
|
||||
},
|
||||
CSS: { escape: (s) => s },
|
||||
localStorage: { _d: {}, setItem(k, v) { this._d[k] = v; }, getItem(k) { return this._d[k]; } },
|
||||
console: console,
|
||||
};
|
||||
umgebung.window = umgebung;
|
||||
|
||||
// --- Den Lauf-Block aus studio.js herausloesen -----------------------------
|
||||
const quelle = fs.readFileSync(
|
||||
path.join(__dirname, '..', 'src', 'static', 'js', 'studio.js'), 'utf8');
|
||||
const von = quelle.indexOf(' // === Der Lauf (Spalte 3) ===');
|
||||
const bis = quelle.indexOf(' async runStage(stage) {', von);
|
||||
if (von === -1 || bis === -1) {
|
||||
console.log('FEHLER: Lauf-Block in studio.js nicht gefunden');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const kontext = vm.createContext(umgebung);
|
||||
const Studio = vm.runInContext(
|
||||
'var Studio = {\n' + quelle.slice(von, bis) + '\n'
|
||||
+ ' incident: null, fresh: null, _runningStage: null,\n'
|
||||
+ ' async runStage(s) { this._letzterStart = s; },\n'
|
||||
+ '};\nStudio;', kontext);
|
||||
|
||||
const LAGE = { id: 7, title: 'Ceuta', type: 'adhoc' };
|
||||
|
||||
// Vier Zustaende, wie sie im Betrieb wirklich vorkommen
|
||||
const FRISCH = {
|
||||
leer: { articles: 0, summary: {}, factcheck: {}, geoparse: {} },
|
||||
veraltet: { articles: 85, summary: { exists: true, last: '2026-08-02 18:10', pending: 3 },
|
||||
factcheck: { exists: true, last: '2026-08-02 16:00', facts: 19, pending: 3 },
|
||||
geoparse: { pending: 0 }, snapshots: 4, events: 12 },
|
||||
aktuell: { articles: 85, summary: { exists: true, last: '2026-08-02 18:40', pending: 0 },
|
||||
factcheck: { exists: true, last: '2026-08-02 18:41', facts: 19, pending: 0 },
|
||||
geoparse: { pending: 0 }, snapshots: 4, events: 12 },
|
||||
halb: { articles: 85, summary: { exists: false }, factcheck: { exists: false },
|
||||
geoparse: { pending: 85 } },
|
||||
};
|
||||
|
||||
function neu(zustand, ueber) {
|
||||
for (const id of IDS) elemente[id] = element(id);
|
||||
Studio.incident = Object.assign({}, LAGE, ueber || {});
|
||||
Studio.fresh = FRISCH[zustand] ? JSON.parse(JSON.stringify(FRISCH[zustand])) : null;
|
||||
Studio._runningStage = null;
|
||||
Studio._schrittOffen = null;
|
||||
Studio._letzterStart = null;
|
||||
meldungen.length = 0;
|
||||
aufrufe.length = 0;
|
||||
}
|
||||
|
||||
const g = (id) => elemente[id];
|
||||
const knopfText = () => g('ls-btn').querySelector('.ls-text').textContent;
|
||||
|
||||
async function main() {
|
||||
console.log('\nA) Der Startknopf richtet sich nach dem Zustand');
|
||||
neu('leer');
|
||||
Studio._renderLauf();
|
||||
pruefe('A1 leerer Fall lädt zum Start ein', knopfText() === 'Lauf starten', knopfText());
|
||||
pruefe('A2 der Stand sagt warum', g('ls-haupt').textContent.includes('Noch keine Artikel'),
|
||||
g('ls-haupt').textContent);
|
||||
pruefe('A3 der Kasten ist hervorgehoben', g('lauf-stand').classList.contains('betont'));
|
||||
pruefe('A4 mit einem Hinweis, was danach kommt',
|
||||
g('ls-hinweis').textContent.length > 10 && g('ls-hinweis').hidden === false);
|
||||
|
||||
neu('leer', { type: 'research' });
|
||||
Studio._renderLauf();
|
||||
pruefe('A5 bei einer Recherche heißt es Recherche starten',
|
||||
knopfText() === 'Recherche starten', knopfText());
|
||||
|
||||
neu('veraltet');
|
||||
Studio._renderLauf();
|
||||
pruefe('A6 bei neuen Artikeln heißt es aktualisieren',
|
||||
knopfText() === 'Jetzt aktualisieren', knopfText());
|
||||
pruefe('A7 der Stand nennt die Zahl',
|
||||
g('ls-haupt').textContent.includes('3 neue Artikel'), g('ls-haupt').textContent);
|
||||
pruefe('A8 und warnt', g('ls-haupt').classList.contains('warn'));
|
||||
|
||||
neu('aktuell');
|
||||
Studio._renderLauf();
|
||||
pruefe('A9 wenn alles frisch ist heißt es erneut durchlaufen',
|
||||
knopfText() === 'Erneut durchlaufen', knopfText());
|
||||
pruefe('A10 der Kasten ist dann ruhig', !g('lauf-stand').classList.contains('betont'));
|
||||
pruefe('A11 und der Hinweis verschwindet', g('ls-hinweis').hidden === true);
|
||||
pruefe('A12 der Bestand steht daneben',
|
||||
g('ls-neben').textContent === '85 Artikel im Bestand', g('ls-neben').textContent);
|
||||
|
||||
console.log('\nB) Während eines Laufs');
|
||||
neu('veraltet');
|
||||
Studio._runningStage = 'full';
|
||||
Studio._renderLauf();
|
||||
pruefe('B1 der Knopf bricht ab', knopfText() === 'Abbrechen', knopfText());
|
||||
pruefe('B2 er ist rot abgesetzt', g('ls-btn').classList.contains('abbrechen'));
|
||||
pruefe('B3 er bleibt klickbar', g('ls-btn').disabled === false);
|
||||
pruefe('B4 der Stand sagt, was läuft',
|
||||
g('ls-haupt').textContent.includes('Kompletter Lauf'), g('ls-haupt').textContent);
|
||||
|
||||
neu('veraltet');
|
||||
Studio._runningStage = 'analyze';
|
||||
Studio._renderLauf();
|
||||
pruefe('B5 ein nicht abbrechbarer Schritt sperrt den Knopf', g('ls-btn').disabled === true);
|
||||
pruefe('B6 und wird nicht als Abbruch angeboten',
|
||||
!g('ls-btn').classList.contains('abbrechen'));
|
||||
pruefe('B7 der Stand nennt den Schritt beim Ergebnisnamen',
|
||||
g('ls-haupt').textContent.includes('Lagebild schreiben'), g('ls-haupt').textContent);
|
||||
|
||||
console.log('\nC) Der Knopf trifft den richtigen Schritt');
|
||||
neu('veraltet');
|
||||
await Studio.laufKnopf();
|
||||
pruefe('C1 ohne Lauf startet er den kompletten Lauf', Studio._letzterStart === 'full');
|
||||
|
||||
neu('veraltet');
|
||||
Studio._runningStage = 'collect';
|
||||
await Studio.laufKnopf();
|
||||
pruefe('C2 ein laufendes Sammeln wird auch als Sammeln abgebrochen',
|
||||
Studio._letzterStart === 'collect', Studio._letzterStart);
|
||||
|
||||
neu('veraltet');
|
||||
Studio._runningStage = 'analyze';
|
||||
await Studio.laufKnopf();
|
||||
pruefe('C3 ein nicht abbrechbarer Schritt löst nichts aus',
|
||||
Studio._letzterStart === null, Studio._letzterStart);
|
||||
pruefe('C4 und sagt das auch', meldungen.length === 1 && meldungen[0].a === 'info', meldungen);
|
||||
|
||||
console.log('\nD) Die Kette');
|
||||
neu('aktuell');
|
||||
Studio._renderLauf();
|
||||
const kette = g('lauf-kette').innerHTML;
|
||||
pruefe('D1 vier Knoten', (kette.match(/class="kn /g) || []).length === 4,
|
||||
(kette.match(/class="kn /g) || []).length);
|
||||
pruefe('D2 drei Verbindungen', (kette.match(/kn-linie/g) || []).length === 3);
|
||||
pruefe('D3 alle vier erledigt', (kette.match(/kn-fertig/g) || []).length === 4);
|
||||
pruefe('D4 der Zähler stimmt',
|
||||
g('lauf-fortschritt').textContent === '4 von 4 Schritten auf dem neuesten Stand',
|
||||
g('lauf-fortschritt').textContent);
|
||||
|
||||
neu('veraltet');
|
||||
Studio._renderLauf();
|
||||
pruefe('D5 bei veralteten Ergebnissen sind nicht alle grün',
|
||||
(g('lauf-kette').innerHTML.match(/kn-fertig/g) || []).length === 2,
|
||||
(g('lauf-kette').innerHTML.match(/kn-fertig/g) || []).length);
|
||||
pruefe('D6 zwei stehen auf veraltet',
|
||||
(g('lauf-kette').innerHTML.match(/kn-veraltet/g) || []).length === 2);
|
||||
pruefe('D7 der Zähler zählt nur die fertigen',
|
||||
g('lauf-fortschritt').textContent.startsWith('2 von 4'),
|
||||
g('lauf-fortschritt').textContent);
|
||||
|
||||
neu('leer');
|
||||
Studio._renderLauf();
|
||||
pruefe('D8 ohne Artikel sind drei Schritte gesperrt',
|
||||
(g('lauf-kette').innerHTML.match(/kn-gesperrt/g) || []).length === 3,
|
||||
(g('lauf-kette').innerHTML.match(/kn-gesperrt/g) || []).length);
|
||||
|
||||
console.log('\nE) Die Schritte');
|
||||
neu('halb');
|
||||
Studio._renderLauf();
|
||||
const l = g('lauf-schritte').innerHTML;
|
||||
pruefe('E1 vier Schritte', (l.match(/class="schritt /g) || []).length === 4,
|
||||
(l.match(/class="schritt /g) || []).length);
|
||||
pruefe('E2 Ergebnisnamen statt Erzeugernamen',
|
||||
l.includes('Artikel holen') && l.includes('Lagebild schreiben')
|
||||
&& l.includes('Fakten prüfen') && l.includes('Orte erkennen'));
|
||||
pruefe('E3 keine Erzeugernamen mehr',
|
||||
!l.includes('Geoparsing') && !l.includes('>Sammeln<') && !l.includes('>Analyse<'));
|
||||
pruefe('E4 der erste Schritt ist erledigt', l.includes('schritt schritt-fertig'));
|
||||
pruefe('E5 die noch nicht erzeugten sind offen',
|
||||
(l.match(/schritt-offen/g) || []).length === 2, (l.match(/schritt-offen/g) || []).length);
|
||||
pruefe('E6 die Karte meldet offene Artikel',
|
||||
l.includes('85 Artikel noch nicht verortet'));
|
||||
|
||||
neu('aktuell', { type: 'research' });
|
||||
Studio._renderLauf();
|
||||
pruefe('E7 bei einer Recherche heißt Schritt zwei Recherchebericht',
|
||||
g('lauf-schritte').innerHTML.includes('Recherchebericht schreiben'));
|
||||
|
||||
console.log('\nF) Kein Schritt startet etwas');
|
||||
for (const z of ['leer', 'veraltet', 'aktuell', 'halb']) {
|
||||
neu(z);
|
||||
Studio._renderLauf();
|
||||
const h = g('lauf-schritte').innerHTML;
|
||||
pruefe('F ' + z + ' enthält keinen Startaufruf',
|
||||
!h.includes('runStage') && !h.includes('laufKnopf'));
|
||||
}
|
||||
|
||||
console.log('\nG) Aufklappen erklärt nur');
|
||||
neu('aktuell');
|
||||
Studio._renderLauf();
|
||||
pruefe('G1 anfangs ist nichts offen',
|
||||
!g('lauf-schritte').innerHTML.includes('schritt offen')
|
||||
&& !g('lauf-schritte').innerHTML.includes(' offen"'));
|
||||
Studio.klappSchritt(1);
|
||||
pruefe('G2 ein Klick öffnet den Schritt',
|
||||
g('lauf-schritte').innerHTML.includes(' offen"'), Studio._schrittOffen);
|
||||
pruefe('G3 und zeigt die Erklärung',
|
||||
g('lauf-schritte').innerHTML.includes('Wertet alle Artikel aus'));
|
||||
Studio.klappSchritt(1);
|
||||
pruefe('G4 nochmal klicken schließt ihn', Studio._schrittOffen === null);
|
||||
Studio.klappSchritt(0);
|
||||
Studio.klappSchritt(2);
|
||||
pruefe('G5 es ist immer nur einer offen', Studio._schrittOffen === 2);
|
||||
|
||||
console.log('\nH) Nebenangaben an den Ansichten');
|
||||
neu('aktuell');
|
||||
Studio._renderLauf();
|
||||
pruefe('H1 Lagebild nennt die Artikelzahl',
|
||||
g('art-summary-meta').textContent.includes('85 Artikel ausgewertet'),
|
||||
g('art-summary-meta').textContent);
|
||||
pruefe('H2 Faktencheck nennt die Fakten',
|
||||
g('art-fc-meta').textContent.includes('19 Fakten'), g('art-fc-meta').textContent);
|
||||
pruefe('H3 frühere Berichte werden gezählt', g('art-snap-meta').textContent === '4');
|
||||
pruefe('H4 Ereignisse werden gezählt',
|
||||
g('art-tl-meta').textContent === '12 Ereignisse', g('art-tl-meta').textContent);
|
||||
|
||||
neu('leer');
|
||||
Studio._renderLauf();
|
||||
pruefe('H5 ohne Lauf steht noch nicht erzeugt',
|
||||
g('art-summary-meta').textContent === 'noch nicht erzeugt',
|
||||
g('art-summary-meta').textContent);
|
||||
|
||||
console.log('\nI) Ohne offenen Fall');
|
||||
neu('leer');
|
||||
Studio.incident = null;
|
||||
Studio.fresh = null;
|
||||
Studio._renderLauf();
|
||||
pruefe('I1 der Knopf ist gesperrt', g('ls-btn').disabled === true);
|
||||
pruefe('I2 der Stand sagt es', g('ls-haupt').textContent === 'Kein Fall geöffnet',
|
||||
g('ls-haupt').textContent);
|
||||
pruefe('I3 die Kette ist leer', g('lauf-kette').innerHTML === '');
|
||||
pruefe('I4 die Schrittliste ist leer', g('lauf-schritte').innerHTML === '');
|
||||
pruefe('I5 nichts stürzt ab', true);
|
||||
|
||||
console.log('\nJ) Ohne Frischedaten');
|
||||
neu('leer');
|
||||
Studio.fresh = null;
|
||||
Studio._renderLauf();
|
||||
pruefe('J1 die Anzeige bleibt bedienbar', g('ls-btn').disabled === false);
|
||||
pruefe('J2 und behandelt den Fall wie leer',
|
||||
g('ls-haupt').textContent.includes('Noch keine Artikel'));
|
||||
|
||||
console.log('\nErgebnis: ' + ok + ' bestanden, ' + fail + ' fehlgeschlagen');
|
||||
process.exit(fail ? 1 : 0);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -39,6 +39,8 @@ studio_js = lies("js", "studio.js")
|
||||
dash_html = lies("dashboard.html")
|
||||
app_js = lies("js", "app.js")
|
||||
comp_js = lies("js", "components.js")
|
||||
style = lies("css", "style.css")
|
||||
studio_css = lies("css", "studio.css")
|
||||
|
||||
|
||||
def ids(html):
|
||||
@@ -106,8 +108,31 @@ for name, html in [("studio.html", studio_html), ("dashboard.html", dash_html)]:
|
||||
# Geprueft wird nur, dass die in diesem Zug geaenderten Dateien mitgezogen sind.
|
||||
eigene = re.findall(r"/static/(?:js|css)/(studio|app|style|components|studio)\.(?:js|css)\?v=([0-9a-z]+)", html)
|
||||
pruefe(f"E {name} nennt Stempel", bool(stempel), stempel)
|
||||
pruefe(f"E {name} geaenderte Dateien tragen 20260802b",
|
||||
all(v == "20260802b" for _, v in eigene), eigene)
|
||||
# Kein fester Wert. Geprueft wird, dass die in einem Zug geaenderten
|
||||
# Dateien denselben Stempel tragen, sonst muesste der Test bei jedem
|
||||
# Deploy mitgepflegt werden.
|
||||
stempel_eigen = {v for _, v in eigene}
|
||||
pruefe(f"E {name} alle eigenen Dateien tragen denselben Stempel",
|
||||
len(stempel_eigen) == 1, sorted(stempel_eigen))
|
||||
|
||||
print("\nE2) Die Bausteinspalte hat genau einen Startpunkt")
|
||||
_i = studio_html.index('id="studio-menu"')
|
||||
menue = studio_html[_i:studio_html.index("</section>", _i)]
|
||||
starts = re.findall(r'data-stage="([a-z]+)"', menue)
|
||||
pruefe("E2a genau ein Element startet einen Lauf", len(starts) == 1, starts)
|
||||
pruefe("E2b und zwar der komplette Lauf", starts == ["full"], starts)
|
||||
pruefe("E2c auch im ganzen Dokument nur dieses eine",
|
||||
len(re.findall(r"data-stage=", studio_html)) == 1,
|
||||
len(re.findall(r"data-stage=", studio_html)))
|
||||
pruefe("E2d die Ansichten starten nichts", "Studio.runStage" not in studio_html)
|
||||
for tot in ["art-card", "art-run", "art-note", "run-all", "collect-btn", "art-foot"]:
|
||||
pruefe(f"E2 {tot} ist restlos weg",
|
||||
tot not in studio_html and tot not in studio_js and tot not in studio_css)
|
||||
pruefe("E2e die Schritte tragen Ergebnisnamen",
|
||||
"Artikel holen" in studio_js and "Orte erkennen" in studio_js
|
||||
and "Fakten prüfen" in studio_js)
|
||||
pruefe("E2f Geoparsing steht nicht mehr in der Oberflaeche",
|
||||
"Geoparsing" not in studio_html)
|
||||
|
||||
print("\nF) Tags im neuen Panel sind ausgeglichen")
|
||||
i = studio_html.index('<div class="tab-panel" data-art="settings">')
|
||||
@@ -119,8 +144,6 @@ for tag in ["div", "select", "label", "textarea"]:
|
||||
pruefe(f"F {tag} ausgeglichen ({auf})", auf == zu, (auf, zu))
|
||||
|
||||
print("\nG) Das Kennzeichen nutzt vorhandene Stile")
|
||||
style = lies("css", "style.css")
|
||||
studio_css = lies("css", "studio.css")
|
||||
pruefe("G1 incident-ai-badge ist definiert", ".incident-ai-badge" in style)
|
||||
pruefe("G2 case-ai-tag ist definiert", ".case-ai-tag" in studio_css)
|
||||
pruefe("G3 studio.html bindet style.css ein", "/static/css/style.css" in studio_html)
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren