Promote develop → main (2026-07-25 22:22 UTC) #16

Zusammengeführt
IntelSight_Admin hat 41 Commits von develop nach main 2026-07-26 00:22:10 +02:00 zusammengeführt
7 geänderte Dateien mit 654 neuen und 80 gelöschten Zeilen
Nur Änderungen aus Commit 8fd087ea0c werden angezeigt - Alle Commits anzeigen

Datei anzeigen

@@ -63,7 +63,8 @@ src/:
favicon.svg: "AegisSight Logo"
css/: "Stylesheets (Dark Theme)"
js/:
app.js: "Hauptlogik, Login, Tab-Switching, Dashboard-Render"
app.js: "Hauptlogik, Login, Tab-Switching, Dashboard-Render, ThemeManager (Dark/Light, localStorage portal_theme)"
a11y.js: "Barrierefreiheits-Panel (Kontrast, Focus, Schrift, Animationen). IDENTISCHE Datei wie im Monitor-Repo, bei Aenderungen dort mitziehen"
sources.js: "Grundquellen + Kundenquellen Management"
source-health.js: "Quellen-Health & KI-Vorschläge"
audit.js: "Audit-Log Tab"

Datei anzeigen

@@ -1,32 +1,63 @@
/* AegisSight Dark Theme - Verwaltungsportal */
/* AegisSight Design System - Verwaltungsportal (Navy/Gold, Dark = Standard, Light per Umschalter) */
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #e2e8f0;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #f0b429;
--accent-hover: #d4a017;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--info: #3b82f6;
--border: #334155;
--radius: 8px;
--radius-lg: 12px;
--bg-primary: #0B1121;
--bg-secondary: #151D2E;
--bg-tertiary: #1A2440;
--bg-hover: #1A2440;
--text-primary: #E8ECF4;
--text-secondary: #8896AB;
--text-muted: #95A3B8;
--accent: #C8A851;
--accent-hover: #B5923E;
--success: #10B981;
--warning: #F59E0B;
--danger: #EF4444;
--info: #7C8DB5;
--border: #1E2D45;
--radius: 6px;
--radius-lg: 8px;
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.5);
--font-title: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
--font-body: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
/* Das Navy-Logo liegt im Dark-Theme auf einer hellen Plakette */
--logo-plaque: rgba(232, 236, 240, 0.9);
}
/* Light-Theme. Akzent wie im Monitor auf das dunklere Gold abgesenkt,
#C8A851 schafft auf hellem Grund den WCAG-Kontrast nicht. */
[data-theme="light"] {
--bg-primary: #F4F5F7;
--bg-secondary: #FFFFFF;
--bg-tertiary: #E8EBF0;
--bg-hover: #E8EBF0;
--text-primary: #1A202C;
--text-secondary: #4A5568;
--text-muted: #A0AEC0;
--accent: #96791A;
--accent-hover: #7D6516;
--success: #059669;
--warning: #D97706;
--danger: #DC2626;
--info: #4A5568;
--border: #E2E8F0;
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.12);
--logo-plaque: transparent;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
font-family: var(--font-body);
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
min-height: 100vh;
}
h1, h2, h3, .logo, .stat-value, .token-stat-value {
font-family: var(--font-title);
}
/* --- Login Page --- */
.login-page {
display: flex;
@@ -143,7 +174,7 @@ input:focus, select:focus, textarea:focus {
color: var(--text-primary);
}
.btn-secondary:hover:not(:disabled) {
background: #475569;
background: var(--border);
}
.btn-danger {
@@ -189,6 +220,9 @@ input:focus, select:focus, textarea:focus {
}
.app-header .logo {
display: flex;
align-items: center;
gap: 10px;
color: var(--accent);
font-size: 18px;
font-weight: 700;
@@ -198,9 +232,19 @@ input:focus, select:focus, textarea:focus {
color: var(--text-secondary);
font-weight: 400;
font-size: 13px;
margin-left: 8px;
margin-left: 2px;
}
/* Navy-Logo bleibt auch im Dark-Theme, liegt dort auf heller Plakette */
.logo-plaque {
display: flex;
align-items: center;
background: var(--logo-plaque);
border-radius: 8px;
padding: 3px 6px;
}
.logo-plaque img { height: 24px; display: block; }
.header-right {
display: flex;
align-items: center;
@@ -237,7 +281,11 @@ input:focus, select:focus, textarea:focus {
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 0.2s;
display: inline-flex;
align-items: center;
gap: 7px;
}
.nav-tab svg { flex-shrink: 0; }
.nav-tab:hover {
color: var(--text-primary);
@@ -1121,3 +1169,224 @@ input[type="date"].filter-select { padding: 6px 10px; }
border-color: var(--accent);
}
/* === Theme-Umschalter (goldener Schiebeschalter, wie im Monitor) === */
.theme-switch {
display: flex;
align-items: center;
gap: 6px;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
}
.theme-switch-icon {
font-size: 14px;
line-height: 1;
opacity: 0.4;
transition: opacity 0.3s;
}
.theme-switch.dark .theme-switch-moon,
.theme-switch.light .theme-switch-sun {
opacity: 1;
}
.theme-switch-track {
position: relative;
width: 40px;
height: 22px;
border-radius: 11px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
transition: background 0.3s, border-color 0.3s;
flex-shrink: 0;
}
.theme-switch-knob {
position: absolute;
top: 2px;
left: 2px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent);
box-shadow: 0 0 8px rgba(200, 168, 81, 0.3);
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s;
}
.theme-switch.dark .theme-switch-knob { transform: translateX(18px); }
.theme-switch.light .theme-switch-knob { transform: translateX(0); }
.theme-switch:hover .theme-switch-track { border-color: var(--accent); }
.theme-switch:hover .theme-switch-knob { box-shadow: 0 0 12px rgba(200, 168, 81, 0.5); }
/* === Kleine Schalter-Optik (fuer das Barrierefreiheits-Panel) === */
.toggle-switch {
position: relative;
display: inline-block;
width: 34px;
height: 18px;
border-radius: 9px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
transition: background 0.2s ease;
}
.toggle-switch::after {
content: "";
position: absolute;
top: 2px;
left: 2px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--text-secondary);
transition: transform 0.2s ease, background 0.2s ease;
}
/* === Barrierefreiheits-Panel (a11y.js haengt sich in .header-right ein) === */
.a11y-center { position: relative; }
.a11y-btn {
background: none;
border: none;
color: var(--text-secondary);
cursor: pointer;
padding: 6px 8px;
border-radius: var(--radius);
display: flex;
align-items: center;
justify-content: center;
transition: color 0.2s ease, background 0.2s ease;
width: 36px;
height: 36px;
}
.a11y-btn:hover { color: var(--accent); background: var(--bg-hover); }
.a11y-panel {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 240px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-lg);
z-index: 200;
padding: 16px;
}
.a11y-panel-title {
font-size: 12px;
font-weight: 600;
color: var(--text-secondary);
letter-spacing: 0.5px;
margin-bottom: 12px;
}
.a11y-option {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 0;
cursor: pointer;
font-size: 13px;
color: var(--text-primary);
user-select: none;
}
.a11y-option input[type="checkbox"] {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.a11y-option .toggle-switch { flex-shrink: 0; }
.a11y-option input:focus-visible + .toggle-switch {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.a11y-option input:checked + .toggle-switch { background: var(--accent); }
.a11y-option input:checked + .toggle-switch::after {
transform: translateX(16px);
background: var(--bg-primary);
}
/* === Barrierefreiheits-Modi === */
[data-a11y-contrast="true"] {
--text-muted: #B0BDD0;
--border: #3A4A66;
}
[data-a11y-contrast="true"] .btn-primary { color: #1A1A1A; }
[data-a11y-contrast="true"][data-theme="light"] {
--accent: #6B5714;
--accent-hover: #5A4A11;
--text-muted: #718096;
--border: #94A3B8;
}
[data-a11y-focus="true"] a:focus-visible,
[data-a11y-focus="true"] button:focus-visible,
[data-a11y-focus="true"] input:focus-visible,
[data-a11y-focus="true"] select:focus-visible,
[data-a11y-focus="true"] textarea:focus-visible,
[data-a11y-focus="true"] [tabindex]:focus-visible,
[data-a11y-focus="true"] [role="button"]:focus-visible {
outline: 3px solid var(--accent) !important;
outline-offset: 2px !important;
box-shadow: 0 0 0 4px rgba(200, 168, 81, 0.3) !important;
}
[data-a11y-fontsize="true"] body { zoom: 1.15; }
[data-a11y-motion="true"] *,
[data-a11y-motion="true"] *::before,
[data-a11y-motion="true"] *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
/* === Icon-Ausrichtung in Knoepfen === */
.btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px; }
.btn svg { flex-shrink: 0; }
/* === Aufklappbarer Bereich (z.B. "Erweitert" im Verbrauchs-Formular) === */
details.adv-section { grid-column: 1 / -1; border-top: 1px solid var(--border); padding-top: 10px; }
details.adv-section summary {
cursor: pointer;
color: var(--text-secondary);
font-size: 13px;
user-select: none;
display: flex;
align-items: center;
gap: 6px;
}
details.adv-section summary:hover { color: var(--text-primary); }
details.adv-section[open] summary { margin-bottom: 10px; }
details.adv-section .adv-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
details.adv-section .adv-hint { grid-column: 1 / -1; font-size: 12px; color: var(--text-muted); margin: 0; }
/* === Light-Theme: dunklere Text-Farben fuer Badges/Status ===
(Die Dark-Pastelltoene sind auf hellem Grund unlesbar. Automatisch
abgeleitete Gegenstuecke, je Original-Farbton eine dunkle Variante.) */
[data-theme="light"] .error-msg { color: #B91C1C; }
[data-theme="light"] .btn-danger { color: #B91C1C; }
[data-theme="light"] .btn-success { color: #047857; }
[data-theme="light"] .badge-active { color: #047857; }
[data-theme="light"] .badge-inactive { color: #B91C1C; }
[data-theme="light"] .badge-trial { color: #1D4ED8; }
[data-theme="light"] .badge-annual { color: #7E22CE; }
[data-theme="light"] .badge-permanent { color: #B45309; }
[data-theme="light"] .badge-expired { color: #B91C1C; }
[data-theme="light"] .badge-revoked { color: #475569; }
[data-theme="light"] .badge-none { color: #475569; }
[data-theme="light"] .activity-icon.org { color: #1D4ED8; }
[data-theme="light"] .activity-icon.user { color: #047857; }
[data-theme="light"] /* --- Health & Suggestion Badges --- */
.badge-health-error { color: #B91C1C; }
[data-theme="light"] .badge-health-warning { color: #B45309; }
[data-theme="light"] .badge-health-ok { color: #047857; }
[data-theme="light"] .badge-suggestion-add_source { color: #1D4ED8; }
[data-theme="light"] .badge-suggestion-deactivate_source { color: #B45309; }
[data-theme="light"] .badge-suggestion-remove_source { color: #B91C1C; }
[data-theme="light"] .badge-suggestion-fix_url { color: #7E22CE; }
[data-theme="light"] .badge-priority-high { color: #B91C1C; }
[data-theme="light"] .badge-priority-medium { color: #B45309; }
[data-theme="light"] .badge-priority-low { color: #475569; }
[data-theme="light"] .sources-stat-item { color: #475569; }
[data-theme="light"] .health-badge-unknown { color: #475569; }
[data-theme="light"] .audit-entry-meta { color: #475569; }
[data-theme="light"] .audit-entry-detail summary { color: #475569; }
[data-theme="light"] /* === Verwendungs-Sicht (Phase 6) === */
.activity-cell { color: #475569; }

Datei anzeigen

@@ -6,32 +6,63 @@
<title>AegisSight Monitor-Verwaltung</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="apple-touch-icon" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/css/style.css?v=20260509d">
<link rel="stylesheet" href="/static/css/style.css?v=20260725a">
<script>(function(){var t=localStorage.getItem('portal_theme');if(t==='light')document.documentElement.setAttribute('data-theme','light');try{var a=JSON.parse(localStorage.getItem('osint_a11y')||'{}');Object.keys(a).forEach(function(k){if(a[k])document.documentElement.setAttribute('data-a11y-'+k,'true');});}catch(e){}})()</script>
<style>
.source-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:600; }
.badge-monitor { background:#e3f2fd; color:#1565c0; }
.badge-globe { background:#e8f5e9; color:#2e7d32; }
.badge-monitor { background:rgba(59,130,246,0.15); color:#93c5fd; }
.badge-globe { background:rgba(16,185,129,0.15); color:#86efac; }
[data-theme="light"] .badge-monitor { background:#e3f2fd; color:#1565c0; }
[data-theme="light"] .badge-globe { background:#e8f5e9; color:#2e7d32; }
</style>
</head>
<body>
<!-- Header -->
<header class="app-header">
<div class="logo">AegisSight <span>Monitor-Verwaltung</span></div>
<div class="logo">
<span class="logo-plaque"><img src="/static/favicon.svg" alt="AegisSight Logo"></span>
AegisSight <span>Monitor-Verwaltung</span>
</div>
<div class="header-right">
<div class="theme-switch dark" id="theme-toggle" onclick="ThemeManager.toggle()" role="switch" aria-checked="true" aria-label="Dark Mode" title="Theme wechseln">
<span class="theme-switch-icon theme-switch-sun">&#9728;&#65038;</span>
<div class="theme-switch-track">
<div class="theme-switch-knob"></div>
</div>
<span class="theme-switch-icon theme-switch-moon">&#9789;</span>
</div>
<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">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" x2="9" y1="12" y2="12"/></svg>
Abmelden
</button>
</div>
</header>
<main class="app-content">
<!-- Navigation -->
<!-- Navigation (Lucide-Icons, Strichstaerke 2 bei 15px) -->
<nav class="nav-tabs">
<button class="nav-tab active" data-section="dashboard">Dashboard</button>
<button class="nav-tab" data-section="orgs">Organisationen</button>
<button class="nav-tab" data-section="licenses">Lizenzen</button>
<button class="nav-tab" data-section="sources">Quellen</button>
<button class="nav-tab" data-section="audit">Audit-Log</button>
<button class="nav-tab active" data-section="dashboard">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="7" height="9" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="16" width="7" height="5" rx="1"/></svg>
Dashboard
</button>
<button class="nav-tab" data-section="orgs">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 22V4a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v18Z"/><path d="M6 12H4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2"/><path d="M18 9h2a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2h-2"/><path d="M10 6h4"/><path d="M10 10h4"/><path d="M10 14h4"/><path d="M10 18h4"/></svg>
Organisationen
</button>
<button class="nav-tab" data-section="licenses">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 18v3c0 .6.4 1 1 1h4v-3h3v-3h2l1.4-1.4a6.5 6.5 0 1 0-4-4Z"/><circle cx="16.5" cy="7.5" r=".5"/></svg>
Lizenzen
</button>
<button class="nav-tab" data-section="sources">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 11a9 9 0 0 1 9 9"/><path d="M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg>
Quellen
</button>
<button class="nav-tab" data-section="audit">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z"/><path d="M14 2v4a2 2 0 0 0 2 2h4"/><path d="M10 9H8"/><path d="M16 13H8"/><path d="M16 17H8"/></svg>
Audit-Log
</button>
</nav>
<!-- Dashboard Section -->
@@ -93,7 +124,7 @@
<div id="orgListView">
<div class="action-bar">
<input type="text" class="search-input" id="orgSearch" placeholder="Organisation suchen...">
<button class="btn btn-primary" id="newOrgBtn">+ Neue Organisation</button>
<button class="btn btn-primary" id="newOrgBtn"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Neue Organisation</button>
</div>
<div class="card">
<div class="table-wrap">
@@ -123,14 +154,14 @@
<button class="nav-tab active" data-subtab="users">Nutzer</button>
<button class="nav-tab" data-subtab="org-licenses">Lizenzen</button>
<button class="nav-tab" data-subtab="settings">Einstellungen</button>
<button class="nav-tab" data-subtab="tokens">Token-Nutzung</button>
<button class="nav-tab" data-subtab="tokens">Verbrauch</button>
</div>
<!-- Users Sub-Tab -->
<div class="section active" id="sub-users">
<div class="action-bar">
<span class="text-secondary" id="userLimitInfo"></span>
<button class="btn btn-primary" id="newUserBtn">+ Nutzer anlegen</button>
<button class="btn btn-primary" id="newUserBtn"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Nutzer anlegen</button>
</div>
<div class="card">
<div class="table-wrap">
@@ -155,7 +186,7 @@
<div class="section" id="sub-org-licenses">
<div class="action-bar">
<span></span>
<button class="btn btn-primary" id="newLicenseBtn">+ Neue Lizenz</button>
<button class="btn btn-primary" id="newLicenseBtn"><svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>Neue Lizenz</button>
</div>
<div class="card">
<div class="table-wrap">
@@ -211,34 +242,46 @@
</div>
<div class="section" id="sub-tokens">
<div class="token-overview">
<!-- Kundensicht: das, was der Kunde im Monitor auch sieht -->
<h3 style="margin-bottom:8px;">Kundensicht (Credits)</h3>
<div class="token-stats-row" id="tokenStatsRow">
<div class="token-stat-card">
<div class="token-stat-label">Verbrauchte Credits</div>
<div class="token-stat-label">Verbrauchte Credits (Periode)</div>
<div class="token-stat-value" id="tokenCreditsUsed">-</div>
</div>
<div class="token-stat-card">
<div class="token-stat-label">Verbleibend</div>
<div class="token-stat-value" id="tokenCreditsRemaining">-</div>
</div>
<div class="token-stat-card">
<div class="token-stat-label">Budget (USD)</div>
<div class="token-stat-value" id="tokenBudgetUsd">-</div>
</div>
<div class="token-stat-card">
<div class="token-stat-label">Kosten (USD)</div>
<div class="token-stat-value" id="tokenCostUsd">-</div>
</div>
</div>
<div id="tokenSourceSplit" style="margin-top:12px;font-size:14px;"></div>
<div class="token-budget-bar-section">
<div class="token-budget-label">
<span>Budget-Auslastung</span>
<span>Kontingent-Auslastung (laufende Periode)</span>
<span id="tokenBudgetPercent">0%</span>
</div>
<div class="token-budget-bar-container">
<div class="token-budget-bar" id="tokenBudgetBar"></div>
</div>
</div>
<p style="margin-top:10px; font-size:12px; color:var(--text-muted); display:flex; align-items:center; gap:6px;">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" style="flex-shrink:0;"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
<span>Zur Einordnung. Ein Live-Lauf kostet 45 Credits, das Anlegen einer Recherche 120 (drei Durchläufe), jede Vertiefung 40, Chat und Beschreibungs-Assistent je 1 Credit.
<a href="#" onclick="openRechnerTab(); return false;" style="color:var(--accent);">Verbrauchsrechner öffnen</a></span>
</p>
<!-- Interne Sicht: was uns der Betrieb im Einkauf kostet -->
<h3 style="margin-top:24px; margin-bottom:8px;">Interne Kosten (Einkauf, USD)</h3>
<div class="token-stats-row">
<div class="token-stat-card">
<div class="token-stat-label">API-Kosten aktueller Monat</div>
<div class="token-stat-value" id="tokenCostUsd">-</div>
</div>
<div class="token-stat-card">
<div class="token-stat-label">Budget-Limit (USD, Altmodus)</div>
<div class="token-stat-value" id="tokenBudgetUsd">-</div>
</div>
</div>
<div id="tokenSourceSplit" style="margin-top:12px;font-size:14px;"></div>
<h3 style="margin-top:24px;">Monatlicher Verlauf</h3>
<table class="data-table">
<thead>
@@ -255,11 +298,11 @@
<tbody id="tokenMonthlyTable"></tbody>
</table>
<h3 style="margin-top:24px;">Budget bearbeiten</h3>
<h3 style="margin-top:24px;">Credits-Kontingent bearbeiten</h3>
<div class="card" style="margin-top:12px;">
<div class="card-body">
<form id="tokenBudgetForm" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
<div style="grid-column:1/-1; padding:8px 12px; background:var(--bg-tertiary,#0f172a); border-radius:6px; border:1px solid var(--border);">
<div style="grid-column:1/-1; padding:8px 12px; background:var(--bg-tertiary); border-radius:6px; border:1px solid var(--border);">
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; margin:0;">
<input type="checkbox" id="editUnlimitedBudget" onchange="onUnlimitedToggle()">
<span><strong>Unbegrenztes Budget</strong> &mdash; Verbrauch wird trotzdem getrackt, aber kein Limit/Hard-Stop</span>
@@ -286,20 +329,37 @@
<label for="editWarnPercent">Warnschwelle (%)</label>
<input type="number" id="editWarnPercent" min="1" max="100" placeholder="80">
</div>
<details class="adv-section">
<summary>
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="9 18 15 12 9 6"/></svg>
Erweitert (Altmodus und Korrektur)
</summary>
<p class="adv-hint">
Die beiden USD-Felder gehören zum alten Abrechnungsmodus (BILLING_MODE=actual)
und haben im heutigen Pauschalmodus keine Wirkung auf den Kunden. Das
Korrektur-Feld überschreibt den Verbrauch der laufenden Periode direkt,
nur für bewusste Eingriffe gedacht.
</p>
<div class="adv-grid" style="margin-top:10px;">
<div class="form-group">
<label for="editCostPerCredit">Kosten pro Credit (USD)</label>
<label for="editCostPerCredit">Kosten pro Credit (USD, nur Altmodus)</label>
<input type="number" id="editCostPerCredit" step="0.01" placeholder="0.20">
</div>
<div class="form-group">
<label for="editBudgetUsd">Budget-Limit (USD)</label>
<label for="editBudgetUsd">Budget-Limit (USD, nur Altmodus)</label>
<input type="number" id="editBudgetUsd" step="0.01" placeholder="z.B. 2000">
</div>
<div class="form-group">
<label for="editCreditsUsed">Verbrauchte Credits (laufende Periode)</label>
<label for="editCreditsUsed">Verbrauch korrigieren (Credits, laufende Periode)</label>
<input type="number" id="editCreditsUsed" step="0.01" placeholder="Aktueller Verbrauch">
</div>
</div>
</details>
<div style="grid-column:1/-1; display:flex; gap:8px; align-items:center;">
<button type="submit" class="btn btn-primary">Speichern</button>
<button type="submit" class="btn btn-primary">
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
Speichern
</button>
<span id="tokenBudgetMsg" style="font-size:13px; color:var(--text-secondary);"></span>
</div>
</form>
@@ -786,6 +846,30 @@
</div>
<!-- Modal: Lizenz verlängern (ersetzt das alte Browser-prompt) -->
<div class="modal-overlay" id="modalExtendLicense">
<div class="modal">
<div class="modal-header">
<h3>Lizenz verlängern</h3>
<button class="modal-close" onclick="closeModal('modalExtendLicense')">&times;</button>
</div>
<form id="extendLicenseForm">
<div class="modal-body">
<div class="form-group">
<label for="extendDays">Um wie viele Tage verlängern?</label>
<input type="number" id="extendDays" value="365" min="1" max="3650" required>
<div class="text-muted mt-8" style="font-size:12px;">Die Verlängerung setzt am aktuellen Ablaufdatum an und aktiviert die Lizenz, falls sie abgelaufen war.</div>
</div>
<div id="extendLicenseError" class="error-msg" style="display:none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" onclick="closeModal('modalExtendLicense')">Abbrechen</button>
<button type="submit" class="btn btn-primary">Verlängern</button>
</div>
</form>
</div>
</div>
<!-- Modal: Source (Create/Edit) -->
<div class="modal-overlay" id="modalSource">
<div class="modal">
@@ -1154,7 +1238,8 @@
</div>
</div>
<script src="/static/js/app.js?v=20260725a"></script>
<script src="/static/js/a11y.js?v=20260725a"></script>
<script src="/static/js/app.js?v=20260725b"></script>
<script src="/static/js/sources.js?v=20260522x2"></script>
<script src="/static/js/x-scraper.js?v=20260522a"></script>
<script src="/static/js/source-health.js?v=20260509l"></script>

Datei anzeigen

@@ -7,12 +7,14 @@
<title>AegisSight Monitor-Verwaltung - Anmeldung</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="apple-touch-icon" href="/static/favicon.svg">
<link rel="stylesheet" href="/static/css/style.css?v=20260509d">
<link rel="stylesheet" href="/static/css/style.css?v=20260725a">
<script>(function(){if(localStorage.getItem('portal_theme')==='light')document.documentElement.setAttribute('data-theme','light');})()</script>
</head>
<body class="login-page">
<div class="login-container">
<div class="login-card">
<div class="login-header">
<span class="logo-plaque" style="display:inline-flex; margin-bottom:12px;"><img src="/static/favicon.svg" alt="AegisSight Logo" style="height:44px;"></span>
<h1>AegisSight</h1>
<p class="subtitle">Monitor-Verwaltung</p>
</div>
@@ -26,7 +28,7 @@
</div>
<div id="loginError" class="error-msg" style="display:none"></div>
<button type="submit" class="btn btn-primary btn-full" id="magicBtn">Login-Link anfordern</button>
<p class="form-hint" style="margin-top:14px;text-align:center;font-size:12px;color:#94a3b8;">
<p class="form-hint" style="margin-top:14px;text-align:center;font-size:12px;color:var(--text-secondary);">
Wir senden dir einen einmaligen Login-Link per E-Mail.
</p>
</form>
@@ -35,7 +37,7 @@
<div id="sentInfo" style="display:none;text-align:center;">
<div style="font-size:42px;margin:8px 0 16px 0;">&#9993;</div>
<h2 style="font-size:17px;margin:0 0 8px 0;">E-Mail unterwegs</h2>
<p style="margin:0 0 18px 0;font-size:14px;color:#94a3b8;line-height:1.5;">
<p style="margin:0 0 18px 0;font-size:14px;color:var(--text-secondary);line-height:1.5;">
Wenn die Adresse berechtigt ist, hast du gleich einen Login-Link in deinem Posteingang.
Der Link ist 10 Minuten gültig.
</p>
@@ -46,7 +48,7 @@
<div id="verifying" style="display:none;text-align:center;">
<div class="spinner" style="margin:8px auto 16px;"></div>
<h2 style="font-size:17px;margin:0 0 8px 0;">Anmeldung wird geprüft...</h2>
<p style="margin:0;font-size:14px;color:#94a3b8;">Einen Moment bitte.</p>
<p style="margin:0;font-size:14px;color:var(--text-secondary);">Einen Moment bitte.</p>
</div>
</div>
</div>
@@ -54,13 +56,13 @@
<style>
.spinner {
width: 36px; height: 36px;
border: 3px solid rgba(240,180,41,0.2);
border-top-color: #f0b429;
border: 3px solid rgba(200,168,81,0.2);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
.form-hint { font-size: 12px; color: #94a3b8; margin-top: 8px; }
.form-hint { font-size: 12px; color: var(--text-secondary); margin-top: 8px; }
</style>
<script>

162
src/static/js/a11y.js Normale Datei
Datei anzeigen

@@ -0,0 +1,162 @@
/**
* Barrierefreiheits-Manager: Panel mit 4 Schaltern (Kontrast, Focus, Schrift, Animationen).
*
* Liegt seit 2026-07-21 in einer eigenen Datei, weil ihn beide Oberflaechen brauchen,
* das klassische Dashboard (.header-right) und die Studio-Ansicht (.studio-top-right).
* Vorher steckte er in app.js und fehlte im Studio deshalb komplett.
*/
const A11yManager = {
_key: 'osint_a11y',
_isOpen: false,
_settings: { contrast: false, focus: false, fontsize: false, motion: false },
init() {
// Einstellungen aus localStorage laden
try {
const saved = JSON.parse(localStorage.getItem(this._key) || '{}');
Object.keys(this._settings).forEach(k => {
if (typeof saved[k] === 'boolean') this._settings[k] = saved[k];
});
} catch (e) { /* Ungültige Daten ignorieren */ }
// Button + Panel dynamisch in die Kopfzeile einfügen (vor Theme-Toggle).
// Beide Oberflaechen benennen ihren rechten Kopfzeilen-Block anders.
const headerRight = document.querySelector('.header-right, .studio-top-right');
const themeToggle = document.getElementById('theme-toggle');
if (!headerRight) return;
if (document.getElementById('a11y-btn')) return; // schon vorhanden
const container = document.createElement('div');
container.className = 'a11y-center';
container.innerHTML = `
<button class="a11y-btn" id="a11y-btn" title="Barrierefreiheit"
aria-label="Barrierefreiheit" aria-expanded="false" aria-haspopup="true">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<circle cx="12" cy="4" r="2"/>
<path d="M12 8c-3.3 0-6 .5-6 .5v2s2.7-.5 5-.5v3l-3 7h2.5l2.5-5.5 2.5 5.5h2.5l-3-7v-3c2.3 0 5 .5 5 .5v-2S15.3 8 12 8z"/>
</svg>
</button>
<div class="a11y-panel" id="a11y-panel" role="group" aria-label="Barrierefreiheits-Einstellungen" style="display:none;">
<div class="a11y-panel-title">Barrierefreiheit</div>
<label class="a11y-option">
<input type="checkbox" id="a11y-contrast">
<span class="toggle-switch"></span>
<span>Hoher Kontrast</span>
</label>
<label class="a11y-option">
<input type="checkbox" id="a11y-focus">
<span class="toggle-switch"></span>
<span>Verstärkte Focus-Anzeige</span>
</label>
<label class="a11y-option">
<input type="checkbox" id="a11y-fontsize">
<span class="toggle-switch"></span>
<span>Größere Schrift</span>
</label>
<label class="a11y-option">
<input type="checkbox" id="a11y-motion">
<span class="toggle-switch"></span>
<span>Animationen aus</span>
</label>
</div>
`;
if (themeToggle && themeToggle.parentNode === headerRight) {
headerRight.insertBefore(container, themeToggle);
} else {
headerRight.prepend(container);
}
// Toggle-Event-Listener
['contrast', 'focus', 'fontsize', 'motion'].forEach(key => {
document.getElementById('a11y-' + key).addEventListener('change', () => this.toggle(key));
});
// Button öffnet/schließt Panel
document.getElementById('a11y-btn').addEventListener('click', (e) => {
e.stopPropagation();
this._isOpen ? this._closePanel() : this._openPanel();
});
// Klick außerhalb schließt Panel
document.addEventListener('click', (e) => {
if (this._isOpen && !container.contains(e.target)) {
this._closePanel();
}
});
// Keyboard: Esc schließt, Pfeiltasten navigieren
container.addEventListener('keydown', (e) => {
if (e.key === 'Escape' && this._isOpen) {
e.stopPropagation();
this._closePanel();
return;
}
if (!this._isOpen) return;
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault();
const options = Array.from(document.querySelectorAll('.a11y-option input[type="checkbox"]'));
const idx = options.indexOf(document.activeElement);
let next;
if (e.key === 'ArrowDown') {
next = idx < options.length - 1 ? idx + 1 : 0;
} else {
next = idx > 0 ? idx - 1 : options.length - 1;
}
options[next].focus();
}
});
// Einstellungen anwenden + Checkboxen synchronisieren
this._apply();
this._syncUI();
},
toggle(key) {
this._settings[key] = !this._settings[key];
this._apply();
this._syncUI();
this._save();
},
_apply() {
const root = document.documentElement;
Object.keys(this._settings).forEach(k => {
if (this._settings[k]) {
root.setAttribute('data-a11y-' + k, 'true');
} else {
root.removeAttribute('data-a11y-' + k);
}
});
},
_syncUI() {
Object.keys(this._settings).forEach(k => {
const cb = document.getElementById('a11y-' + k);
if (cb) cb.checked = this._settings[k];
});
},
_save() {
localStorage.setItem(this._key, JSON.stringify(this._settings));
},
_openPanel() {
this._isOpen = true;
document.getElementById('a11y-panel').style.display = '';
document.getElementById('a11y-btn').setAttribute('aria-expanded', 'true');
// Fokus auf erste Option setzen
requestAnimationFrame(() => {
const first = document.querySelector('.a11y-option input[type="checkbox"]');
if (first) first.focus();
});
},
_closePanel() {
this._isOpen = false;
document.getElementById('a11y-panel').style.display = 'none';
const btn = document.getElementById('a11y-btn');
btn.setAttribute('aria-expanded', 'false');
btn.focus();
}
};

Datei anzeigen

@@ -45,6 +45,36 @@ const API = {
},
};
// --- Theme (Dark = Standard, Wahl bleibt in localStorage erhalten) ---
const ThemeManager = {
_key: "portal_theme",
init() {
const theme = localStorage.getItem(this._key) === "light" ? "light" : "dark";
this._apply(theme);
},
toggle() {
const next = document.documentElement.getAttribute("data-theme") === "light" ? "dark" : "light";
localStorage.setItem(this._key, next);
this._apply(next);
},
_apply(theme) {
if (theme === "light") document.documentElement.setAttribute("data-theme", "light");
else document.documentElement.removeAttribute("data-theme");
const el = document.getElementById("theme-toggle");
if (el) {
el.classList.remove("dark", "light");
el.classList.add(theme);
el.setAttribute("aria-checked", theme === "dark" ? "true" : "false");
}
},
};
// Sprung zum (nachgeladenen) Verbrauchsrechner-Tab
function openRechnerTab() {
const tab = document.querySelector('.nav-tab[data-section="rechner"]');
if (tab) tab.click();
}
// --- State ---
let currentOrgId = null;
let orgsCache = [];
@@ -53,6 +83,10 @@ let orgsCache = [];
document.addEventListener("DOMContentLoaded", () => {
if (!API.token) { window.location.href = "/"; return; }
ThemeManager.init();
// Barrierefreiheits-Panel (a11y.js, identische Datei wie im Monitor)
if (typeof A11yManager !== "undefined") A11yManager.init();
document.getElementById("headerUser").textContent = localStorage.getItem("username") || "";
document.getElementById("logoutBtn").addEventListener("click", logout);
@@ -368,16 +402,36 @@ async function loadOrgLicenses(orgId) {
}
}
async function extendLicense(licId) {
const days = prompt("Um wie viele Tage verlängern?", "365");
if (!days) return;
// Lizenz verlängern über ein eigenes Modal statt des rohen Browser-prompt()
let _extendLicenseId = null;
function extendLicense(licId) {
_extendLicenseId = licId;
const errEl = document.getElementById("extendLicenseError");
if (errEl) errEl.style.display = "none";
openModal("modalExtendLicense");
const input = document.getElementById("extendDays");
if (input) { input.value = "365"; input.focus(); }
}
document.addEventListener("DOMContentLoaded", function () {
const form = document.getElementById("extendLicenseForm");
if (!form) return;
form.addEventListener("submit", async function (e) {
e.preventDefault();
const errEl = document.getElementById("extendLicenseError");
if (errEl) errEl.style.display = "none";
const days = parseInt(document.getElementById("extendDays").value);
if (!days || days < 1) return;
try {
await API.put(`/api/licenses/${licId}/extend?days=${parseInt(days)}`);
await API.put(`/api/licenses/${_extendLicenseId}/extend?days=${days}`);
closeModal("modalExtendLicense");
if (currentOrgId) loadOrgLicenses(currentOrgId);
loadExpiringLicenses();
} catch (err) {
showToast(err.message, "error");
}
if (errEl) { errEl.textContent = err.message; errEl.style.display = "block"; }
}
});
});
function confirmRevokeLicense(licId) {
showConfirm(
@@ -1169,6 +1223,6 @@ document.addEventListener("DOMContentLoaded", () => {
// --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) ---
(function () {
var s = document.createElement("script");
s.src = "/static/js/rechner.js?v=20260724d";
s.src = "/static/js/rechner.js?v=20260725a";
document.head.appendChild(s);
})();

Datei anzeigen

@@ -344,6 +344,7 @@
return '<div class="vr-kopf">' +
"<h2>Verbrauchsrechner</h2>" +
"<p>Sätze frei einstellbar. Die Empfehlung stammt aus den echten API-Kosten der letzten 30 Tage (Median je Aktion geteilt durch 0,20 USD).</p>" +
'<p class="vr-basis"><strong>Reine Simulation.</strong> Die Regler ändern keine Sätze im Monitor, abgerechnet wird nach der zentralen Konfiguration (aktuell 45 je Live-Lauf, 40 je Recherche-Durchlauf, Nebenkosten 1).</p>' +
"</div>" +
'<div class="vr-regler-grid">' +
reglerHtml("live", "Live-Monitoring", "Anlegen und Aktualisierung", 1, 100) +
@@ -388,7 +389,7 @@
var btn = document.createElement("button");
btn.className = "nav-tab";
btn.setAttribute("data-section", "rechner");
btn.textContent = "Verbrauchsrechner";
btn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="4" y="2" width="16" height="20" rx="2"/><line x1="8" x2="16" y1="6" y2="6"/><line x1="16" x2="16" y1="14" y2="18"/><path d="M16 10h.01"/><path d="M12 10h.01"/><path d="M8 10h.01"/><path d="M12 14h.01"/><path d="M8 14h.01"/><path d="M12 18h.01"/><path d="M8 18h.01"/></svg>Verbrauchsrechner';
nav.appendChild(btn);
btn.addEventListener("click", function () {