diff --git a/src/static/css/style.css b/src/static/css/style.css index dbdd55c..40b1028 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -1064,6 +1064,103 @@ a:hover { white-space: nowrap; } +/* === Neueste Entwicklungen (Live-Monitoring) === */ +.dev-list { + display: flex; + flex-direction: column; + gap: var(--sp-sm); + white-space: normal; +} + +.dev-bullet { + background: var(--bg-elevated); + border-left: 3px solid var(--accent); + border-radius: var(--radius); + padding: var(--sp-md) var(--sp-lg); +} + +.dev-bullet-head { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--sp-md); + margin-bottom: var(--sp-xs); + flex-wrap: wrap; +} + +.dev-sources { + display: inline-flex; + flex-wrap: wrap; + gap: var(--sp-xs); + align-items: center; + min-width: 0; +} + +.dev-sources-empty { + color: var(--text-disabled); + font-size: 11px; + font-style: italic; +} + +.dev-source-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + background: var(--tint-accent); + color: var(--text-primary); + border-radius: 3px; + font-size: 11px; + font-weight: 500; + text-decoration: none; + line-height: 1.5; + transition: background 0.15s; + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +a.dev-source-pill:hover { + background: var(--tint-accent-strong); + text-decoration: none; + color: var(--text-primary); +} + +.dev-bias { + font-size: 9px; + padding: 1px 4px; + border-radius: 2px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.3px; + line-height: 1.4; +} + +.dev-bias-pro-ru { + background: var(--tint-error); + color: var(--error); +} + +.dev-bias-staatsnah { + background: var(--tint-warning); + color: var(--warning); +} + +.dev-time { + color: var(--text-tertiary); + font-size: 11px; + font-variant-numeric: tabular-nums; + white-space: nowrap; + flex-shrink: 0; +} + +.dev-body { + font-size: 14px; + line-height: 1.5; + color: var(--text-primary); +} + /* === Faktencheck Card === */ .factcheck-list { display: flex; diff --git a/src/static/js/app.js b/src/static/js/app.js index ff38356..95c9344 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -878,7 +878,7 @@ const App = { if (zusammenfassungCard) zusammenfassungCard.style.display = ''; const devText = (incident.latest_developments || '').trim(); if (devText) { - if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderZusammenfassung(devText, incident.sources_json); + if (zusammenfassungText) zusammenfassungText.innerHTML = UI.renderLatestDevelopments(devText, incident.sources_json); } else if (zusammenfassungText) { zusammenfassungText.innerHTML = 'Noch keine Entwicklungen erfasst. Wird beim n\u00e4chsten Refresh generiert.'; } diff --git a/src/static/js/components.js b/src/static/js/components.js index 1c55304..375eb97 100644 --- a/src/static/js/components.js +++ b/src/static/js/components.js @@ -744,6 +744,92 @@ const UI = { return html; }, + /** + * Rendert "Neueste Entwicklungen" für Live-Monitoring (adhoc). + * Erwartet Bullets im Format "- [DD.MM. HH:MM] Text [N][M]". + * Erzeugt Karten mit sichtbaren Quellen-Pills + dezentem Zeitstempel. + */ + renderLatestDevelopments(text, sourcesJson) { + if (!text) return 'Noch keine Entwicklungen erfasst.'; + let sources = []; + try { sources = JSON.parse(sourcesJson || '[]'); } catch(e) {} + + const bulletLines = text.split("\n").map(l => l.trim()).filter(l => l.startsWith("- ")); + if (bulletLines.length === 0) { + // Fallback: kein Bullet-Format erkannt, alten Render verwenden + return this.renderZusammenfassung(text, sourcesJson); + } + + const bulletRe = /^-\s*\[(\d{1,2}\.\d{1,2}\.)\s+(\d{1,2}:\d{2})\]\s*(.+?)\s*$/; + const citationRe = /\[(\d+[a-z]?)\]/g; + + const lookupSource = (num) => { + let src = sources.find(s => String(s.nr) === num || Number(s.nr) === Number(num)); + if (!src && /[a-z]$/.test(num)) { + const baseNum = num.replace(/[a-z]$/, ''); + src = sources.find(s => String(s.nr) === baseNum || Number(s.nr) === Number(baseNum)); + } + return src || null; + }; + + const cards = bulletLines.map(line => { + const m = bulletRe.exec(line); + if (!m) { + const body = this.escape(line.replace(/^-\s*/, '')); + return `
${body}
`; + } + const date = m[1]; + const time = m[2]; + const rawBody = m[3]; + + const nums = []; + let cm; + while ((cm = citationRe.exec(rawBody)) !== null) { + if (!nums.includes(cm[1])) nums.push(cm[1]); + } + citationRe.lastIndex = 0; + + const cleanBody = this.escape(rawBody.replace(citationRe, '').replace(/\s+/g, ' ').trim()); + + const pills = nums.map(num => { + const src = lookupSource(num); + if (!src) return ''; + const name = this.escape(src.name || `Quelle ${num}`); + const biasClass = this._classifyBias(src.name || ''); + const biasHtml = biasClass ? `${this._biasLabel(biasClass)}` : ''; + if (src.url) { + return `${name}${biasHtml}`; + } + return `${name}${biasHtml}`; + }).filter(Boolean).join(''); + + const sourcesHtml = pills + ? `${pills}` + : `Keine Quelle`; + const timeHtml = `${this.escape(time)} \u00b7 ${this.escape(date)}`; + + return `
${sourcesHtml}${timeHtml}
${cleanBody}
`; + }); + + return `
${cards.join('')}
`; + }, + + _classifyBias(name) { + if (!name) return null; + const n = name.toLowerCase(); + const proRu = ['rybar', 'sputnik', 'tass', 'ria novosti', 'ria.ru', 'tsargrad', 'readovka', 'pravda', 'russia today', 'rt.com', 'rt deutsch']; + const staatsnah = ['cctv', 'global times', 'xinhua', "people's daily", 'press tv', 'irna', 'kcna']; + if (proRu.some(k => n.includes(k))) return 'pro-ru'; + if (staatsnah.some(k => n.includes(k))) return 'staatsnah'; + return null; + }, + + _biasLabel(cls) { + if (cls === 'pro-ru') return 'pro-RU'; + if (cls === 'staatsnah') return 'staatsnah'; + return cls; + }, + renderSummary(summary, sourcesJson, incidentType) { if (!summary) return 'Noch keine Zusammenfassung.';