feat(public-mood): Stimmungs-Kachel aus Foren-Quellen
Eigene Pipeline-Stufe nach factcheck, vor summary, die Foren-Artikel (media_type='forum') zu einer Themen-Zusammenfassung verarbeitet. Wird als separate Dashboard-Kachel "Öffentliche Stimmung" angezeigt — getrennt von Lagebild und Faktencheck, damit anonyme Forenposts nicht mit belegter Faktenlage verwechselt werden. - DB-Migration: incidents.public_mood (TEXT) + public_mood_updated_at (TS). - pipeline_tracker: neuer Pipeline-Step "public_mood" (DE/EN-Labels). - analyzer.generate_public_mood: Haiku-Call der Foren-Beitraege pro Quelle gruppiert und 3-6 thematische Bullets erzeugt, mit expliziter Quellen- Herkunft pro Bullet. Bei zu duennem Material gibt's keinen Output. - orchestrator: neuer Schritt zwischen Factcheck und Summary. Laedt alle Foren-Artikel der Lage (via JOIN auf sources), uebergibt sie an den Stimmungs-Agent, speichert den Markdown-Text in incidents.public_mood. - Topic-Filter (analyzer.filter_relevant_articles) markiert Foren-Quellen mit [FORUM]-Tag und bekommt im Prompt die Regel, Foren-Artikel weicher zu bewerten (Lage-Keyword im Titel reicht). Sie sollen in der Stimmungs- Kachel landen, nicht voreilig verworfen werden. - IncidentResponse-Modell: public_mood/public_mood_updated_at ergaenzt. - Frontend: neuer Tab "Öffentliche Stimmung" (nur sichtbar wenn Inhalt da), eigene Kachel mit Warn-Hinweis "keine Faktenlage". UI.renderPublicMood als einfacher Bullet-Renderer. - dashboard.html Cache-Buster fuer components.js + app.js gebumpt. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -209,6 +209,7 @@
|
||||
<button class="tab-btn" data-tab="timeline" data-i18n="tab.timeline">Ereignis-Timeline</button>
|
||||
<button class="tab-btn" data-tab="karte" data-i18n="tab.map">Geografische Verteilung</button>
|
||||
<button class="tab-btn" data-tab="faktencheck" data-i18n="tab.factcheck">Faktencheck</button>
|
||||
<button class="tab-btn" data-tab="stimmung" data-i18n="tab.public_mood" id="tab-btn-stimmung" style="display:none;">Öffentliche Stimmung</button>
|
||||
<button class="tab-btn" data-tab="pipeline" data-i18n="tab.pipeline">Analysepipeline</button>
|
||||
<button class="tab-btn" data-tab="quellen" data-i18n="tab.sources_overview">Quellenübersicht</button>
|
||||
</div>
|
||||
@@ -293,6 +294,24 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-stimmung">
|
||||
<div class="card incident-analysis-stimmung" id="stimmung-card">
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
<span data-i18n="card.public_mood">Öffentliche Stimmung</span>
|
||||
<span class="info-icon" data-tooltip="Themen und Bruchlinien aus Foren-Quellen (z.B. 5ch, Hatena, Note). KEINE Faktenlage - reines Stimmungsmaterial. Beitraege sind anonym und koennen Trolling enthalten."><svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span>
|
||||
</div>
|
||||
<span class="stimmung-timestamp" id="stimmung-timestamp"></span>
|
||||
</div>
|
||||
<div id="stimmung-content">
|
||||
<div id="stimmung-text" class="summary-text" style="padding:8px 16px;"></div>
|
||||
<div style="padding:0 16px 16px; font-size:11px; color:var(--text-disabled); border-top:1px solid var(--border); margin-top:8px; padding-top:8px;">
|
||||
Hinweis: Forenbeiträge sind anonyme Online-Stimmungen, keine Faktenlage. Sie fließen nicht in den Faktencheck ein.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="panel-pipeline">
|
||||
<div class="card pipeline-card" id="pipeline-card">
|
||||
<div class="card-header">
|
||||
@@ -778,10 +797,10 @@
|
||||
<script src="/static/js/i18n.js?v=20260513a"></script>
|
||||
<script src="/static/js/api.js?v=20260423a"></script>
|
||||
<script src="/static/js/ws.js?v=20260316b"></script>
|
||||
<script src="/static/js/components.js?v=20260514e"></script>
|
||||
<script src="/static/js/components.js?v=20260522a"></script>
|
||||
<script src="/static/js/layout.js?v=20260513f"></script>
|
||||
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
||||
<script src="/static/js/app.js?v=20260514e"></script>
|
||||
<script src="/static/js/app.js?v=20260522a"></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>
|
||||
|
||||
@@ -1131,6 +1131,26 @@ const App = {
|
||||
: '';
|
||||
}
|
||||
|
||||
// Öffentliche Stimmung (Foren-Kachel): Tab + Inhalt nur einblenden,
|
||||
// wenn fuer diese Lage tatsaechlich Stimmungs-Text vorhanden ist.
|
||||
const stimmungTabBtn = document.getElementById('tab-btn-stimmung');
|
||||
const stimmungText = document.getElementById('stimmung-text');
|
||||
const stimmungTs = document.getElementById('stimmung-timestamp');
|
||||
const moodText = (incident.public_mood || '').trim();
|
||||
if (moodText && stimmungTabBtn) {
|
||||
stimmungTabBtn.style.display = '';
|
||||
if (stimmungText) stimmungText.innerHTML = UI.renderPublicMood(moodText);
|
||||
if (stimmungTs && incident.public_mood_updated_at) {
|
||||
const mUpd = parseUTC(incident.public_mood_updated_at);
|
||||
if (mUpd) {
|
||||
stimmungTs.textContent = `Stand: ${mUpd.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', timeZone: TIMEZONE })} ${mUpd.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE })} Uhr`;
|
||||
}
|
||||
}
|
||||
} else if (stimmungTabBtn) {
|
||||
stimmungTabBtn.style.display = 'none';
|
||||
if (stimmungText) stimmungText.innerHTML = '';
|
||||
}
|
||||
|
||||
{ const _e = document.getElementById('meta-refresh-mode'); if (_e) {
|
||||
if (incident.refresh_mode === 'auto' && incident.refresh_start_time) {
|
||||
const intervalText = App._formatInterval(incident.refresh_interval);
|
||||
|
||||
@@ -813,6 +813,26 @@ const UI = {
|
||||
return html;
|
||||
},
|
||||
|
||||
/**
|
||||
* Rendert die "Öffentliche Stimmung"-Kachel.
|
||||
* Eingabe ist Markdown mit "- "-Bullets (vom AnalyzerAgent.generate_public_mood).
|
||||
* Quellen-Pills brauchen wir hier nicht — die Bullet-Texte nennen die Foren-Herkunft
|
||||
* explizit ("auf 5ch /seiji/ ...", "Hatena-Kommentare betonen ...").
|
||||
*/
|
||||
renderPublicMood(text) {
|
||||
if (!text) return '<span style="color:var(--text-disabled);">Noch kein Stimmungsbild erfasst.</span>';
|
||||
const bulletLines = text.split("\n").map(l => l.trim()).filter(l => l.startsWith("- "));
|
||||
if (bulletLines.length === 0) {
|
||||
// Fliesstext-Fallback: HTML-escapen + Zeilenumbrueche
|
||||
return this.escape(text).replace(/\n/g, '<br>');
|
||||
}
|
||||
const items = bulletLines.map(l => {
|
||||
const body = l.replace(/^-\s+/, '');
|
||||
return `<li>${this.escape(body)}</li>`;
|
||||
}).join('');
|
||||
return `<ul style="margin:4px 0 4px 18px;line-height:1.7;">${items}</ul>`;
|
||||
},
|
||||
|
||||
/**
|
||||
* Rendert "Neueste Entwicklungen" für Live-Monitoring (adhoc).
|
||||
* Erwartet Bullets im Format "- [DD.MM. HH:MM] Text {Quelle1, Quelle2}".
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren