From 0edbf7e3b8704c0755f3afa808ef97f3dd997092 Mon Sep 17 00:00:00 2001 From: AegisSight Promote-UI Date: Fri, 1 May 2026 15:22:13 +0200 Subject: [PATCH] =?UTF-8?q?Revert=20"Ereignis-Timeline:=20S=C3=A4ulen,=20L?= =?UTF-8?q?agebericht-Linien,=20Themen-Labels"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 370bb94b265d046a4c14774e0c775af472114f2c. --- src/static/css/style.css | 225 +++------------------------------------ src/static/js/app.js | 214 +++++++++---------------------------- 2 files changed, 64 insertions(+), 375 deletions(-) diff --git a/src/static/css/style.css b/src/static/css/style.css index d38956a..d944697 100644 --- a/src/static/css/style.css +++ b/src/static/css/style.css @@ -2453,25 +2453,25 @@ a.dev-source-pill:hover { /* Achsen-Container */ .ht-axis { position: relative; - height: 150px; + height: 110px; } /* Stündliches Layout: höher wegen Datums-Markern oben */ .ht-axis--hourly { - height: 170px; + height: 130px; } -/* Saeulen-Bereich (ueber der Linie) */ -.ht-bars { +/* Punkte-Bereich (über der Linie) */ +.ht-points { position: absolute; left: 4%; right: 4%; - top: 22px; - bottom: 60px; + top: 0; + height: 56px; } -.ht-axis--hourly .ht-bars { - top: 42px; +.ht-axis--hourly .ht-points { + top: 20px; } /* Achsenlinie */ @@ -2479,14 +2479,13 @@ a.dev-source-pill:hover { position: absolute; left: 2%; right: 2%; - top: 100px; + top: 60px; height: 2px; background: var(--border); - z-index: 3; } .ht-axis--hourly .ht-axis-line { - top: 120px; + top: 80px; } /* Datums-Marker (vertikale Linie + Datum oben, nur bei Stunden-Granularität) */ @@ -2640,12 +2639,12 @@ a.dev-source-pill:hover { position: absolute; left: 4%; right: 4%; - top: 110px; + top: 72px; height: 20px; } .ht-axis--hourly .ht-axis-labels { - top: 130px; + top: 90px; } .ht-axis-label { @@ -2665,206 +2664,6 @@ a.dev-source-pill:hover { color: var(--text-tertiary); } -/* === Säulen-Variante (neuer Stil) === */ -.ht-bar { - position: absolute; - bottom: 0; - width: 12px; - margin-left: -6px; - height: 100%; - cursor: pointer; - z-index: 5; - transition: transform 0.15s ease; -} -.ht-bar:hover { transform: scaleX(1.15); } -.ht-bar-fill { - position: absolute; - bottom: 0; - left: 0; - right: 0; - background: var(--text-disabled); - border-radius: 2px 2px 0 0; - transition: background 0.2s ease, height 0.2s ease; -} -.ht-bar:hover .ht-bar-fill, -.ht-bar.active .ht-bar-fill { background: var(--accent); } -.ht-bar-fill--empty { - height: 4px; - background: var(--border); -} -.ht-bar-snap-cap { - position: absolute; - top: -8px; - left: -2px; - right: -2px; - height: 6px; - background: var(--accent); - border-radius: 2px; - box-shadow: var(--glow-accent); -} -.ht-bars:has(.ht-bar.active) .ht-bar:not(.active) { opacity: 0.45; } - -/* Aktiver Bar: dezenter Marker oben */ -.ht-bar.active::before { - content: ''; - position: absolute; - bottom: 100%; - left: 50%; - transform: translateX(-50%); - width: 0; - height: 0; - border-left: 5px solid transparent; - border-right: 5px solid transparent; - border-top: 5px solid var(--accent); - margin-bottom: 4px; -} - -/* Themen-Label ueber aktiven Buckets */ -.ht-bucket-label { - position: absolute; - bottom: calc(100% + 14px); - left: 50%; - transform: translateX(-50%); - font-size: 10px; - font-family: var(--font-mono); - font-weight: 600; - color: var(--accent); - background: var(--bg-card); - padding: 1px 6px; - border-radius: 8px; - border: 1px solid var(--tint-accent-strong); - white-space: nowrap; - pointer-events: none; - opacity: 0.85; -} - -/* Hover-Vorschaukarte */ -.ht-hover-card { - position: absolute; - bottom: calc(100% + 8px); - left: 50%; - transform: translateX(-50%); - min-width: 240px; - max-width: 320px; - background: var(--bg-card); - border: 1px solid var(--accent); - border-radius: var(--radius); - box-shadow: var(--shadow-md); - padding: 8px 10px; - opacity: 0; - visibility: hidden; - transition: opacity 0.15s ease, visibility 0.15s ease; - pointer-events: none; - z-index: 20; - text-align: left; - white-space: normal; -} -.ht-bar:hover .ht-hover-card { opacity: 1; visibility: visible; } -.ht-hover-card-head { - font-size: 11px; - font-family: var(--font-mono); - color: var(--accent); - font-weight: 600; - margin-bottom: 4px; -} -.ht-hover-card-list { - list-style: none; - margin: 0; - padding: 0; - font-size: 12px; - color: var(--text-primary); - line-height: 1.4; -} -.ht-hover-card-list li { - padding: 2px 0; - border-top: 1px dashed var(--border); -} -.ht-hover-card-list li:first-child { border-top: none; } -.ht-hover-card-more { - margin-top: 4px; - font-size: 11px; - color: var(--text-tertiary); - font-style: italic; -} -.ht-hover-card-snap { - margin-top: 4px; - font-size: 11px; - color: var(--accent); - font-weight: 500; -} - -/* Lagebericht-Linien quer durch die Achse */ -.ht-snapshot-lines { - position: absolute; - left: 4%; - right: 4%; - top: 0; - bottom: 38px; - pointer-events: none; - z-index: 2; -} -.ht-snapshot-line { - position: absolute; - top: 0; - bottom: 0; - width: 1px; - margin-left: -1px; - background: var(--accent); - opacity: 0.35; - cursor: pointer; - pointer-events: auto; - transition: opacity 0.15s ease; -} -.ht-snapshot-line:hover { opacity: 0.85; } -.ht-snapshot-line-cap { - position: absolute; - top: -2px; - left: 50%; - transform: translateX(-50%); - width: 14px; - height: 14px; - background: var(--bg-card); - border: 1px solid var(--accent); - border-radius: 50%; - color: var(--accent); - display: flex; - align-items: center; - justify-content: center; -} - -/* "Heute"-Linie */ -.ht-today-line { - position: absolute; - left: 4%; - top: 0; - bottom: 38px; - width: 1px; - margin-left: -1px; - background: var(--accent); - opacity: 0.5; - border-left: 1px dashed var(--accent); - pointer-events: none; - z-index: 4; -} -.ht-today-label { - position: absolute; - top: 0; - left: 4px; - font-size: 9px; - font-family: var(--font-mono); - font-weight: 700; - color: var(--accent); - background: var(--bg-card); - padding: 0 4px; - border-radius: 2px; -} - -@media (prefers-reduced-motion: reduce) { - .ht-bar { transition: none; } - .ht-bar-fill { transition: none; } - .ht-detail-panel { animation: none; } -} - /* Detail-Panel */ .ht-detail-panel { margin-top: 8px; diff --git a/src/static/js/app.js b/src/static/js/app.js index bae85f8..79f6a02 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -1135,26 +1135,27 @@ const App = { entries.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0)); const granularity = this._calcGranularity(entries, range); - // Bei Filter "Lageberichte" zaehlen Snapshots; bei "Meldungen" nur Artikel; bei "Alle" beides. - const buckets = this._buildBuckets(entries, granularity); + let buckets = this._buildBuckets(entries, granularity); + buckets = this._mergeCloseBuckets(buckets); // Aktiven Index validieren if (this._activePointIndex !== null && this._activePointIndex >= buckets.length) { this._activePointIndex = null; } - // Achsen-Bereich (mit etwas Padding rechts/links, damit Bars nicht abgeschnitten werden) + // Achsen-Bereich const rangeStart = buckets[0].timestamp; const rangeEnd = buckets[buckets.length - 1].timestamp; - const articleMax = Math.max(1, ...buckets.map(b => b.entries.filter(e => e.kind === 'article').length)); + const maxCount = Math.max(...buckets.map(b => b.entries.length)); + // Stunden- vs. Tages-Granularität const isHourly = granularity === 'hour'; - const axisLabels = this._buildAxisLabels(buckets, granularity, isHourly); - const topActiveIdx = new Set(this._pickTopActiveBucketIdx(buckets, 3)); + const axisLabels = this._buildAxisLabels(buckets, granularity, true); - let html = `
`; + // HTML aufbauen + let html = `
`; - // Datums-Marker (Tageslinien, ausgeduennt) + // Datums-Marker (immer anzeigen, ausgedünnt) const dayMarkers = this._thinLabels(this._buildDayMarkers(buckets, rangeStart, rangeEnd), 10); html += '
'; dayMarkers.forEach(m => { @@ -1165,78 +1166,26 @@ const App = { }); html += '
'; - // Lagebericht-Linien quer durch die Achse (klickbar, oeffnet Snapshot) - html += '
'; - const snapshots = entries.filter(e => e.kind === 'snapshot'); - snapshots.forEach(snap => { - const ts = new Date(snap.timestamp || 0).getTime(); - if (rangeEnd === rangeStart) return; - const pos = ((ts - rangeStart) / (rangeEnd - rangeStart)) * 100; - if (pos < 0 || pos > 100) return; - const dateStr = new Date(snap.timestamp).toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE }); - html += `
`; - html += ``; - html += `
`; - }); - html += '
'; - - // "Heute"-Linie wenn heute im sichtbaren Bereich liegt - const now = Date.now(); - if (rangeEnd > rangeStart && now >= rangeStart && now <= rangeEnd) { - const todayPos = ((now - rangeStart) / (rangeEnd - rangeStart)) * 100; - html += ``; - } - - // Saeulen pro Bucket - html += '
'; + // Punkte + html += '
'; buckets.forEach((bucket, idx) => { const pos = this._bucketPositionPercent(bucket, rangeStart, rangeEnd, buckets.length); - const articleCount = bucket.entries.filter(e => e.kind === 'article').length; - const snapshotCount = bucket.entries.filter(e => e.kind === 'snapshot').length; - // Bar-Hoehe: Anteil articleCount/articleMax, mindestens 6%, maximal 100% - const heightPct = articleCount === 0 ? 0 : Math.max(6, Math.round((articleCount / articleMax) * 100)); - const isActive = this._activePointIndex === idx; - const showLabel = topActiveIdx.has(idx) && articleCount >= 2; - const keyword = showLabel ? this._extractBucketKeyword(bucket) : null; - const top3 = this._topRelevantHeadlines(bucket, 3); - const remaining = bucket.entries.filter(e => e.kind === 'article').length - top3.length; + const size = this._calcPointSize(bucket.entries.length, maxCount); + const hasSnapshots = bucket.entries.some(e => e.kind === 'snapshot'); + const hasArticles = bucket.entries.some(e => e.kind === 'article'); - let barClass = 'ht-bar'; - if (snapshotCount > 0) barClass += ' has-snapshot'; - if (articleCount === 0 && snapshotCount > 0) barClass += ' snapshot-only'; - if (isActive) barClass += ' active'; + let pointClass = 'ht-point'; + if (filterType === 'snapshots') { + pointClass += ' ht-snapshot-point'; + } else if (hasSnapshots) { + pointClass += ' ht-mixed-point'; + } + if (this._activePointIndex === idx) pointClass += ' active'; - html += `
`; - // Saeule selbst (gefuellt zur Hoehe) - if (articleCount > 0) { - html += `
`; - } else { - html += `
`; - } - if (snapshotCount > 0) { - html += ``; - } - // Themen-Label fuer aktivste Buckets - if (keyword) { - html += `
${UI.escape(keyword)}
`; - } - // Hover-Vorschaukarte - html += `
-
${UI.escape(bucket.label)} · ${articleCount + snapshotCount} Eintr${(articleCount + snapshotCount) === 1 ? 'ag' : 'äge'}
`; - if (top3.length > 0) { - html += '
    '; - top3.forEach(h => { html += `
  • ${UI.escape(h.length > 80 ? h.slice(0, 78) + '…' : h)}
  • `; }); - html += '
'; - if (remaining > 0) html += `
+${remaining} weitere
`; - } - if (snapshotCount > 0) { - html += `
+ ${snapshotCount} Lagebericht${snapshotCount === 1 ? '' : 'e'}
`; - } - html += `
`; + const tooltip = `${bucket.label}: ${bucket.entries.length} Eintr${bucket.entries.length === 1 ? 'ag' : 'äge'}`; + + html += `
`; + html += `
${UI.escape(tooltip)}
`; html += `
`; }); html += '
'; @@ -1244,7 +1193,7 @@ const App = { // Achsenlinie html += '
'; - // Achsen-Labels (ausgeduennt) + // Achsen-Labels (ausgedünnt um Überlappung zu vermeiden) const thinned = this._thinLabels(axisLabels); html += '
'; thinned.forEach(lbl => { @@ -1253,7 +1202,7 @@ const App = { html += '
'; html += '
'; - // Detail-Panel (wenn eine Bar aktiv ist) + // Detail-Panel (wenn ein Punkt aktiv ist) if (this._activePointIndex !== null && this._activePointIndex < buckets.length) { html += this._renderDetailPanel(buckets[this._activePointIndex]); } @@ -1261,36 +1210,13 @@ const App = { container.innerHTML = html; }, - /** Lagebericht direkt aus der Timeline-Snapshot-Linie oeffnen. - * Findet den passenden Bucket und expandiert dessen Detail-Panel. - */ - openSnapshotFromTimeline(snapshotId, evt) { - if (evt && evt.stopPropagation) evt.stopPropagation(); - // Bucket finden, der diesen Snapshot enthaelt - const filterType = this._timelineFilter; - const range = this._timelineRange; - const searchTerm = (document.getElementById('timeline-search')?.value || '').toLowerCase(); - let entries = this._collectEntries(filterType, searchTerm, range); - entries.sort((a, b) => new Date(a.timestamp || 0) - new Date(b.timestamp || 0)); - const granularity = this._calcGranularity(entries, range); - const buckets = this._buildBuckets(entries, granularity); - const idx = buckets.findIndex(b => b.entries.some(e => e.kind === 'snapshot' && e.data.id === snapshotId)); - if (idx === -1) return; - this._activePointIndex = idx; - this.rerenderTimeline(); - this._resizeTimelineTile(); - }, - _calcGranularity(entries, range) { if (entries.length < 2) return 'day'; const timestamps = entries.map(e => new Date(e.timestamp || 0).getTime()).filter(t => t > 0); if (timestamps.length < 2) return 'day'; const span = Math.max(...timestamps) - Math.min(...timestamps); - const DAY = 24 * 60 * 60 * 1000; if (range === '24h' || span <= 48 * 60 * 60 * 1000) return 'hour'; - if (range === '7d' || span <= 30 * DAY) return 'day'; - if (span <= 180 * DAY) return 'week'; - return 'month'; + return 'day'; }, _buildBuckets(entries, granularity) { @@ -1303,19 +1229,6 @@ const App = { key = `${b.year}-${b.month + 1}-${b.date}-${b.hours}`; label = d.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE }) + ', ' + b.hours.toString().padStart(2, '0') + ':00'; ts = new Date(b.year, b.month, b.date, b.hours).getTime(); - } else if (granularity === 'week') { - // ISO-Wochen-Start (Montag) - const weekStart = new Date(b.year, b.month, b.date); - const dow = (weekStart.getDay() + 6) % 7; // 0=Mo, 6=So - weekStart.setDate(weekStart.getDate() - dow); - const ws = _tz(weekStart); - key = `w-${ws.year}-${ws.month + 1}-${ws.date}`; - label = 'KW ' + weekStart.toLocaleDateString('de-DE', { day: '2-digit', month: 'short', timeZone: TIMEZONE }); - ts = new Date(ws.year, ws.month, ws.date, 12).getTime(); - } else if (granularity === 'month') { - key = `m-${b.year}-${b.month + 1}`; - label = d.toLocaleDateString('de-DE', { month: 'long', year: 'numeric', timeZone: TIMEZONE }); - ts = new Date(b.year, b.month, 15).getTime(); } else { key = `${b.year}-${b.month + 1}-${b.date}`; label = d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit', month: 'short', timeZone: TIMEZONE }); @@ -1329,56 +1242,33 @@ const App = { return Object.values(bucketMap).sort((a, b) => a.timestamp - b.timestamp); }, - /** Top-3 Headlines nach relevance_score (Fallback: Reihenfolge). */ - _topRelevantHeadlines(bucket, n = 3) { - const articles = bucket.entries - .filter(e => e.kind === 'article') - .map(e => e.data) - .sort((a, b) => (b.relevance_score || 0) - (a.relevance_score || 0)); - return articles.slice(0, n).map(a => a.headline_de || a.headline || '(ohne Titel)'); - }, + _mergeCloseBuckets(buckets) { + if (buckets.length < 2) return buckets; + const rangeStart = buckets[0].timestamp; + const rangeEnd = buckets[buckets.length - 1].timestamp; + if (rangeEnd <= rangeStart) return buckets; - /** Top-Keyword aus den Headlines des Buckets, mit deutscher Stopwort-Filterung. */ - _extractBucketKeyword(bucket) { - const STOP = this._timelineStopwords || (this._timelineStopwords = new Set([ - 'aber','alle','allem','allen','aller','alles','als','also','am','an','andere','anderem','anderen','ander','anders','auch','auf','aus','bei','beim','bin','bist','bis','bist','da','damit','dann','das','dass','dem','den','der','des','die','dies','diese','diesem','diesen','dieser','dieses','doch','dort','du','durch','ein','eine','einem','einen','einer','eines','er','es','etwa','etwas','euch','euer','eure','fuer','für','gegen','gibt','habe','haben','hat','hatte','hatten','hier','hin','hinter','ich','ihm','ihn','ihnen','ihr','ihre','ihrem','ihren','ihrer','ihres','im','in','indem','ins','ist','jede','jedem','jeden','jeder','jedes','jene','jenem','jenen','jener','jenes','jetzt','kann','kein','keine','keinem','keinen','keiner','keines','koennen','können','machen','man','manche','manchem','manchen','mancher','manches','mein','meine','meinem','meinen','meiner','meines','mit','muss','muessen','müssen','nach','nicht','nichts','noch','nun','nur','ob','oder','ohne','sehr','sein','seine','seinem','seinen','seiner','seines','seit','sich','sind','so','solche','solchem','solchen','solcher','solches','sollte','sondern','sonst','ueber','über','um','und','uns','unser','unsere','unter','viel','vom','von','vor','war','waren','was','weiter','welche','welchem','welchen','welcher','welches','wenn','werde','werden','wie','wieder','wir','wird','wirst','wo','wollen','wuerde','würde','zu','zum','zur','zwischen', - 'mehr','schon','beim','dem','dafür','dafuer','damit','jedoch','sowie','laut','rund','etwa','erst','dazu','dabei','ueber','daran','sowieso','seitdem','besonders','immer','heute','gestern','morgen','jahr','jahre','jahren','tag','tage','tagen','woche','wochen','monat','monate','jetzt', - 'the','and','for','with','from','that','this','have','has','was','were','been','will','would','could','should', - ])); - const counts = {}; - bucket.entries.forEach(e => { - const headline = (e.kind === 'article' ? (e.data.headline_de || e.data.headline) : '') || ''; - const words = headline.toLowerCase().match(/[a-zäöüß]{4,}/gi) || []; - words.forEach(w => { - if (STOP.has(w)) return; - counts[w] = (counts[w] || 0) + 1; - }); - }); - let bestWord = null; - let bestCount = 0; - Object.entries(counts).forEach(([w, c]) => { - if (c > bestCount) { - bestCount = c; - bestWord = w; + const container = document.getElementById('timeline'); + const axisWidth = (container ? container.offsetWidth : 800) * 0.92; + const maxCount = Math.max(...buckets.map(b => b.entries.length)); + const result = [buckets[0]]; + + for (let i = 1; i < buckets.length; i++) { + const prev = result[result.length - 1]; + const curr = buckets[i]; + + const distPx = ((curr.timestamp - prev.timestamp) / (rangeEnd - rangeStart)) * axisWidth; + const prevSize = Math.min(32, this._calcPointSize(prev.entries.length, maxCount)); + const currSize = Math.min(32, this._calcPointSize(curr.entries.length, maxCount)); + const minDistPx = (prevSize + currSize) / 2 + 6; + + if (distPx < minDistPx) { + prev.entries = prev.entries.concat(curr.entries); + } else { + result.push(curr); } - }); - if (!bestWord || bestCount < 2) return null; // braucht mindestens 2 Vorkommen - // Erstes Vorkommen mit Original-Casing zurueckgeben (statt all-lowercase) - for (const e of bucket.entries) { - const headline = (e.kind === 'article' ? (e.data.headline_de || e.data.headline) : '') || ''; - const m = headline.match(new RegExp('\\b(' + bestWord + ')\\b', 'i')); - if (m) return m[1]; } - return bestWord.charAt(0).toUpperCase() + bestWord.slice(1); - }, - - /** Indices der Top-N aktivsten Buckets (fuer Themen-Labels). */ - _pickTopActiveBucketIdx(buckets, n = 3) { - return buckets - .map((b, i) => ({ i, c: b.entries.filter(e => e.kind === 'article').length })) - .sort((a, b) => b.c - a.c) - .slice(0, n) - .map(x => x.i); + return result; }, _bucketPositionPercent(bucket, rangeStart, rangeEnd, totalBuckets) {