feat(design): AegisSight-Design, Lucide-Icons und aufgeräumter Verbrauchs-Bereich
Drei Pakete auf Nutzerwunsch. 1. Design-Angleichung an das AegisSight-Design-System. Navy/Gold-Token statt Slate/Amber, Light- UND Dark-Theme mit goldenem Schiebeschalter oben rechts (Dark = Standard, Wahl persistiert), Barrierefreiheits- Panel über js/a11y.js (identische Datei wie im Monitor), echtes Logo mit heller Plakette in Kopfzeile und Login, Poppins/Inter-Typografie, Radius 6/8 nach Styleguide. Automatisch erzeugte Light-Theme- Überschreibungen für alle Badge-Pastelltöne. 2. Lucide-Icons (Inline-SVG, keine externen Quellen) für alle Navigations-Tabs inkl. nachgeladenem Verbrauchsrechner, die Anlege-Knöpfe, Abmelden und Speichern. 3. Verbrauchs-Bereich aufgeräumt. Reiter heißt jetzt Verbrauch, Statistik in Kundensicht (Credits) und Interne Kosten (Einkauf, USD) getrennt, Leiste heißt Kontingent-Auslastung, Sätze-Infozeile mit Sprung zum Verbrauchsrechner, Altmodus-Felder (USD) und die Verbrauchs-Korrektur liegen in einem eingeklappten Bereich Erweitert mit Erklärtext. Der Rechner trägt einen deutlichen Simulations-Hinweis. Lizenz verlängern nutzt ein eigenes Modal statt des Browser-prompt(). Cache-Buster auf 20260725 gebumpt, alle Restfarben tokenisiert. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -63,7 +63,8 @@ src/:
|
|||||||
favicon.svg: "AegisSight Logo"
|
favicon.svg: "AegisSight Logo"
|
||||||
css/: "Stylesheets (Dark Theme)"
|
css/: "Stylesheets (Dark Theme)"
|
||||||
js/:
|
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"
|
sources.js: "Grundquellen + Kundenquellen Management"
|
||||||
source-health.js: "Quellen-Health & KI-Vorschläge"
|
source-health.js: "Quellen-Health & KI-Vorschläge"
|
||||||
audit.js: "Audit-Log Tab"
|
audit.js: "Audit-Log Tab"
|
||||||
|
|||||||
@@ -1,32 +1,63 @@
|
|||||||
/* AegisSight Dark Theme - Verwaltungsportal */
|
/* AegisSight Design System - Verwaltungsportal (Navy/Gold, Dark = Standard, Light per Umschalter) */
|
||||||
:root {
|
:root {
|
||||||
--bg-primary: #0f172a;
|
--bg-primary: #0B1121;
|
||||||
--bg-secondary: #1e293b;
|
--bg-secondary: #151D2E;
|
||||||
--bg-tertiary: #334155;
|
--bg-tertiary: #1A2440;
|
||||||
--text-primary: #e2e8f0;
|
--bg-hover: #1A2440;
|
||||||
--text-secondary: #94a3b8;
|
--text-primary: #E8ECF4;
|
||||||
--text-muted: #64748b;
|
--text-secondary: #8896AB;
|
||||||
--accent: #f0b429;
|
--text-muted: #95A3B8;
|
||||||
--accent-hover: #d4a017;
|
--accent: #C8A851;
|
||||||
--success: #22c55e;
|
--accent-hover: #B5923E;
|
||||||
--warning: #f59e0b;
|
--success: #10B981;
|
||||||
--danger: #ef4444;
|
--warning: #F59E0B;
|
||||||
--info: #3b82f6;
|
--danger: #EF4444;
|
||||||
--border: #334155;
|
--info: #7C8DB5;
|
||||||
--radius: 8px;
|
--border: #1E2D45;
|
||||||
--radius-lg: 12px;
|
--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; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
|
||||||
body {
|
body {
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Inter, sans-serif;
|
font-family: var(--font-body);
|
||||||
background: var(--bg-primary);
|
background: var(--bg-primary);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, .logo, .stat-value, .token-stat-value {
|
||||||
|
font-family: var(--font-title);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Login Page --- */
|
/* --- Login Page --- */
|
||||||
.login-page {
|
.login-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -143,7 +174,7 @@ input:focus, select:focus, textarea:focus {
|
|||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
.btn-secondary:hover:not(:disabled) {
|
.btn-secondary:hover:not(:disabled) {
|
||||||
background: #475569;
|
background: var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
@@ -189,6 +220,9 @@ input:focus, select:focus, textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-header .logo {
|
.app-header .logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
@@ -198,9 +232,19 @@ input:focus, select:focus, textarea:focus {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
font-size: 13px;
|
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 {
|
.header-right {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -237,7 +281,11 @@ input:focus, select:focus, textarea:focus {
|
|||||||
border-bottom: 2px solid transparent;
|
border-bottom: 2px solid transparent;
|
||||||
margin-bottom: -1px;
|
margin-bottom: -1px;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
}
|
}
|
||||||
|
.nav-tab svg { flex-shrink: 0; }
|
||||||
|
|
||||||
.nav-tab:hover {
|
.nav-tab:hover {
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
@@ -1121,3 +1169,224 @@ input[type="date"].filter-select { padding: 6px 10px; }
|
|||||||
border-color: var(--accent);
|
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; }
|
||||||
|
|||||||
@@ -6,32 +6,63 @@
|
|||||||
<title>AegisSight Monitor-Verwaltung</title>
|
<title>AegisSight Monitor-Verwaltung</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
<link rel="apple-touch-icon" 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>
|
<style>
|
||||||
.source-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:600; }
|
.source-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:12px; font-weight:600; }
|
||||||
.badge-monitor { background:#e3f2fd; color:#1565c0; }
|
.badge-monitor { background:rgba(59,130,246,0.15); color:#93c5fd; }
|
||||||
.badge-globe { background:#e8f5e9; color:#2e7d32; }
|
.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>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header class="app-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="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">☀︎</span>
|
||||||
|
<div class="theme-switch-track">
|
||||||
|
<div class="theme-switch-knob"></div>
|
||||||
|
</div>
|
||||||
|
<span class="theme-switch-icon theme-switch-moon">☽</span>
|
||||||
|
</div>
|
||||||
<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">
|
||||||
|
<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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="app-content">
|
<main class="app-content">
|
||||||
<!-- Navigation -->
|
<!-- Navigation (Lucide-Icons, Strichstaerke 2 bei 15px) -->
|
||||||
<nav class="nav-tabs">
|
<nav class="nav-tabs">
|
||||||
<button class="nav-tab active" data-section="dashboard">Dashboard</button>
|
<button class="nav-tab active" data-section="dashboard">
|
||||||
<button class="nav-tab" data-section="orgs">Organisationen</button>
|
<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>
|
||||||
<button class="nav-tab" data-section="licenses">Lizenzen</button>
|
Dashboard
|
||||||
<button class="nav-tab" data-section="sources">Quellen</button>
|
</button>
|
||||||
<button class="nav-tab" data-section="audit">Audit-Log</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>
|
</nav>
|
||||||
|
|
||||||
<!-- Dashboard Section -->
|
<!-- Dashboard Section -->
|
||||||
@@ -93,7 +124,7 @@
|
|||||||
<div id="orgListView">
|
<div id="orgListView">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<input type="text" class="search-input" id="orgSearch" placeholder="Organisation suchen...">
|
<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>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -123,14 +154,14 @@
|
|||||||
<button class="nav-tab active" data-subtab="users">Nutzer</button>
|
<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="org-licenses">Lizenzen</button>
|
||||||
<button class="nav-tab" data-subtab="settings">Einstellungen</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>
|
</div>
|
||||||
|
|
||||||
<!-- Users Sub-Tab -->
|
<!-- Users Sub-Tab -->
|
||||||
<div class="section active" id="sub-users">
|
<div class="section active" id="sub-users">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<span class="text-secondary" id="userLimitInfo"></span>
|
<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>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -155,7 +186,7 @@
|
|||||||
<div class="section" id="sub-org-licenses">
|
<div class="section" id="sub-org-licenses">
|
||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<span></span>
|
<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>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="table-wrap">
|
<div class="table-wrap">
|
||||||
@@ -211,34 +242,46 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="section" id="sub-tokens">
|
<div class="section" id="sub-tokens">
|
||||||
<div class="token-overview">
|
<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-stats-row" id="tokenStatsRow">
|
||||||
<div class="token-stat-card">
|
<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 class="token-stat-value" id="tokenCreditsUsed">-</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="token-stat-card">
|
<div class="token-stat-card">
|
||||||
<div class="token-stat-label">Verbleibend</div>
|
<div class="token-stat-label">Verbleibend</div>
|
||||||
<div class="token-stat-value" id="tokenCreditsRemaining">-</div>
|
<div class="token-stat-value" id="tokenCreditsRemaining">-</div>
|
||||||
</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>
|
||||||
<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-bar-section">
|
||||||
<div class="token-budget-label">
|
<div class="token-budget-label">
|
||||||
<span>Budget-Auslastung</span>
|
<span>Kontingent-Auslastung (laufende Periode)</span>
|
||||||
<span id="tokenBudgetPercent">0%</span>
|
<span id="tokenBudgetPercent">0%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="token-budget-bar-container">
|
<div class="token-budget-bar-container">
|
||||||
<div class="token-budget-bar" id="tokenBudgetBar"></div>
|
<div class="token-budget-bar" id="tokenBudgetBar"></div>
|
||||||
</div>
|
</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>
|
<h3 style="margin-top:24px;">Monatlicher Verlauf</h3>
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead>
|
<thead>
|
||||||
@@ -255,11 +298,11 @@
|
|||||||
<tbody id="tokenMonthlyTable"></tbody>
|
<tbody id="tokenMonthlyTable"></tbody>
|
||||||
</table>
|
</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" style="margin-top:12px;">
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form id="tokenBudgetForm" style="display:grid; grid-template-columns:1fr 1fr; gap:12px;">
|
<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;">
|
<label style="display:flex; align-items:center; gap:8px; cursor:pointer; margin:0;">
|
||||||
<input type="checkbox" id="editUnlimitedBudget" onchange="onUnlimitedToggle()">
|
<input type="checkbox" id="editUnlimitedBudget" onchange="onUnlimitedToggle()">
|
||||||
<span><strong>Unbegrenztes Budget</strong> — Verbrauch wird trotzdem getrackt, aber kein Limit/Hard-Stop</span>
|
<span><strong>Unbegrenztes Budget</strong> — Verbrauch wird trotzdem getrackt, aber kein Limit/Hard-Stop</span>
|
||||||
@@ -286,20 +329,37 @@
|
|||||||
<label for="editWarnPercent">Warnschwelle (%)</label>
|
<label for="editWarnPercent">Warnschwelle (%)</label>
|
||||||
<input type="number" id="editWarnPercent" min="1" max="100" placeholder="80">
|
<input type="number" id="editWarnPercent" min="1" max="100" placeholder="80">
|
||||||
</div>
|
</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">
|
<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">
|
<input type="number" id="editCostPerCredit" step="0.01" placeholder="0.20">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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">
|
<input type="number" id="editBudgetUsd" step="0.01" placeholder="z.B. 2000">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<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">
|
<input type="number" id="editCreditsUsed" step="0.01" placeholder="Aktueller Verbrauch">
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
<div style="grid-column:1/-1; display:flex; gap:8px; align-items:center;">
|
<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>
|
<span id="tokenBudgetMsg" style="font-size:13px; color:var(--text-secondary);"></span>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@@ -786,6 +846,30 @@
|
|||||||
</div>
|
</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')">×</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) -->
|
<!-- Modal: Source (Create/Edit) -->
|
||||||
<div class="modal-overlay" id="modalSource">
|
<div class="modal-overlay" id="modalSource">
|
||||||
<div class="modal">
|
<div class="modal">
|
||||||
@@ -1154,7 +1238,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</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/sources.js?v=20260522x2"></script>
|
||||||
<script src="/static/js/x-scraper.js?v=20260522a"></script>
|
<script src="/static/js/x-scraper.js?v=20260522a"></script>
|
||||||
<script src="/static/js/source-health.js?v=20260509l"></script>
|
<script src="/static/js/source-health.js?v=20260509l"></script>
|
||||||
|
|||||||
@@ -7,12 +7,14 @@
|
|||||||
<title>AegisSight Monitor-Verwaltung - Anmeldung</title>
|
<title>AegisSight Monitor-Verwaltung - Anmeldung</title>
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||||
<link rel="apple-touch-icon" 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>
|
</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">
|
||||||
|
<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>
|
<h1>AegisSight</h1>
|
||||||
<p class="subtitle">Monitor-Verwaltung</p>
|
<p class="subtitle">Monitor-Verwaltung</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -26,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="loginError" class="error-msg" style="display:none"></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>
|
<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.
|
Wir senden dir einen einmaligen Login-Link per E-Mail.
|
||||||
</p>
|
</p>
|
||||||
</form>
|
</form>
|
||||||
@@ -35,7 +37,7 @@
|
|||||||
<div id="sentInfo" style="display:none;text-align:center;">
|
<div id="sentInfo" style="display:none;text-align:center;">
|
||||||
<div style="font-size:42px;margin:8px 0 16px 0;">✉</div>
|
<div style="font-size:42px;margin:8px 0 16px 0;">✉</div>
|
||||||
<h2 style="font-size:17px;margin:0 0 8px 0;">E-Mail unterwegs</h2>
|
<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.
|
Wenn die Adresse berechtigt ist, hast du gleich einen Login-Link in deinem Posteingang.
|
||||||
Der Link ist 10 Minuten gültig.
|
Der Link ist 10 Minuten gültig.
|
||||||
</p>
|
</p>
|
||||||
@@ -46,7 +48,7 @@
|
|||||||
<div id="verifying" style="display:none;text-align:center;">
|
<div id="verifying" style="display:none;text-align:center;">
|
||||||
<div class="spinner" style="margin:8px auto 16px;"></div>
|
<div class="spinner" style="margin:8px auto 16px;"></div>
|
||||||
<h2 style="font-size:17px;margin:0 0 8px 0;">Anmeldung wird geprüft...</h2>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -54,13 +56,13 @@
|
|||||||
<style>
|
<style>
|
||||||
.spinner {
|
.spinner {
|
||||||
width: 36px; height: 36px;
|
width: 36px; height: 36px;
|
||||||
border: 3px solid rgba(240,180,41,0.2);
|
border: 3px solid rgba(200,168,81,0.2);
|
||||||
border-top-color: #f0b429;
|
border-top-color: var(--accent);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 0.8s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
}
|
}
|
||||||
@keyframes spin { to { transform: rotate(360deg); } }
|
@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>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
162
src/static/js/a11y.js
Normale Datei
162
src/static/js/a11y.js
Normale Datei
@@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 ---
|
// --- State ---
|
||||||
let currentOrgId = null;
|
let currentOrgId = null;
|
||||||
let orgsCache = [];
|
let orgsCache = [];
|
||||||
@@ -53,6 +83,10 @@ let orgsCache = [];
|
|||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
if (!API.token) { window.location.href = "/"; return; }
|
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("headerUser").textContent = localStorage.getItem("username") || "";
|
||||||
document.getElementById("logoutBtn").addEventListener("click", logout);
|
document.getElementById("logoutBtn").addEventListener("click", logout);
|
||||||
|
|
||||||
@@ -368,17 +402,37 @@ async function loadOrgLicenses(orgId) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function extendLicense(licId) {
|
// Lizenz verlängern über ein eigenes Modal statt des rohen Browser-prompt()
|
||||||
const days = prompt("Um wie viele Tage verlängern?", "365");
|
let _extendLicenseId = null;
|
||||||
if (!days) return;
|
function extendLicense(licId) {
|
||||||
try {
|
_extendLicenseId = licId;
|
||||||
await API.put(`/api/licenses/${licId}/extend?days=${parseInt(days)}`);
|
const errEl = document.getElementById("extendLicenseError");
|
||||||
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
if (errEl) errEl.style.display = "none";
|
||||||
} catch (err) {
|
openModal("modalExtendLicense");
|
||||||
showToast(err.message, "error");
|
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/${_extendLicenseId}/extend?days=${days}`);
|
||||||
|
closeModal("modalExtendLicense");
|
||||||
|
if (currentOrgId) loadOrgLicenses(currentOrgId);
|
||||||
|
loadExpiringLicenses();
|
||||||
|
} catch (err) {
|
||||||
|
if (errEl) { errEl.textContent = err.message; errEl.style.display = "block"; }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
function confirmRevokeLicense(licId) {
|
function confirmRevokeLicense(licId) {
|
||||||
showConfirm(
|
showConfirm(
|
||||||
"Lizenz widerrufen",
|
"Lizenz widerrufen",
|
||||||
@@ -1169,6 +1223,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
// --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) ---
|
// --- Verbrauchsrechner nachladen (eigenstaendiges Modul, siehe rechner.js) ---
|
||||||
(function () {
|
(function () {
|
||||||
var s = document.createElement("script");
|
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);
|
document.head.appendChild(s);
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -344,6 +344,7 @@
|
|||||||
return '<div class="vr-kopf">' +
|
return '<div class="vr-kopf">' +
|
||||||
"<h2>Verbrauchsrechner</h2>" +
|
"<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>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>" +
|
||||||
'<div class="vr-regler-grid">' +
|
'<div class="vr-regler-grid">' +
|
||||||
reglerHtml("live", "Live-Monitoring", "Anlegen und Aktualisierung", 1, 100) +
|
reglerHtml("live", "Live-Monitoring", "Anlegen und Aktualisierung", 1, 100) +
|
||||||
@@ -388,7 +389,7 @@
|
|||||||
var btn = document.createElement("button");
|
var btn = document.createElement("button");
|
||||||
btn.className = "nav-tab";
|
btn.className = "nav-tab";
|
||||||
btn.setAttribute("data-section", "rechner");
|
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);
|
nav.appendChild(btn);
|
||||||
|
|
||||||
btn.addEventListener("click", function () {
|
btn.addEventListener("click", function () {
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren