Commits vergleichen

..

4 Commits

Autor SHA1 Nachricht Datum
4a2d85d3b8 Promote develop → main (2026-05-01 16:09 UTC) 2026-05-01 18:09:11 +02:00
ad5b723d79 Quellenübersicht: Lagebild-Quellennummer [N] statt fortlaufender Nummer
Statt einer eigenen Nummerierung (1., 2., ...) wird jetzt die echte
Lagebild-Quellennummer im Format [N] angezeigt — also exakt das, was im
Lagebild-Text als Zitat erscheint. Match per exakter source_url, mit
Quellen-Name als Fallback.

Artikel ohne Match (nicht im Lagebild zitiert) bekommen einen dezenten
Strich "—" mit Tooltip "Nicht im Lagebild zitiert", damit sichtbar ist
welche Belege Claude überhaupt verwendet hat und welche nicht.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:04:52 +02:00
51615cae62 Quellenübersicht: Detail-Liste mit Nummer, Datum und Link
Aufklapp-Liste pro Quelle zeigt jetzt:
1. fortlaufende Nummer (gold, monospace)
2. Datum + Uhrzeit (klein, dezent grau, monospace)
3. Headline als Link zum Originalartikel

Drei-Spalten-Grid (Nummer | Datum | Headline). Auf schmalem Viewport
(<600px) klappt das Datum unter die Nummer. Bei research-Lagen wird
published_at bevorzugt, sonst collected_at.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 18:01:06 +02:00
a2610d0094 Quellenübersicht: Klick auf Quelle klappt Artikel-Liste auf
Quellen-Boxen waren bisher reine Anzeige. Jetzt sind sie klickbar:
beim Klick erscheint direkt unter der Box (über die volle Grid-Breite)
eine Liste der Artikel-Headlines dieser Quelle, jede mit Link zum
Originalartikel. Mutual-exclusive — Klick auf eine andere Quelle
schließt die vorherige automatisch.

- components.js: Item bekommt data-source, onclick + Tastatur-Support
  (Enter/Space), aria-expanded.
- app.js: toggleSourceOverviewDetail filtert _currentArticles nach
  Quelle, sortiert chronologisch absteigend, fügt das Detail-Element
  via insertAdjacentElement direkt nach dem geklickten Item ein.
- CSS: aktiver Item-Status (Glow + Tint), Detail-Block mit
  grid-column 1/-1 (volle Breite) + max-height 320px scrollbar bei
  vielen Artikeln + dezente Slide-In-Animation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 17:57:48 +02:00
4 geänderte Dateien mit 198 neuen und 4 gelöschten Zeilen

Datei anzeigen

@@ -1704,6 +1704,108 @@ a.dev-source-pill:hover {
border-radius: var(--radius);
background: var(--bg-primary);
border: 1px solid var(--border);
cursor: pointer;
transition: border-color 0.15s ease, background 0.15s ease;
outline: none;
}
.source-overview-item:hover {
border-color: var(--accent);
background: var(--bg-elevated);
}
.source-overview-item:focus-visible {
box-shadow: 0 0 0 2px var(--tint-accent-strong);
}
.source-overview-item.active {
border-color: var(--accent);
background: var(--tint-accent-subtle);
box-shadow: var(--glow-accent);
}
/* Inline-Aufklapp-Bereich (volle Reihen-Breite, direkt unter dem geklickten Item) */
.source-overview-detail {
grid-column: 1 / -1;
padding: var(--sp-md) var(--sp-lg);
background: var(--bg-elevated);
border: 1px solid var(--accent);
border-radius: var(--radius);
animation: source-detail-in 0.18s ease;
}
@keyframes source-detail-in {
from { opacity: 0; transform: translateY(-4px); }
to { opacity: 1; transform: translateY(0); }
}
.source-overview-detail-empty {
font-size: 12px;
color: var(--text-tertiary);
font-style: italic;
}
.source-overview-detail-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 4px;
max-height: 320px;
overflow-y: auto;
}
.source-overview-detail-list::-webkit-scrollbar { width: 6px; }
.source-overview-detail-list::-webkit-scrollbar-track { background: var(--bg-primary); border-radius: 3px; }
.source-overview-detail-list::-webkit-scrollbar-thumb { background: var(--text-disabled); border-radius: 3px; }
.source-overview-detail-list li {
font-size: 12px;
line-height: 1.4;
padding: 4px 0;
border-top: 1px dashed var(--border);
display: grid;
grid-template-columns: auto auto 1fr;
gap: var(--sp-md);
align-items: baseline;
}
.source-overview-detail-list li:first-child { border-top: none; }
.source-overview-detail-list li a {
color: var(--text-primary);
text-decoration: none;
}
.source-overview-detail-list li a:hover {
color: var(--accent);
text-decoration: underline;
}
.source-overview-detail-num {
font-family: var(--font-mono);
font-size: 11px;
font-weight: 700;
color: var(--accent);
min-width: 36px;
text-align: right;
white-space: nowrap;
}
.source-overview-detail-num--none {
color: var(--text-disabled);
font-weight: 400;
}
.source-overview-detail-date {
font-family: var(--font-mono);
font-size: 11px;
color: var(--text-tertiary);
white-space: nowrap;
}
.source-overview-detail-headline {
min-width: 0;
overflow-wrap: anywhere;
}
@media (max-width: 600px) {
.source-overview-detail-list li {
grid-template-columns: auto 1fr;
}
.source-overview-detail-date {
grid-column: 1 / -1;
margin-left: 32px;
}
}
@media (prefers-reduced-motion: reduce) {
.source-overview-detail { animation: none; }
.source-overview-item { transition: none; }
}
.source-overview-name {

Datei anzeigen

@@ -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=20260501e">
<link rel="stylesheet" href="/static/css/style.css?v=20260501h">
<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; }
@@ -647,7 +647,7 @@
<script src="/static/js/components.js?v=20260427a"></script>
<script src="/static/js/layout.js?v=20260316b"></script>
<script src="/static/js/pipeline.js?v=20260501a"></script>
<script src="/static/js/app.js?v=20260501e"></script>
<script src="/static/js/app.js?v=20260501h"></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=20260422a"></script>

Datei anzeigen

@@ -866,6 +866,97 @@ const App = {
}
},
/** Klick auf eine Quellen-Box: Liste der Artikel inline aufklappen (mutual-exclusive). */
toggleSourceOverviewDetail(el) {
if (!el) return;
const grid = el.parentElement;
if (!grid) return;
const sourceName = el.dataset.source || '';
const wasActive = el.classList.contains('active');
// Alle anderen schliessen + bestehendes Detail entfernen
grid.querySelectorAll('.source-overview-item.active').forEach(it => {
it.classList.remove('active');
it.setAttribute('aria-expanded', 'false');
});
const existingDetail = grid.querySelector('.source-overview-detail');
if (existingDetail) existingDetail.remove();
// Wenn das geklickte Item bereits aktiv war: nur schliessen
if (wasActive) return;
// Neues Detail einfuegen direkt nach dem geklickten Item
el.classList.add('active');
el.setAttribute('aria-expanded', 'true');
const type = this._currentIncidentType;
const getDate = (a) => (type === 'research' && a.published_at) ? a.published_at : (a.collected_at || a.published_at);
const articles = (this._currentArticles || [])
.filter(a => (a.source || 'Unbekannt') === sourceName)
.sort((a, b) => {
const ta = new Date(getDate(a) || 0).getTime();
const tb = new Date(getDate(b) || 0).getTime();
return tb - ta;
});
// Lagebild-Quellennummer pro Artikel ermitteln (matcht Artikel zu sources_json)
const normalize = (s) => (s || '').toLowerCase().replace(/^(der|die|das)\s+/, '').replace(/\s+/g, ' ').trim();
const sourcesList = this._currentSources || [];
const urlToNr = new Map();
sourcesList.forEach(s => {
if (s.url && s.nr != null) urlToNr.set(String(s.url).trim(), s.nr);
});
const findNr = (a) => {
// 1) Exakter URL-Match
if (a.source_url) {
const exact = urlToNr.get(String(a.source_url).trim());
if (exact != null) return exact;
}
// 2) Fallback: Match via Quellen-Namen (kann mehrfach treffen, nimm erstes)
if (a.source) {
const target = normalize(a.source);
const hit = sourcesList.find(s => s.nr != null && normalize(s.name) === target);
if (hit) return hit.nr;
}
return null;
};
const detail = document.createElement('div');
detail.className = 'source-overview-detail';
if (articles.length === 0) {
detail.innerHTML = '<div class="source-overview-detail-empty">Keine Artikel gefunden.</div>';
} else {
const fmtDate = (ts) => {
if (!ts) return '—';
try {
const d = new Date(ts);
if (isNaN(d.getTime())) return '—';
return d.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', timeZone: TIMEZONE })
+ ' '
+ d.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit', timeZone: TIMEZONE });
} catch (e) { return '—'; }
};
const items = articles.map(a => {
const nr = findNr(a);
const numHtml = nr != null
? `<span class="source-overview-detail-num">[${UI.escape(String(nr))}]</span>`
: `<span class="source-overview-detail-num source-overview-detail-num--none" title="Nicht im Lagebild zitiert">—</span>`;
const dateStr = fmtDate(getDate(a));
const headline = UI.escape(a.headline_de || a.headline || '(ohne Titel)');
const inner = a.source_url
? `<a href="${UI.escape(a.source_url)}" target="_blank" rel="noopener">${headline}</a>`
: headline;
return `<li>
${numHtml}
<span class="source-overview-detail-date">${UI.escape(dateStr)}</span>
<span class="source-overview-detail-headline">${inner}</span>
</li>`;
}).join('');
detail.innerHTML = `<ul class="source-overview-detail-list">${items}</ul>`;
}
el.insertAdjacentElement('afterend', detail);
},
/** Restliche Artikel seitenweise im Hintergrund nachladen und in _currentArticles mergen. */
async _loadRemainingArticlesInBackground(incidentId) {
const BATCH = 500;

Datei anzeigen

@@ -971,8 +971,9 @@ const UI = {
html += '<div class="source-overview-grid">';
data.sources.forEach(s => {
const langs = (s.languages || ['de']).map(l => (l || 'de').toUpperCase()).join('/');
html += `<div class="source-overview-item">
<span class="source-overview-name">${this.escape(s.source || 'Unbekannt')}</span>
const sourceName = this.escape(s.source || 'Unbekannt');
html += `<div class="source-overview-item" data-source="${sourceName}" tabindex="0" role="button" aria-expanded="false" onclick="App.toggleSourceOverviewDetail(this)" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();App.toggleSourceOverviewDetail(this);}">
<span class="source-overview-name">${sourceName}</span>
<span class="source-overview-lang">${langs}</span>
<span class="source-overview-count">${s.article_count}</span>
</div>`;