From 26b2c0813857756bdd7eff6c8e8313ca0ec4e16d Mon Sep 17 00:00:00 2001 From: claude-dev Date: Sat, 25 Jul 2026 11:39:25 +0000 Subject: [PATCH] fix(studio): Kopfzeile und Fall-Auswahl an die klassische Ansicht angeglichen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Portierung aus dem Lokal-Fork (AegisSight-Monitor-Local, Commit 9613610). Vier Angleichungen an das klassische Dashboard. 1. "Klassische Ansicht" ist jetzt derselbe Knopf wie "Studio-Ansicht" im Dashboard, mit Symbol, an erster Stelle im rechten Kopfzeilen-Block. 2. Oben rechts fehlten Barrierefreiheit, Konto-Menü und Abmelden. Der A11yManager liegt jetzt in js/a11y.js und wird von beiden Oberflächen eingebunden, denn das Studio lädt app.js nicht. Konto-Menü (Organisation, Lizenz, Credits, Über KI-Inhalte) und Abmelden nachgezogen, der Theme-Schalter ist derselbe Schiebeschalter wie im Dashboard. 3. Die Fall-Liste hat jetzt die Umschaltung "Alle" / "Eigene" in der Knopf-Optik der klassischen Seitenleiste, der Reiter-Zähler zieht mit. 4. Der Hinweis "Wähle links einen Fall aus" endet vor der Bausteine-Spalte statt am Fensterrand, im gestapelten Layout nimmt er die volle Breite. Das info@-Gating des Studios bleibt unverändert bestehen. Versionsmarker der angefassten Dateien auf 20260725a angehoben. Co-Authored-By: Claude Fable 5 --- src/static/css/studio.css | 33 ++++---- src/static/dashboard.html | 3 +- src/static/js/a11y.js | 162 ++++++++++++++++++++++++++++++++++++++ src/static/js/app.js | 158 +------------------------------------ src/static/js/studio.js | 124 ++++++++++++++++++++++++++--- src/static/studio.html | 61 ++++++++++++-- 6 files changed, 354 insertions(+), 187 deletions(-) create mode 100644 src/static/js/a11y.js diff --git a/src/static/css/studio.css b/src/static/css/studio.css index 58bdae9..22f7a8b 100644 --- a/src/static/css/studio.css +++ b/src/static/css/studio.css @@ -114,21 +114,10 @@ align-items: center; gap: var(--sp-md); } -.studio-top-right a { color: var(--text-secondary); font-size: 12px; text-decoration: none; } -.studio-top-right a:hover { color: var(--accent); } - -.studio-theme-toggle { - background: transparent; - border: 1px solid var(--border); - color: var(--text-secondary); - border-radius: var(--radius); - width: 32px; height: 32px; - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; -} -.studio-theme-toggle:hover { color: var(--accent); border-color: var(--accent); } +/* Reine Textlinks in der Kopfzeile. Links, die als Knopf auftreten (.btn), + bleiben ausgenommen, sonst ueberschreibt die Farbe hier die Knopf-Optik. */ +.studio-top-right a:not(.btn) { color: var(--text-secondary); font-size: 12px; text-decoration: none; } +.studio-top-right a:not(.btn):hover { color: var(--accent); } /* === 3 Spalten === */ .studio-cols { @@ -196,6 +185,15 @@ } .left-pane[hidden] { display: none; } +/* "Alle" / "Eigene" ueber der Fall-Liste. Nutzt die Knopf-Optik der klassischen + Seitenleiste (.sidebar-filter-btn aus style.css), braucht hier aber den + Innenabstand der Studio-Spalte statt des Seitenleisten-Abstands. */ +.case-scope { + padding: var(--sp-md) var(--sp-xl) 0; + margin-bottom: 0; + flex-shrink: 0; +} + /* Fall-Liste (ersetzt das Dropdown in der Kopfzeile) */ .case-group { display: flex; @@ -1330,7 +1328,8 @@ top: 0; bottom: 0; left: 301px; /* rechts neben der Fall-Liste (300px + 1px Fuge) */ - right: 0; + right: 321px; /* und links neben der Bausteine-Spalte (320px + 1px Fuge), + sonst sitzt der Hinweis sichtbar zu weit rechts */ z-index: 5; display: flex; align-items: center; @@ -1360,6 +1359,8 @@ @media (max-width: 1100px) { .studio-cols { grid-template-columns: 1fr; grid-auto-rows: minmax(0, 1fr); } .studio-col { max-height: 60vh; } + /* Gestapelte Spalten: der Hinweis nimmt die volle Breite */ + .studio-empty { left: 0; right: 0; } } /* Fall-Chat: Recherche-Angebot des Analysten (Phase 3) */ diff --git a/src/static/dashboard.html b/src/static/dashboard.html index 32b96ef..48b8d57 100644 --- a/src/static/dashboard.html +++ b/src/static/dashboard.html @@ -812,7 +812,8 @@ - + + diff --git a/src/static/js/a11y.js b/src/static/js/a11y.js new file mode 100644 index 0000000..93db539 --- /dev/null +++ b/src/static/js/a11y.js @@ -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 = ` + + + `; + + 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(); + } +}; diff --git a/src/static/js/app.js b/src/static/js/app.js index f59e99b..c445835 100644 --- a/src/static/js/app.js +++ b/src/static/js/app.js @@ -44,162 +44,10 @@ const ThemeManager = { } }; -/** - * Barrierefreiheits-Manager: Panel mit 4 Schaltern (Kontrast, Focus, Schrift, Animationen). +/* + * Der A11yManager (Barrierefreiheits-Panel) liegt seit 2026-07-25 in + * js/a11y.js, weil ihn die Studio-Ansicht ebenfalls einbindet. */ -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 .header-right einfügen (vor Theme-Toggle) - const headerRight = document.querySelector('.header-right'); - const themeToggle = document.getElementById('theme-toggle'); - if (!headerRight) return; - - const container = document.createElement('div'); - container.className = 'a11y-center'; - container.innerHTML = ` - - - `; - - if (themeToggle) { - 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(); - } -}; /** * Notification-Center: Glocke mit Badge + History-Panel. diff --git a/src/static/js/studio.js b/src/static/js/studio.js index d641093..82f0da4 100644 --- a/src/static/js/studio.js +++ b/src/static/js/studio.js @@ -60,6 +60,8 @@ const Studio = { ], caseFilter: '', + caseScope: 'all', // 'all' | 'mine' - wie die Seitenleiste im klassischen Dashboard + _me: null, // Antwort von /auth/me (fuer Konto-Menue und "Eigene") _archiveOpen: false, caseSel: new Set(), // Mehrfachauswahl in der Fall-Liste (IDs) caseMode: null, // null = normale Liste, 'archive' | 'delete' = Auswahlmodus @@ -67,10 +69,11 @@ const Studio = { async init() { if (!localStorage.getItem('osint_token')) { window.location.href = '/'; return; } try { - const me = await API.getMe(); + this._me = await API.getMe(); // Studio ist vorerst nur fuer info@aegis-sight.de freigegeben. - if (!me || me.email !== 'info@aegis-sight.de') { window.location.href = '/dashboard'; return; } + if (!this._me || this._me.email !== 'info@aegis-sight.de') { window.location.href = '/dashboard'; return; } } catch (e) { /* 401 leitet in api.js um */ } + this._initHeader(); WS.connect(); this._wireWs(); this._initDivider(); @@ -86,6 +89,77 @@ const Studio = { } }, + /** + * Kopfzeile rechts aufbauen. Bewusst derselbe Umfang wie im klassischen + * Dashboard, damit man beim Wechsel der Ansicht nichts verliert. + * Barrierefreiheit, Theme-Schalter, Konto-Menue und Abmelden. + */ + _initHeader() { + // Barrierefreiheits-Knopf haengt sich selbst vor den Theme-Schalter + if (window.A11yManager || typeof A11yManager !== 'undefined') A11yManager.init(); + + // Theme-Schalter kennt zwei Zustaende ueber seine Klasse + this._syncThemeSwitch(); + + const user = this._me; + if (!user) return; + + const userEl = document.getElementById('header-user'); + if (userEl) userEl.textContent = user.email || ''; + + const orgEl = document.getElementById('header-org-name'); + if (orgEl) orgEl.textContent = user.org_name || '-'; + + const licEl = document.getElementById('header-license-info'); + if (licEl) { + const labels = { trial: 'Trial', annual: 'Jahreslizenz', permanent: 'Permanent' }; + licEl.textContent = user.read_only + ? 'Abgelaufen' + : (labels[user.license_type] || user.license_status || '-'); + } + + // Credits nur zeigen, wenn die Lizenz welche fuehrt + const creditsSection = document.getElementById('credits-section'); + if (creditsSection && user.credits_total) { + creditsSection.style.display = 'block'; + const percentUsed = user.credits_percent_used || 0; + const percentRemaining = Math.max(0, 100 - percentUsed); + const bar = document.getElementById('credits-bar'); + document.getElementById('credits-remaining').textContent = (user.credits_remaining || 0).toLocaleString('de-DE'); + document.getElementById('credits-total').textContent = (user.credits_total || 0).toLocaleString('de-DE'); + bar.style.width = percentRemaining + '%'; + bar.classList.remove('warning', 'critical'); + if (percentUsed > 80) bar.classList.add('critical'); + else if (percentUsed > 50) bar.classList.add('warning'); + const pct = document.getElementById('credits-percent'); + if (pct) pct.textContent = percentRemaining.toFixed(0) + '% verbleibend'; + } + + // Konto-Menue auf- und zuklappen + const btn = document.getElementById('header-user-btn'); + const dropdown = document.getElementById('header-user-dropdown'); + if (btn && dropdown) { + btn.addEventListener('click', (e) => { + e.stopPropagation(); + const isOpen = dropdown.classList.toggle('open'); + btn.setAttribute('aria-expanded', String(isOpen)); + }); + dropdown.addEventListener('click', (e) => e.stopPropagation()); + document.addEventListener('click', () => { + dropdown.classList.remove('open'); + btn.setAttribute('aria-expanded', 'false'); + }); + } + }, + + logout() { + localStorage.removeItem('osint_token'); + localStorage.removeItem('osint_username'); + localStorage.removeItem('studio_incident'); + WS.disconnect(); + window.location.href = '/'; + }, + /** * Ohne offenen Fall bleiben Mitte und rechte Spalte leer - die LINKE Spalte * muss aber stehen bleiben, sonst käme man an die Fall-Auswahl nicht heran. @@ -129,6 +203,29 @@ const Studio = { this.renderCases(); }, + // "Alle" oder nur die selbst angelegten Fälle, wie in der Seitenleiste + // des klassischen Dashboards. + setCaseScope(scope) { + this.caseScope = scope === 'mine' ? 'mine' : 'all'; + document.querySelectorAll('.case-scope .sidebar-filter-btn').forEach(btn => { + const on = btn.dataset.scope === this.caseScope; + btn.classList.toggle('active', on); + btn.setAttribute('aria-pressed', String(on)); + }); + this.renderCases(); + }, + + // Trifft der Fall den aktuellen Suchtext und die Auswahl "Alle"/"Eigene"? + _caseMatches(i) { + const q = this.caseFilter; + if (q && !(i.title || '').toLowerCase().includes(q)) return false; + if (this.caseScope === 'mine') { + const me = this._me && this._me.email; + if (!me || i.created_by_username !== me) return false; + } + return true; + }, + toggleArchive() { this._archiveOpen = !this._archiveOpen; this.renderCases(); @@ -137,17 +234,13 @@ const Studio = { // Fälle, die der aktuelle Filter zeigt (Archiv nur, wenn aufgeklappt) - genau // diese Menge trifft "Alle auswählen". _visibleCases() { - const q = this.caseFilter; - const match = (i) => !q || (i.title || '').toLowerCase().includes(q); - return this.incidents.filter(i => match(i) && (i.status === 'active' || this._archiveOpen)); + return this.incidents.filter(i => this._caseMatches(i) && (i.status === 'active' || this._archiveOpen)); }, renderCases() { const list = document.getElementById('cases-list'); if (!list) return; - const q = this.caseFilter; - const match = (i) => !q || (i.title || '').toLowerCase().includes(q); - const all = this.incidents.filter(match); + const all = this.incidents.filter(i => this._caseMatches(i)); const cur = this.incident ? String(this.incident.id) : null; const groups = [ @@ -189,8 +282,9 @@ const Studio = { } list.innerHTML = html; // ohne Treffer bleibt die Liste bewusst leer (kein Hinweistext) + // Zähler am Reiter zeigt, was die Liste gerade zeigt (also inkl. "Eigene") const cnt = document.getElementById('cases-count'); - if (cnt) cnt.textContent = this.incidents.filter(i => i.status === 'active').length || ''; + if (cnt) cnt.textContent = all.filter(i => i.status === 'active').length || ''; this._renderBulkBar(); }, @@ -2047,9 +2141,21 @@ const Studio = { if (next === 'light') document.documentElement.setAttribute('data-theme', 'light'); else document.documentElement.removeAttribute('data-theme'); localStorage.setItem('osint_theme', next); + this._syncThemeSwitch(); if (UI.updateMapTheme) UI.updateMapTheme(); }, + // Der Schiebeschalter aus dem klassischen Dashboard zeigt seinen Zustand ueber + // die Klasse 'dark' bzw. 'light'. + _syncThemeSwitch() { + const el = document.getElementById('theme-toggle'); + if (!el) return; + const theme = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark'; + el.classList.remove('dark', 'light'); + el.classList.add(theme); + el.setAttribute('aria-checked', theme === 'dark' ? 'true' : 'false'); + }, + // === Zeit-Helfer === _ts(a) { const d = parseUTC(a.published_at || a.collected_at || a.created_at); diff --git a/src/static/studio.html b/src/static/studio.html index 7c74494..7398643 100644 --- a/src/static/studio.html +++ b/src/static/studio.html @@ -13,7 +13,7 @@ - +
@@ -37,11 +37,53 @@
+
- - Klassische Ansicht + + + Klassische Ansicht + +
+ ☀︎ +
+
+
+ +
+ +
@@ -62,6 +104,11 @@