Promote develop → main (2026-05-22 13:45 UTC) #40
@@ -507,6 +507,14 @@ async def get_articles_sources_summary(
|
|||||||
d = dict(r)
|
d = dict(r)
|
||||||
langs = (d.pop("languages") or "de").split(",")
|
langs = (d.pop("languages") or "de").split(",")
|
||||||
d["languages"] = sorted({(l or "de").strip() for l in langs if l is not None})
|
d["languages"] = sorted({(l or "de").strip() for l in langs if l is not None})
|
||||||
|
# Quellentyp aus dem source-Praefix ableiten (fuer den Typ-Filter der Quellenuebersicht)
|
||||||
|
src = d.get("source") or ""
|
||||||
|
if src.startswith("X: "):
|
||||||
|
d["source_type"] = "x"
|
||||||
|
elif src.startswith("Telegram: "):
|
||||||
|
d["source_type"] = "telegram"
|
||||||
|
else:
|
||||||
|
d["source_type"] = "web"
|
||||||
sources.append(d)
|
sources.append(d)
|
||||||
# Sprach-Verteilung gesamt
|
# Sprach-Verteilung gesamt
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
|
|||||||
@@ -1715,6 +1715,39 @@ a.dev-source-pill:hover {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.source-type-filter-chips {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--sp-xs);
|
||||||
|
margin: var(--sp-sm) 0 var(--sp-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-type-filter-chip {
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-type-filter-chip:hover {
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-type-filter-chip.active {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.source-type-filter-chip.active strong {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.source-overview-grid {
|
.source-overview-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<link rel="stylesheet" href="/static/vendor/leaflet.css">
|
<link rel="stylesheet" href="/static/vendor/leaflet.css">
|
||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260501h">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260522c">
|
||||||
<style>
|
<style>
|
||||||
/* Export Modal Radio */
|
/* 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; }
|
.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; }
|
||||||
@@ -807,10 +807,10 @@
|
|||||||
<script src="/static/js/i18n.js?v=20260513a"></script>
|
<script src="/static/js/i18n.js?v=20260513a"></script>
|
||||||
<script src="/static/js/api.js?v=20260423a"></script>
|
<script src="/static/js/api.js?v=20260423a"></script>
|
||||||
<script src="/static/js/ws.js?v=20260316b"></script>
|
<script src="/static/js/ws.js?v=20260316b"></script>
|
||||||
<script src="/static/js/components.js?v=20260522b"></script>
|
<script src="/static/js/components.js?v=20260522c"></script>
|
||||||
<script src="/static/js/layout.js?v=20260513f"></script>
|
<script src="/static/js/layout.js?v=20260513f"></script>
|
||||||
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
||||||
<script src="/static/js/app.js?v=20260522a"></script>
|
<script src="/static/js/app.js?v=20260522c"></script>
|
||||||
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
||||||
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
||||||
<script src="/static/js/chat.js?v=20260514e"></script>
|
<script src="/static/js/chat.js?v=20260514e"></script>
|
||||||
|
|||||||
@@ -909,6 +909,26 @@ const App = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/** Quellenuebersicht der Lage nach Quellentyp filtern (Web/Telegram/X). */
|
||||||
|
filterSourceOverview(type, chipEl) {
|
||||||
|
const content = document.getElementById('source-overview-content');
|
||||||
|
if (!content) return;
|
||||||
|
content.querySelectorAll('.source-type-filter-chip').forEach(c => c.classList.remove('active'));
|
||||||
|
if (chipEl) chipEl.classList.add('active');
|
||||||
|
// ein offenes Detail-Panel schliessen
|
||||||
|
const det = content.querySelector('.source-overview-detail');
|
||||||
|
if (det) det.remove();
|
||||||
|
content.querySelectorAll('.source-overview-item.active').forEach(it => {
|
||||||
|
it.classList.remove('active');
|
||||||
|
it.setAttribute('aria-expanded', 'false');
|
||||||
|
});
|
||||||
|
// Quellen-Boxen nach Typ ein-/ausblenden
|
||||||
|
content.querySelectorAll('.source-overview-item').forEach(it => {
|
||||||
|
const t = it.dataset.type || 'web';
|
||||||
|
it.style.display = (!type || t === type) ? '' : 'none';
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
/** Klick auf eine Quellen-Box: Liste der Artikel inline aufklappen (mutual-exclusive). */
|
/** Klick auf eine Quellen-Box: Liste der Artikel inline aufklappen (mutual-exclusive). */
|
||||||
toggleSourceOverviewDetail(el) {
|
toggleSourceOverviewDetail(el) {
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|||||||
@@ -1034,11 +1034,32 @@ const UI = {
|
|||||||
html += `<div class="source-lang-chips">${langChips}</div>`;
|
html += `<div class="source-lang-chips">${langChips}</div>`;
|
||||||
html += `</div>`;
|
html += `</div>`;
|
||||||
|
|
||||||
|
// Typ-Filter-Chips (nur zeigen, wenn neben Web auch Telegram/X vorkommt)
|
||||||
|
const typeCounts = { web: 0, telegram: 0, x: 0 };
|
||||||
|
data.sources.forEach(s => {
|
||||||
|
const t = s.source_type || 'web';
|
||||||
|
typeCounts[t] = (typeCounts[t] || 0) + 1;
|
||||||
|
});
|
||||||
|
if (typeCounts.telegram > 0 || typeCounts.x > 0) {
|
||||||
|
const typeMeta = [
|
||||||
|
{ key: '', label: 'Alle', count: data.sources.length },
|
||||||
|
{ key: 'web', label: 'Web', count: typeCounts.web },
|
||||||
|
{ key: 'telegram', label: 'Telegram', count: typeCounts.telegram },
|
||||||
|
{ key: 'x', label: 'X', count: typeCounts.x },
|
||||||
|
];
|
||||||
|
const chips = typeMeta
|
||||||
|
.filter(t => t.key === '' || t.count > 0)
|
||||||
|
.map(t => `<button type="button" class="source-type-filter-chip${t.key === '' ? ' active' : ''}" data-type="${t.key}" onclick="App.filterSourceOverview('${t.key}', this)">${t.label} <strong>${t.count}</strong></button>`)
|
||||||
|
.join('');
|
||||||
|
html += `<div class="source-type-filter-chips">${chips}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
html += '<div class="source-overview-grid">';
|
html += '<div class="source-overview-grid">';
|
||||||
data.sources.forEach(s => {
|
data.sources.forEach(s => {
|
||||||
const langs = (s.languages || ['de']).map(l => (l || 'de').toUpperCase()).join('/');
|
const langs = (s.languages || ['de']).map(l => (l || 'de').toUpperCase()).join('/');
|
||||||
const sourceName = this.escape(s.source || 'Unbekannt');
|
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);}">
|
const sType = s.source_type || 'web';
|
||||||
|
html += `<div class="source-overview-item" data-source="${sourceName}" data-type="${sType}" 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-name">${sourceName}</span>
|
||||||
<span class="source-overview-lang">${langs}</span>
|
<span class="source-overview-lang">${langs}</span>
|
||||||
<span class="source-overview-count">${s.article_count}</span>
|
<span class="source-overview-count">${s.article_count}</span>
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren