fix(studio): Kopfzeile und Fall-Auswahl an die klassische Ansicht angeglichen
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 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
@@ -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);
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren