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:
@@ -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>`;
|
||||
const arts = f.articles || 0;
|
||||
const s = f.summary || {};
|
||||
const fc = f.factcheck || {};
|
||||
const geo = f.geoparse || {};
|
||||
|
||||
// Sammeln-Knopf spiegelt den Lauf mit
|
||||
const cb = document.getElementById('collect-btn');
|
||||
if (cb) cb.classList.toggle('running', run === 'collect');
|
||||
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();
|
||||
};
|
||||
|
||||
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`);
|
||||
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',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
// 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));
|
||||
/** 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 {
|
||||
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';
|
||||
}
|
||||
}
|
||||
this._setNote('network', 'blocked', 'Baustein folgt');
|
||||
this._setNote('timeline', 'fresh', 'entsteht automatisch aus den Artikeln');
|
||||
if (run || !f) return;
|
||||
|
||||
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;
|
||||
|
||||
// --- 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'));
|
||||
} else {
|
||||
this._setNote('geoparse', 'fresh', 'alle Artikel verortet');
|
||||
}
|
||||
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');
|
||||
|
||||
// --- 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`;
|
||||
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) {
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren