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:
@@ -332,7 +332,7 @@ class AgentOrchestrator:
|
||||
self._running = False
|
||||
logger.info("Agenten-Orchestrator gestoppt")
|
||||
|
||||
async def enqueue_refresh(self, incident_id: int, trigger_type: str = "manual") -> bool:
|
||||
async def enqueue_refresh(self, incident_id: int, trigger_type: str = "manual", user_id: int = None) -> bool:
|
||||
"""Refresh-Auftrag in die Queue stellen. Gibt False zurueck wenn bereits in Queue/aktiv."""
|
||||
if incident_id in self._queued_ids or self._current_task == incident_id:
|
||||
logger.info(f"Refresh fuer Lage {incident_id} uebersprungen: bereits aktiv/in Queue")
|
||||
@@ -341,7 +341,7 @@ class AgentOrchestrator:
|
||||
visibility, created_by, tenant_id = await self._get_incident_visibility(incident_id)
|
||||
|
||||
self._queued_ids.add(incident_id)
|
||||
await self._queue.put((incident_id, trigger_type))
|
||||
await self._queue.put((incident_id, trigger_type, user_id))
|
||||
queue_size = self._queue.qsize()
|
||||
logger.info(f"Refresh fuer Lage {incident_id} eingereiht (Queue: {queue_size}, Trigger: {trigger_type})")
|
||||
|
||||
@@ -386,7 +386,11 @@ class AgentOrchestrator:
|
||||
except asyncio.TimeoutError:
|
||||
continue
|
||||
|
||||
if len(item) == 3:
|
||||
incident_id, trigger_type, user_id = item
|
||||
else:
|
||||
incident_id, trigger_type = item
|
||||
user_id = None
|
||||
self._queued_ids.discard(incident_id)
|
||||
self._current_task = incident_id
|
||||
logger.info(f"Starte Refresh für Lage {incident_id} (Trigger: {trigger_type})")
|
||||
@@ -398,7 +402,7 @@ class AgentOrchestrator:
|
||||
try:
|
||||
for attempt in range(3):
|
||||
try:
|
||||
await self._run_refresh(incident_id, trigger_type=trigger_type, retry_count=attempt)
|
||||
await self._run_refresh(incident_id, trigger_type=trigger_type, retry_count=attempt, user_id=user_id)
|
||||
last_error = None
|
||||
break # Erfolg
|
||||
except asyncio.CancelledError:
|
||||
@@ -509,7 +513,7 @@ class AgentOrchestrator:
|
||||
await db.close()
|
||||
return visibility, created_by, tenant_id
|
||||
|
||||
async def _run_refresh(self, incident_id: int, trigger_type: str = "manual", retry_count: int = 0):
|
||||
async def _run_refresh(self, incident_id: int, trigger_type: str = "manual", retry_count: int = 0, user_id: int = None):
|
||||
"""Führt einen kompletten Refresh-Zyklus durch."""
|
||||
import aiosqlite
|
||||
from database import get_db
|
||||
@@ -604,7 +608,7 @@ class AgentOrchestrator:
|
||||
keywords = feed_sel_keywords
|
||||
articles = await rss_parser.search_feeds_selective(title, selected_feeds, keywords=keywords)
|
||||
else:
|
||||
articles = await rss_parser.search_feeds(title, international=international, tenant_id=tenant_id, keywords=keywords)
|
||||
articles = await rss_parser.search_feeds(title, international=international, tenant_id=tenant_id, keywords=keywords, user_id=user_id)
|
||||
|
||||
logger.info(f"RSS: {len(articles)} relevante Artikel gefunden (international={international})")
|
||||
return articles, feed_usage
|
||||
@@ -612,7 +616,7 @@ class AgentOrchestrator:
|
||||
async def _web_search_pipeline():
|
||||
"""Claude WebSearch-Recherche."""
|
||||
researcher = ResearcherAgent()
|
||||
results, usage = await researcher.search(title, description, incident_type, international=international)
|
||||
results, usage = await researcher.search(title, description, incident_type, international=international, user_id=user_id)
|
||||
logger.info(f"Claude-Recherche: {len(results)} Ergebnisse")
|
||||
return results, usage
|
||||
|
||||
|
||||
@@ -269,7 +269,7 @@ class ResearcherAgent:
|
||||
logger.warning(f"Keyword-Extraktion fehlgeschlagen: {e}")
|
||||
return None, None
|
||||
|
||||
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True) -> tuple[list[dict], ClaudeUsage | None]:
|
||||
async def search(self, title: str, description: str = "", incident_type: str = "adhoc", international: bool = True, user_id: int = None) -> tuple[list[dict], ClaudeUsage | None]:
|
||||
"""Sucht nach Informationen zu einem Vorfall."""
|
||||
from config import OUTPUT_LANGUAGE
|
||||
if incident_type == "research":
|
||||
@@ -290,7 +290,7 @@ class ResearcherAgent:
|
||||
articles = self._parse_response(result)
|
||||
|
||||
# Ausgeschlossene Quellen dynamisch aus DB laden
|
||||
excluded_sources = await self._get_excluded_sources()
|
||||
excluded_sources = await self._get_excluded_sources(user_id=user_id)
|
||||
|
||||
# Ausgeschlossene Quellen filtern
|
||||
filtered = []
|
||||
@@ -317,14 +317,23 @@ class ResearcherAgent:
|
||||
logger.error(f"Recherche-Fehler: {e}")
|
||||
return [], None
|
||||
|
||||
async def _get_excluded_sources(self) -> list[str]:
|
||||
"""Lädt ausgeschlossene Quellen aus der Datenbank."""
|
||||
async def _get_excluded_sources(self, user_id: int = None) -> list[str]:
|
||||
"""Laedt ausgeschlossene Quellen (global + per-User)."""
|
||||
try:
|
||||
from source_rules import get_source_rules
|
||||
from source_rules import get_source_rules, get_user_excluded_domains
|
||||
rules = await get_source_rules()
|
||||
return rules.get("excluded_domains", [])
|
||||
excluded = list(rules.get("excluded_domains", []))
|
||||
|
||||
# User-spezifische Ausschluesse hinzufuegen
|
||||
if user_id:
|
||||
user_excluded = await get_user_excluded_domains(user_id)
|
||||
for domain in user_excluded:
|
||||
if domain not in excluded:
|
||||
excluded.append(domain)
|
||||
|
||||
return excluded
|
||||
except Exception as e:
|
||||
logger.warning(f"Fallback auf config.py für Excluded Sources: {e}")
|
||||
logger.warning(f"Fallback auf config.py fuer Excluded Sources: {e}")
|
||||
from config import EXCLUDED_SOURCES
|
||||
return list(EXCLUDED_SOURCES)
|
||||
|
||||
|
||||
@@ -183,6 +183,15 @@ CREATE TABLE IF NOT EXISTS article_locations (
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_locations_incident ON article_locations(incident_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_article_locations_article ON article_locations(article_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_excluded_domains (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
domain TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(user_id, domain)
|
||||
);
|
||||
"""
|
||||
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ class RSSParser:
|
||||
cleaned = [w for w in words if not w.isdigit()]
|
||||
return cleaned if cleaned else words
|
||||
|
||||
async def search_feeds(self, search_term: str, international: bool = True, tenant_id: int = None, keywords: list[str] | None = None) -> list[dict]:
|
||||
async def search_feeds(self, search_term: str, international: bool = True, tenant_id: int = None, keywords: list[str] | None = None, user_id: int = None) -> list[dict]:
|
||||
"""Durchsucht RSS-Feeds nach einem Suchbegriff.
|
||||
|
||||
Args:
|
||||
@@ -50,6 +50,19 @@ class RSSParser:
|
||||
|
||||
rss_feeds = await self._get_rss_feeds(tenant_id=tenant_id)
|
||||
|
||||
# User-spezifische Ausschluesse anwenden
|
||||
if user_id:
|
||||
try:
|
||||
from source_rules import get_user_excluded_domains
|
||||
user_excluded = await get_user_excluded_domains(user_id)
|
||||
if user_excluded:
|
||||
for cat in rss_feeds:
|
||||
rss_feeds[cat] = [f for f in rss_feeds[cat]
|
||||
if not any(excl in (f.get("url", "") + f.get("name", "")).lower()
|
||||
for excl in user_excluded)]
|
||||
except Exception as e:
|
||||
logger.warning(f"User-Ausschluesse konnten nicht geladen werden: {e}")
|
||||
|
||||
# Feed-Kategorien filtern
|
||||
if international:
|
||||
categories = rss_feeds.keys()
|
||||
|
||||
@@ -124,6 +124,7 @@ class SourceResponse(BaseModel):
|
||||
article_count: int = 0
|
||||
last_seen_at: Optional[str] = None
|
||||
created_at: str
|
||||
is_global: bool = False
|
||||
|
||||
|
||||
# Source Discovery
|
||||
|
||||
@@ -550,7 +550,7 @@ async def trigger_refresh(
|
||||
await _check_incident_access(db, incident_id, current_user["id"], tenant_id)
|
||||
|
||||
from agents.orchestrator import orchestrator
|
||||
enqueued = await orchestrator.enqueue_refresh(incident_id)
|
||||
enqueued = await orchestrator.enqueue_refresh(incident_id, user_id=current_user["id"])
|
||||
|
||||
if not enqueued:
|
||||
return {"status": "skipped", "incident_id": incident_id}
|
||||
|
||||
@@ -57,7 +57,12 @@ async def list_sources(
|
||||
query += " ORDER BY source_type, category, name"
|
||||
cursor = await db.execute(query, params)
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
results = []
|
||||
for row in rows:
|
||||
d = dict(row)
|
||||
d["is_global"] = d.get("tenant_id") is None
|
||||
results.append(d)
|
||||
return results
|
||||
|
||||
|
||||
@router.get("/stats")
|
||||
@@ -285,64 +290,54 @@ async def rediscover_existing_endpoint(
|
||||
raise HTTPException(status_code=500, detail="Rediscovery fehlgeschlagen")
|
||||
|
||||
|
||||
@router.get("/my-exclusions")
|
||||
async def get_my_exclusions(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Gibt die vom aktuellen User ausgeschlossenen Domains zurück."""
|
||||
user_id = current_user["id"]
|
||||
cursor = await db.execute(
|
||||
"SELECT domain, notes, created_at FROM user_excluded_domains WHERE user_id = ? ORDER BY domain",
|
||||
(user_id,),
|
||||
)
|
||||
rows = await cursor.fetchall()
|
||||
return [dict(row) for row in rows]
|
||||
|
||||
|
||||
@router.post("/block-domain")
|
||||
async def block_domain(
|
||||
data: DomainActionRequest,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Domain ausschließen: Alle Feeds deaktivieren + excluded-Eintrag anlegen."""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
"""Domain fuer den aktuellen User ausschließen (per-User, nicht org-weit)."""
|
||||
user_id = current_user["id"]
|
||||
domain = data.domain.lower().strip()
|
||||
username = current_user["username"]
|
||||
|
||||
# Pruefen ob bereits ausgeschlossen
|
||||
cursor = await db.execute(
|
||||
"SELECT added_by FROM sources WHERE LOWER(domain) = ? AND source_type != 'excluded' AND status = 'active' AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
affected = await cursor.fetchall()
|
||||
for row in affected:
|
||||
ab = row["added_by"] or ""
|
||||
if ab != "system" and ab != username and ab != "":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Domain enthaelt Quellen anderer Nutzer",
|
||||
)
|
||||
|
||||
cursor = await db.execute(
|
||||
"UPDATE sources SET status = 'inactive' WHERE LOWER(domain) = ? AND source_type != 'excluded' AND tenant_id = ?",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
feeds_deactivated = cursor.rowcount
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT id FROM sources WHERE LOWER(domain) = ? AND source_type = 'excluded' AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(domain, tenant_id),
|
||||
"SELECT id FROM user_excluded_domains WHERE user_id = ? AND domain = ?",
|
||||
(user_id, domain),
|
||||
)
|
||||
existing = await cursor.fetchone()
|
||||
|
||||
if existing:
|
||||
excluded_id = existing["id"]
|
||||
if data.notes:
|
||||
await db.execute(
|
||||
"UPDATE sources SET notes = ? WHERE id = ?",
|
||||
(data.notes, excluded_id),
|
||||
"UPDATE user_excluded_domains SET notes = ? WHERE id = ?",
|
||||
(data.notes, existing["id"]),
|
||||
)
|
||||
else:
|
||||
cursor = await db.execute(
|
||||
"""INSERT INTO sources (name, url, domain, source_type, category, status, notes, added_by, tenant_id)
|
||||
VALUES (?, NULL, ?, 'excluded', 'sonstige', 'active', ?, ?, ?)""",
|
||||
(domain, domain, data.notes, current_user["username"], tenant_id),
|
||||
)
|
||||
excluded_id = cursor.lastrowid
|
||||
await db.commit()
|
||||
return {"domain": domain, "status": "already_excluded"}
|
||||
|
||||
await db.execute(
|
||||
"INSERT INTO user_excluded_domains (user_id, domain, notes) VALUES (?, ?, ?)",
|
||||
(user_id, domain, data.notes),
|
||||
)
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"feeds_deactivated": feeds_deactivated,
|
||||
"excluded_id": excluded_id,
|
||||
}
|
||||
return {"domain": domain, "status": "excluded"}
|
||||
|
||||
|
||||
@router.post("/unblock-domain")
|
||||
@@ -351,41 +346,18 @@ async def unblock_domain(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
db: aiosqlite.Connection = Depends(db_dependency),
|
||||
):
|
||||
"""Domain-Ausschluss aufheben: excluded-Eintrag loeschen + Feeds reaktivieren."""
|
||||
tenant_id = current_user.get("tenant_id")
|
||||
"""Domain-Ausschluss fuer den aktuellen User aufheben."""
|
||||
user_id = current_user["id"]
|
||||
domain = data.domain.lower().strip()
|
||||
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) as cnt FROM sources WHERE LOWER(domain) = ? AND source_type != 'excluded' AND (tenant_id IS NULL OR tenant_id = ?)",
|
||||
(domain, tenant_id),
|
||||
"DELETE FROM user_excluded_domains WHERE user_id = ? AND domain = ?",
|
||||
(user_id, domain),
|
||||
)
|
||||
row = await cursor.fetchone()
|
||||
has_feeds = row["cnt"] > 0
|
||||
|
||||
if has_feeds:
|
||||
await db.execute(
|
||||
"DELETE FROM sources WHERE LOWER(domain) = ? AND source_type = 'excluded' AND tenant_id = ?",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
cursor = await db.execute(
|
||||
"UPDATE sources SET status = 'active' WHERE LOWER(domain) = ? AND source_type != 'excluded' AND tenant_id = ?",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
feeds_reactivated = cursor.rowcount
|
||||
else:
|
||||
await db.execute(
|
||||
"""UPDATE sources SET source_type = 'web_source', status = 'active', notes = 'Ausschluss aufgehoben'
|
||||
WHERE LOWER(domain) = ? AND source_type = 'excluded' AND (tenant_id IS NULL OR tenant_id = ?)""",
|
||||
(domain, tenant_id),
|
||||
)
|
||||
feeds_reactivated = 0
|
||||
|
||||
removed = cursor.rowcount
|
||||
await db.commit()
|
||||
|
||||
return {
|
||||
"domain": domain,
|
||||
"feeds_reactivated": feeds_reactivated,
|
||||
}
|
||||
return {"domain": domain, "removed": removed > 0}
|
||||
|
||||
|
||||
@router.delete("/domain/{domain}")
|
||||
|
||||
@@ -661,6 +661,24 @@ async def get_feeds_with_metadata(tenant_id: int = None) -> list[dict]:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_user_excluded_domains(user_id: int) -> list[str]:
|
||||
"""Laedt die vom User ausgeschlossenen Domains."""
|
||||
from database import get_db
|
||||
|
||||
db = await get_db()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"SELECT domain FROM user_excluded_domains WHERE user_id = ?",
|
||||
(user_id,),
|
||||
)
|
||||
return [row[0] for row in await cursor.fetchall()]
|
||||
except Exception as e:
|
||||
logger.warning(f"Fehler beim Laden der User-Ausschluesse: {e}")
|
||||
return []
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_source_rules(tenant_id: int = None) -> dict:
|
||||
"""Liest Quellen-Konfiguration aus DB (global + org-spezifisch).
|
||||
|
||||
|
||||
@@ -3437,6 +3437,16 @@ a:hover {
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
/* Grundquelle-Badge */
|
||||
.source-global-badge {
|
||||
font-size: 10px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
background: var(--bg-tertiary, #2a2a2a);
|
||||
color: var(--text-secondary, #888);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Ausgeschlossene Domain */
|
||||
.source-group-header.excluded {
|
||||
grid-template-columns: 1fr auto auto;
|
||||
|
||||
@@ -427,7 +427,7 @@
|
||||
<option value="">Alle Typen</option>
|
||||
<option value="rss_feed">RSS-Feed</option>
|
||||
<option value="web_source">Web-Quelle</option>
|
||||
<option value="excluded">Ausgeschlossen</option>
|
||||
<option value="excluded">Von mir ausgeschlossen</option>
|
||||
</select>
|
||||
<label for="sources-filter-category" class="sr-only">Kategorie filtern</label>
|
||||
<select id="sources-filter-category" class="timeline-filter-select" onchange="App.filterSources()">
|
||||
|
||||
@@ -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