Neue Kategorie russische-opposition in Verwaltungs-UI + 10 neue Telegram-Kanaele (Wave 3)

Dieser Commit ist enthalten in:
Claude Dev
2026-03-13 19:08:44 +01:00
Ursprung 29f3e73480
Commit 1d9de549ec
7 geänderte Dateien mit 975 neuen und 850 gelöschten Zeilen

Datei anzeigen

@@ -24,7 +24,7 @@ SMTP_PORT = int(os.environ.get("SMTP_PORT", "587"))
SMTP_USER = os.environ.get("SMTP_USER", "") SMTP_USER = os.environ.get("SMTP_USER", "")
SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "") SMTP_PASSWORD = os.environ.get("SMTP_PASSWORD", "")
SMTP_FROM_EMAIL = os.environ.get("SMTP_FROM_EMAIL", "noreply@intelsight.de") SMTP_FROM_EMAIL = os.environ.get("SMTP_FROM_EMAIL", "noreply@intelsight.de")
SMTP_FROM_NAME = os.environ.get("SMTP_FROM_NAME", "IntelSight Verwaltung") SMTP_FROM_NAME = os.environ.get("SMTP_FROM_NAME", "AegisSight Verwaltung")
SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "true").lower() == "true" SMTP_USE_TLS = os.environ.get("SMTP_USE_TLS", "true").lower() == "true"
# Magic Link Base URL (fuer OSINT-Monitor Einladungen) # Magic Link Base URL (fuer OSINT-Monitor Einladungen)

Datei anzeigen

@@ -29,7 +29,7 @@ async def lifespan(app: FastAPI):
app = FastAPI( app = FastAPI(
title="IntelSight Verwaltungsportal", title="AegisSight Verwaltungsportal",
version="1.0.0", version="1.0.0",
lifespan=lifespan, lifespan=lifespan,
) )

Datei anzeigen

@@ -34,7 +34,7 @@ class GlobalSourceCreate(BaseModel):
name: str = Field(min_length=1, max_length=200) name: str = Field(min_length=1, max_length=200)
url: Optional[str] = None url: Optional[str] = None
domain: Optional[str] = None domain: Optional[str] = None
source_type: str = Field(default="rss_feed", pattern="^(rss_feed|web_source|excluded)$") source_type: str = Field(default="rss_feed", pattern="^(rss_feed|web_source|excluded|telegram_channel)$")
category: str = Field(default="sonstige") category: str = Field(default="sonstige")
status: str = Field(default="active", pattern="^(active|inactive)$") status: str = Field(default="active", pattern="^(active|inactive)$")
notes: Optional[str] = None notes: Optional[str] = None
@@ -44,7 +44,7 @@ class GlobalSourceUpdate(BaseModel):
name: Optional[str] = Field(default=None, max_length=200) name: Optional[str] = Field(default=None, max_length=200)
url: Optional[str] = None url: Optional[str] = None
domain: Optional[str] = None domain: Optional[str] = None
source_type: Optional[str] = Field(default=None, pattern="^(rss_feed|web_source|excluded)$") source_type: Optional[str] = Field(default=None, pattern="^(rss_feed|web_source|excluded|telegram_channel)$")
category: Optional[str] = None category: Optional[str] = None
status: Optional[str] = Field(default=None, pattern="^(active|inactive)$") status: Optional[str] = Field(default=None, pattern="^(active|inactive)$")
notes: Optional[str] = None notes: Optional[str] = None

Datei anzeigen

@@ -703,3 +703,63 @@ tr:hover td {
background: rgba(100, 116, 139, 0.2); background: rgba(100, 116, 139, 0.2);
color: #94a3b8; color: #94a3b8;
} }
/* Source Info Toggle */
.src-info-toggle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 13px;
cursor: pointer;
margin-right: 6px;
transition: background 0.15s, color 0.15s;
vertical-align: middle;
user-select: none;
}
.src-info-toggle:hover,
.src-info-toggle.active {
background: var(--accent);
color: #fff;
}
.src-notes-row .src-notes-cell {
padding: 10px 16px 12px 36px;
background: var(--bg-tertiary);
color: var(--text-secondary);
font-size: 12.5px;
line-height: 1.5;
border-bottom: 1px solid var(--border);
white-space: pre-wrap;
}
/* Category Header Rows in Source Table */
.cat-header-row td {
background: var(--bg-tertiary) !important;
border-bottom: 2px solid var(--accent);
padding: 10px 16px !important;
font-size: 0;
}
.cat-header-label {
font-size: 13px;
font-weight: 600;
color: var(--accent);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.cat-header-count {
font-size: 11px;
color: var(--text-secondary);
margin-left: 8px;
font-weight: 400;
}
.cat-header-count::before {
content: "(";
}
.cat-header-count::after {
content: ")";
}

Datei anzeigen

@@ -3,13 +3,13 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IntelSight Verwaltung</title> <title>AegisSight Monitor-Verwaltung</title>
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
</head> </head>
<body> <body>
<!-- Header --> <!-- Header -->
<header class="app-header"> <header class="app-header">
<div class="logo">IntelSight <span>Verwaltung</span></div> <div class="logo">AegisSight <span>Monitor-Verwaltung</span></div>
<div class="header-right"> <div class="header-right">
<span class="header-user" id="headerUser"></span> <span class="header-user" id="headerUser"></span>
<button class="btn btn-secondary btn-small" id="logoutBtn">Abmelden</button> <button class="btn btn-secondary btn-small" id="logoutBtn">Abmelden</button>
@@ -210,6 +210,7 @@
<option value="">Alle Typen</option> <option value="">Alle Typen</option>
<option value="rss_feed">RSS-Feed</option> <option value="rss_feed">RSS-Feed</option>
<option value="web_source">Webquelle</option> <option value="web_source">Webquelle</option>
<option value="telegram_channel">Telegram-Kanal</option>
</select> </select>
<select class="filter-select" id="globalFilterCategory" onchange="filterGlobalSources()"> <select class="filter-select" id="globalFilterCategory" onchange="filterGlobalSources()">
<option value="">Alle Kategorien</option> <option value="">Alle Kategorien</option>
@@ -223,6 +224,15 @@
<option value="regional">Regional</option> <option value="regional">Regional</option>
<option value="boulevard">Boulevard</option> <option value="boulevard">Boulevard</option>
<option value="sonstige">Sonstige</option> <option value="sonstige">Sonstige</option>
<option value="cybercrime">Cybercrime / Hacktivismus</option>
<option value="cybercrime-leaks">Cybercrime / Leaks</option>
<option value="ukraine-russland-krieg">Ukraine-Russland-Krieg</option>
<option value="irankonflikt">Irankonflikt</option>
<option value="osint-international">OSINT International</option>
<option value="extremismus-deutschland">Extremismus Deutschland</option>
<option value="russische-staatspropaganda">Russische Staatspropaganda</option>
<option value="russische-opposition">Russische Opposition / Exilmedien</option>
<option value="syrien-nahost">Syrien / Nahost</option>
</select> </select>
<select class="filter-select" id="globalFilterStatus" onchange="filterGlobalSources()"> <select class="filter-select" id="globalFilterStatus" onchange="filterGlobalSources()">
<option value="">Alle Status</option> <option value="">Alle Status</option>
@@ -243,7 +253,6 @@
<th>URL</th> <th>URL</th>
<th class="sortable" data-sort="domain" onclick="sortGlobalSources('domain')">Domain <span class="sort-icon"></span></th> <th class="sortable" data-sort="domain" onclick="sortGlobalSources('domain')">Domain <span class="sort-icon"></span></th>
<th class="sortable" data-sort="source_type" onclick="sortGlobalSources('source_type')">Typ <span class="sort-icon"></span></th> <th class="sortable" data-sort="source_type" onclick="sortGlobalSources('source_type')">Typ <span class="sort-icon"></span></th>
<th class="sortable" data-sort="category" onclick="sortGlobalSources('category')">Kategorie <span class="sort-icon"></span></th>
<th class="sortable" data-sort="article_count" onclick="sortGlobalSources('article_count')">Artikel <span class="sort-icon"></span></th> <th class="sortable" data-sort="article_count" onclick="sortGlobalSources('article_count')">Artikel <span class="sort-icon"></span></th>
<th class="sortable" data-sort="status" onclick="sortGlobalSources('status')">Status <span class="sort-icon"></span></th> <th class="sortable" data-sort="status" onclick="sortGlobalSources('status')">Status <span class="sort-icon"></span></th>
<th>Aktionen</th> <th>Aktionen</th>
@@ -417,6 +426,7 @@
<select id="sourceType"> <select id="sourceType">
<option value="rss_feed">RSS-Feed</option> <option value="rss_feed">RSS-Feed</option>
<option value="web_source">Webquelle</option> <option value="web_source">Webquelle</option>
<option value="telegram_channel">Telegram-Kanal</option>
<option value="excluded">Ausgeschlossen</option> <option value="excluded">Ausgeschlossen</option>
</select> </select>
</div> </div>
@@ -433,7 +443,16 @@
<option value="regional">Regional</option> <option value="regional">Regional</option>
<option value="boulevard">Boulevard</option> <option value="boulevard">Boulevard</option>
<option value="sonstige" selected>Sonstige</option> <option value="sonstige" selected>Sonstige</option>
<option value="cybercrime">Cybercrime / Hacktivismus</option>
<option value="cybercrime-leaks">Cybercrime / Leaks</option>
<option value="ukraine-russland-krieg">Ukraine-Russland-Krieg</option>
<option value="irankonflikt">Irankonflikt</option>
<option value="osint-international">OSINT International</option>
<option value="extremismus-deutschland">Extremismus Deutschland</option>
</select> </select>
<option value="russische-staatspropaganda">Russische Staatspropaganda</option>
<option value="russische-opposition">Russische Opposition / Exilmedien</option>
<option value="syrien-nahost">Syrien / Nahost</option>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

Datei anzeigen

@@ -3,15 +3,15 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>IntelSight Verwaltung - Login</title> <title>AegisSight Monitor-Verwaltung - Login</title>
<link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/css/style.css">
</head> </head>
<body class="login-page"> <body class="login-page">
<div class="login-container"> <div class="login-container">
<div class="login-card"> <div class="login-card">
<div class="login-header"> <div class="login-header">
<h1>IntelSight</h1> <h1>AegisSight</h1>
<p class="subtitle">Verwaltungsportal</p> <p class="subtitle">Monitor-Verwaltung</p>
</div> </div>
<form id="loginForm" class="login-form"> <form id="loginForm" class="login-form">

Datei anzeigen

@@ -4,7 +4,7 @@
let globalSourcesCache = []; let globalSourcesCache = [];
let tenantSourcesCache = []; let tenantSourcesCache = [];
let editingSourceId = null; let editingSourceId = null;
let globalSortField = "name"; let globalSortField = "category";
let globalSortAsc = true; let globalSortAsc = true;
const CATEGORY_LABELS = { const CATEGORY_LABELS = {
@@ -18,11 +18,21 @@ const CATEGORY_LABELS = {
regional: "Regional", regional: "Regional",
boulevard: "Boulevard", boulevard: "Boulevard",
sonstige: "Sonstige", sonstige: "Sonstige",
"cybercrime": "Cybercrime / Hacktivismus",
"cybercrime-leaks": "Cybercrime / Leaks",
"ukraine-russland-krieg": "Ukraine-Russland-Krieg",
"irankonflikt": "Irankonflikt",
"osint-international": "OSINT International",
"extremismus-deutschland": "Extremismus Deutschland",
"russische-staatspropaganda": "Russische Staatspropaganda",
"russische-opposition": "Russische Opposition / Exilmedien",
"syrien-nahost": "Syrien / Nahost",
}; };
const TYPE_LABELS = { const TYPE_LABELS = {
rss_feed: "RSS-Feed", rss_feed: "RSS-Feed",
web_source: "Webquelle", web_source: "Webquelle",
telegram_channel: "Telegram-Kanal",
excluded: "Ausgeschlossen", excluded: "Ausgeschlossen",
}; };
@@ -65,25 +75,49 @@ async function loadGlobalSources() {
function renderGlobalSources(sources) { function renderGlobalSources(sources) {
const tbody = document.getElementById("globalSourceTable"); const tbody = document.getElementById("globalSourceTable");
const cols = 7;
if (sources.length === 0) { if (sources.length === 0) {
tbody.innerHTML = '<tr><td colspan="8" class="text-muted">Keine Grundquellen</td></tr>'; tbody.innerHTML = `<tr><td colspan="${cols}" class="text-muted">Keine Grundquellen</td></tr>`;
return; return;
} }
tbody.innerHTML = sources.map((s) => `
<tr> // Nach Kategorie gruppieren (Reihenfolge beibehalten)
<td>${esc(s.name)}</td> const grouped = {};
<td class="text-secondary" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.url || "-")}</td> const order = [];
<td>${esc(s.domain || "-")}</td> sources.forEach((s) => {
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td> const cat = s.category || "sonstige";
<td>${CATEGORY_LABELS[s.category] || s.category}</td> if (!grouped[cat]) { grouped[cat] = []; order.push(cat); }
<td class="text-right">${s.article_count || 0}</td> grouped[cat].push(s);
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td> });
<td>
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button> let html = "";
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Löschen</button> order.forEach((cat) => {
</td> const label = CATEGORY_LABELS[cat] || cat;
</tr> const count = grouped[cat].length;
`).join(""); html += `<tr class="cat-header-row"><td colspan="${cols}"><span class="cat-header-label">${esc(label)}</span><span class="cat-header-count">${count}</span></td></tr>`;
grouped[cat].forEach((s) => {
const hasNotes = s.notes && s.notes.trim();
const infoBtn = hasNotes
? `<span class="src-info-toggle" onclick="toggleSourceInfo(${s.id})" title="Info einblenden">&#9432;</span>`
: '';
const notesRow = hasNotes
? `<tr class="src-notes-row" id="notes-${s.id}" style="display:none;"><td colspan="${cols}" class="src-notes-cell">${esc(s.notes)}</td></tr>`
: '';
html += `<tr>
<td>${infoBtn} ${esc(s.name)}</td>
<td class="text-secondary" style="max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="${esc(s.url || '')}">${esc(s.url || "-")}</td>
<td>${esc(s.domain || "-")}</td>
<td>${TYPE_LABELS[s.source_type] || s.source_type}</td>
<td class="text-right">${s.article_count || 0}</td>
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td>
<td>
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button>
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Löschen</button>
</td>
</tr>${notesRow}`;
});
});
tbody.innerHTML = html;
document.getElementById("globalSourceCount").textContent = `${sources.length} Grundquellen`; document.getElementById("globalSourceCount").textContent = `${sources.length} Grundquellen`;
@@ -384,3 +418,15 @@ async function addDiscoveredFeeds() {
btn.textContent = "Ausgewählte hinzufügen"; btn.textContent = "Ausgewählte hinzufügen";
} }
} }
function toggleSourceInfo(id) {
const row = document.getElementById("notes-" + id);
if (!row) return;
const isVisible = row.style.display !== "none";
row.style.display = isVisible ? "none" : "table-row";
const mainRow = row.previousElementSibling;
if (mainRow) {
const btn = mainRow.querySelector(".src-info-toggle");
if (btn) btn.classList.toggle("active", !isVisible);
}
}