Per-User Domain-Ausschlüsse + Grundquellen-Schutz
- Neue Tabelle user_excluded_domains für benutzerspezifische Ausschlüsse - Domain-Ausschlüsse wirken nur für den jeweiligen User, nicht org-weit - user_id wird durch die gesamte Pipeline geschleust (Orchestrator → Researcher → RSS-Parser) - Grundquellen (is_global) können nicht mehr bearbeitet/gelöscht werden im Frontend - Grundquelle-Badge bei globalen Quellen statt Edit/Delete-Buttons - Filter Von mir ausgeschlossen im Quellen-Modal Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -152,6 +152,10 @@ const API = {
|
||||
return this._request('POST', '/sources/discover-multi', { url });
|
||||
},
|
||||
|
||||
getMyExclusions() {
|
||||
return this._request('GET', '/sources/my-exclusions');
|
||||
},
|
||||
|
||||
blockDomain(domain, notes) {
|
||||
return this._request('POST', '/sources/block-domain', { domain, notes });
|
||||
},
|
||||
|
||||
@@ -425,7 +425,7 @@ const App = {
|
||||
_currentUsername: '',
|
||||
_allSources: [],
|
||||
_sourcesOnly: [],
|
||||
_blacklistOnly: [],
|
||||
_myExclusions: [], // [{domain, notes, created_at}]
|
||||
_expandedGroups: new Set(),
|
||||
_editingSourceId: null,
|
||||
_timelineFilter: 'all',
|
||||
@@ -2173,13 +2173,14 @@ const App = {
|
||||
|
||||
async loadSources() {
|
||||
try {
|
||||
const [sources, stats] = await Promise.all([
|
||||
const [sources, stats, myExclusions] = await Promise.all([
|
||||
API.listSources(),
|
||||
API.getSourceStats(),
|
||||
API.getMyExclusions(),
|
||||
]);
|
||||
this._allSources = sources;
|
||||
this._sourcesOnly = sources.filter(s => s.source_type !== 'excluded');
|
||||
this._blacklistOnly = sources.filter(s => s.source_type === 'excluded');
|
||||
this._myExclusions = myExclusions || [];
|
||||
|
||||
this.renderSourceStats(stats);
|
||||
this.renderSourceList();
|
||||
@@ -2194,7 +2195,7 @@ const App = {
|
||||
|
||||
const rss = stats.by_type.rss_feed || { count: 0, articles: 0 };
|
||||
const web = stats.by_type.web_source || { count: 0, articles: 0 };
|
||||
const excluded = this._blacklistOnly.length;
|
||||
const excluded = this._myExclusions.length;
|
||||
|
||||
bar.innerHTML = `
|
||||
<span class="sources-stat-item"><span class="sources-stat-value">${rss.count}</span> RSS-Feeds</span>
|
||||
@@ -2221,12 +2222,12 @@ const App = {
|
||||
const excludedDomains = new Set();
|
||||
const excludedNotes = {};
|
||||
|
||||
// Blacklist-Domains sammeln
|
||||
this._blacklistOnly.forEach(s => {
|
||||
const domain = (s.domain || s.name || '').toLowerCase();
|
||||
// User-Ausschlüsse sammeln
|
||||
this._myExclusions.forEach(e => {
|
||||
const domain = (e.domain || '').toLowerCase();
|
||||
if (domain) {
|
||||
excludedDomains.add(domain);
|
||||
excludedNotes[domain] = s.notes || '';
|
||||
excludedNotes[domain] = e.notes || '';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2238,10 +2239,10 @@ const App = {
|
||||
});
|
||||
|
||||
// Ausgeschlossene Domains die keine Feeds haben auch als Gruppe
|
||||
this._blacklistOnly.forEach(s => {
|
||||
const domain = (s.domain || s.name || '').toLowerCase();
|
||||
this._myExclusions.forEach(e => {
|
||||
const domain = (e.domain || '').toLowerCase();
|
||||
if (domain && !groups.has(domain)) {
|
||||
groups.set(domain, [s]);
|
||||
groups.set(domain, []);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2249,6 +2250,7 @@ const App = {
|
||||
let filteredGroups = [];
|
||||
for (const [domain, feeds] of groups) {
|
||||
const isExcluded = excludedDomains.has(domain);
|
||||
const isGlobal = feeds.some(f => f.is_global);
|
||||
|
||||
// Typ-Filter
|
||||
if (typeFilter === 'excluded' && !isExcluded) continue;
|
||||
@@ -2271,7 +2273,7 @@ const App = {
|
||||
if (!groupText.includes(search)) continue;
|
||||
}
|
||||
|
||||
filteredGroups.push({ domain, feeds, isExcluded });
|
||||
filteredGroups.push({ domain, feeds, isExcluded, isGlobal });
|
||||
}
|
||||
|
||||
if (filteredGroups.length === 0) {
|
||||
@@ -2286,7 +2288,7 @@ const App = {
|
||||
});
|
||||
|
||||
list.innerHTML = filteredGroups.map(g =>
|
||||
UI.renderSourceGroup(g.domain, g.feeds, g.isExcluded, excludedNotes[g.domain] || '')
|
||||
UI.renderSourceGroup(g.domain, g.feeds, g.isExcluded, excludedNotes[g.domain] || '', g.isGlobal)
|
||||
).join('');
|
||||
|
||||
// Erweiterte Gruppen wiederherstellen
|
||||
@@ -2440,11 +2442,11 @@ const App = {
|
||||
* Domain direkt ausschließen (aus der Gruppenliste).
|
||||
*/
|
||||
async blockDomainDirect(domain) {
|
||||
if (!await confirmDialog(`"${domain}" wirklich ausschließen? Alle Feeds dieser Domain werden deaktiviert.`)) return;
|
||||
if (!await confirmDialog(`"${domain}" wirklich ausschließen? Artikel dieser Domain werden bei deinen Recherchen ignoriert.`)) return;
|
||||
|
||||
try {
|
||||
await API.blockDomain(domain);
|
||||
UI.showToast(`${domain} gesperrt.`, 'success');
|
||||
UI.showToast(`${domain} ausgeschlossen.`, 'success');
|
||||
await this.loadSources();
|
||||
this.updateSidebarStats();
|
||||
} catch (err) {
|
||||
@@ -2560,7 +2562,7 @@ const App = {
|
||||
|
||||
// Prüfen ob Domain ausgeschlossen ist
|
||||
const inputDomain = url.replace(/^https?:\/\//, '').replace(/^www\./, '').split('/')[0].toLowerCase();
|
||||
const isBlocked = inputDomain && this._blacklistOnly.some(s => (s.domain || '').toLowerCase() === inputDomain);
|
||||
const isBlocked = inputDomain && this._myExclusions.some(e => (e.domain || '').toLowerCase() === inputDomain);
|
||||
|
||||
if (isBlocked) {
|
||||
if (!await confirmDialog(`"${inputDomain}" ist ausgeschlossen. Trotzdem hinzufügen? Der Ausschluss wird dabei aufgehoben.`)) return;
|
||||
|
||||
@@ -529,7 +529,7 @@ const UI = {
|
||||
/**
|
||||
* Domain-Gruppe rendern (aufklappbar mit Feeds).
|
||||
*/
|
||||
renderSourceGroup(domain, feeds, isExcluded, excludedNotes) {
|
||||
renderSourceGroup(domain, feeds, isExcluded, excludedNotes, isGlobal) {
|
||||
const catLabel = this._categoryLabels[feeds[0]?.category] || feeds[0]?.category || '';
|
||||
const feedCount = feeds.filter(f => f.source_type !== 'excluded').length;
|
||||
const hasMultiple = feedCount > 1;
|
||||
@@ -547,7 +547,6 @@ const UI = {
|
||||
<span class="source-excluded-badge">Ausgeschlossen</span>
|
||||
<div class="source-group-actions">
|
||||
<button class="btn btn-small btn-secondary" onclick="App.unblockDomain('${escapedDomain}')">Ausschluss aufheben</button>
|
||||
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="Löschen" aria-label="Löschen">×</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
@@ -571,8 +570,8 @@ const UI = {
|
||||
<span class="source-feed-name">${this.escape(feed.name)}</span>
|
||||
<span class="source-type-badge type-${feed.source_type}">${typeLabel}</span>
|
||||
<span class="source-feed-url" title="${this.escape(feed.url || '')}">${this.escape(urlDisplay)}</span>
|
||||
<button class="source-edit-btn" onclick="App.editSource(${feed.id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>
|
||||
<button class="source-delete-btn" onclick="App.deleteSingleFeed(${feed.id})" title="Löschen" aria-label="Löschen">×</button>
|
||||
${!feed.is_global ? `<button class="source-edit-btn" onclick="App.editSource(${feed.id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>
|
||||
<button class="source-delete-btn" onclick="App.deleteSingleFeed(${feed.id})" title="Löschen" aria-label="Löschen">×</button>` : '<span class="source-global-badge">Grundquelle</span>'}
|
||||
</div>`;
|
||||
});
|
||||
feedRows += '</div>';
|
||||
@@ -591,9 +590,9 @@ const UI = {
|
||||
<span class="source-category-badge cat-${feeds[0]?.category || 'sonstige'}">${catLabel}</span>
|
||||
${feedCountBadge}
|
||||
<div class="source-group-actions" onclick="event.stopPropagation()">
|
||||
${!hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>` : ''}
|
||||
${!isGlobal && !hasMultiple && feeds[0]?.id ? `<button class="source-edit-btn" onclick="App.editSource(${feeds[0].id})" title="Bearbeiten" aria-label="Bearbeiten">✎</button>` : ''}
|
||||
<button class="btn btn-small btn-secondary" onclick="App.blockDomainDirect('${escapedDomain}')">Ausschließen</button>
|
||||
<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="Löschen" aria-label="Löschen">×</button>
|
||||
${!isGlobal ? `<button class="source-delete-btn" onclick="App.deleteDomain('${escapedDomain}')" title="Löschen" aria-label="Löschen">×</button>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
${feedRows}
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren