Articles: Paginierung, Timeline-Buckets, Sources-Summary-Endpunkt
Backend:
- GET /{id}/articles paginiert jetzt per limit/offset (Default 500,
Max 1000) und unterstuetzt optionalen search-Parameter (LIKE ueber
headline/source/content). Response-Shape: {total, articles}.
- Neuer Endpunkt GET /{id}/articles/sources-summary liefert pro Quelle
{source, article_count, languages} sowie language_counts gesamt —
serverseitige Aggregation, unabhaengig von Artikel-Paginierung.
- Neuer Endpunkt GET /{id}/articles/timeline-buckets?granularity=hour|day|week|month
aggregiert Artikel + Snapshot-Counts pro Zeitbucket (fuer spaetere
Timeline-Zaehler ueber die volle Historie).
- database.py: Index idx_articles_incident_collected auf
(incident_id, collected_at DESC) fuer schnelleres ORDER BY + Pagination.
Frontend:
- api.js: getArticles({limit, offset, search}),
getArticlesSourcesSummary(), getArticlesTimelineBuckets().
- app.js: loadIncidentDetail laedt erste Seite (500 Artikel), startet
_loadSourcesSummary parallel und zieht restliche Artikel
batchweise (500er Bloecke) im Hintergrund nach, bis _currentArticlesTotal
erreicht ist. rerenderTimeline nach jedem Batch.
- components.js: renderSourceOverviewFromSummary(data) rendert aus
Aggregat-Daten (ersetzt clientseitige Zaehlung ueber geladene Artikel).
Hintergrund: /articles lieferte bei der Iran-Lage 22 MB (17.286 Artikel
mit SELECT *). Die Erstantwort sinkt auf ~650 KB (500 Artikel), weitere
werden progressiv im Hintergrund nachgeladen. Quellenuebersicht zeigt
dank Aggregat-Endpunkt sofort alle Quellen + Sprachen komplett.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -930,6 +930,37 @@ const UI = {
|
||||
/**
|
||||
* Quellenübersicht für eine Lage rendern.
|
||||
*/
|
||||
/**
|
||||
* Quellenuebersicht aus Aggregat-Endpunkt rendern (alle Artikel der Lage,
|
||||
* unabhaengig von Paginierung im Frontend).
|
||||
* data: {total, sources: [{source, article_count, languages: []}], language_counts: [{language, cnt}]}
|
||||
*/
|
||||
renderSourceOverviewFromSummary(data) {
|
||||
if (!data || !data.sources || data.sources.length === 0) return '';
|
||||
|
||||
const langChips = (data.language_counts || [])
|
||||
.map(l => `<span class="source-lang-chip">${(l.language || 'de').toUpperCase()} <strong>${l.cnt}</strong></span>`)
|
||||
.join('');
|
||||
|
||||
let html = `<div class="source-overview-header">`;
|
||||
html += `<span class="source-overview-stat">${data.total} Artikel aus ${data.sources.length} Quellen</span>`;
|
||||
html += `<div class="source-lang-chips">${langChips}</div>`;
|
||||
html += `</div>`;
|
||||
|
||||
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>
|
||||
<span class="source-overview-lang">${langs}</span>
|
||||
<span class="source-overview-count">${s.article_count}</span>
|
||||
</div>`;
|
||||
});
|
||||
html += '</div>';
|
||||
|
||||
return html;
|
||||
},
|
||||
|
||||
renderSourceOverview(articles) {
|
||||
if (!articles || articles.length === 0) return '';
|
||||
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren