Neue Kategorie russische-opposition in Verwaltungs-UI + 10 neue Telegram-Kanaele (Wave 3)
Dieser Commit ist enthalten in:
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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: ")";
|
||||||
|
}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">ⓘ</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren