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:
@@ -114,21 +114,10 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--sp-md);
|
gap: var(--sp-md);
|
||||||
}
|
}
|
||||||
.studio-top-right a { color: var(--text-secondary); font-size: 12px; text-decoration: none; }
|
/* Reine Textlinks in der Kopfzeile. Links, die als Knopf auftreten (.btn),
|
||||||
.studio-top-right a:hover { color: var(--accent); }
|
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-theme-toggle {
|
.studio-top-right a:not(.btn):hover { color: var(--accent); }
|
||||||
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); }
|
|
||||||
|
|
||||||
/* === 3 Spalten === */
|
/* === 3 Spalten === */
|
||||||
.studio-cols {
|
.studio-cols {
|
||||||
@@ -196,6 +185,15 @@
|
|||||||
}
|
}
|
||||||
.left-pane[hidden] { display: none; }
|
.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) */
|
/* Fall-Liste (ersetzt das Dropdown in der Kopfzeile) */
|
||||||
.case-group {
|
.case-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -1330,7 +1328,8 @@
|
|||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 301px; /* rechts neben der Fall-Liste (300px + 1px Fuge) */
|
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;
|
z-index: 5;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1360,6 +1359,8 @@
|
|||||||
@media (max-width: 1100px) {
|
@media (max-width: 1100px) {
|
||||||
.studio-cols { grid-template-columns: 1fr; grid-auto-rows: minmax(0, 1fr); }
|
.studio-cols { grid-template-columns: 1fr; grid-auto-rows: minmax(0, 1fr); }
|
||||||
.studio-col { max-height: 60vh; }
|
.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) */
|
/* Fall-Chat: Recherche-Angebot des Analysten (Phase 3) */
|
||||||
|
|||||||
@@ -812,7 +812,8 @@
|
|||||||
<script src="/static/js/components.js?v=20260723a"></script>
|
<script src="/static/js/components.js?v=20260723a"></script>
|
||||||
<script src="/static/js/layout.js?v=20260513f"></script>
|
<script src="/static/js/layout.js?v=20260513f"></script>
|
||||||
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
<script src="/static/js/pipeline.js?v=20260513d"></script>
|
||||||
<script src="/static/js/app.js?v=20260723a"></script>
|
<script src="/static/js/a11y.js?v=20260725a"></script>
|
||||||
|
<script src="/static/js/app.js?v=20260725a"></script>
|
||||||
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
<script src="/static/js/cluster-data.js?v=20260322f"></script>
|
||||||
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
<script src="/static/js/tutorial.js?v=20260316z"></script>
|
||||||
<script src="/static/js/chat.js?v=20260514e"></script>
|
<script src="/static/js/chat.js?v=20260514e"></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();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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 = `
|
|
||||||
<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) {
|
|
||||||
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.
|
* Notification-Center: Glocke mit Badge + History-Panel.
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ const Studio = {
|
|||||||
],
|
],
|
||||||
|
|
||||||
caseFilter: '',
|
caseFilter: '',
|
||||||
|
caseScope: 'all', // 'all' | 'mine' - wie die Seitenleiste im klassischen Dashboard
|
||||||
|
_me: null, // Antwort von /auth/me (fuer Konto-Menue und "Eigene")
|
||||||
_archiveOpen: false,
|
_archiveOpen: false,
|
||||||
caseSel: new Set(), // Mehrfachauswahl in der Fall-Liste (IDs)
|
caseSel: new Set(), // Mehrfachauswahl in der Fall-Liste (IDs)
|
||||||
caseMode: null, // null = normale Liste, 'archive' | 'delete' = Auswahlmodus
|
caseMode: null, // null = normale Liste, 'archive' | 'delete' = Auswahlmodus
|
||||||
@@ -67,10 +69,11 @@ const Studio = {
|
|||||||
async init() {
|
async init() {
|
||||||
if (!localStorage.getItem('osint_token')) { window.location.href = '/'; return; }
|
if (!localStorage.getItem('osint_token')) { window.location.href = '/'; return; }
|
||||||
try {
|
try {
|
||||||
const me = await API.getMe();
|
this._me = await API.getMe();
|
||||||
// Studio ist vorerst nur fuer info@aegis-sight.de freigegeben.
|
// 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 */ }
|
} catch (e) { /* 401 leitet in api.js um */ }
|
||||||
|
this._initHeader();
|
||||||
WS.connect();
|
WS.connect();
|
||||||
this._wireWs();
|
this._wireWs();
|
||||||
this._initDivider();
|
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
|
* 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.
|
* muss aber stehen bleiben, sonst käme man an die Fall-Auswahl nicht heran.
|
||||||
@@ -129,6 +203,29 @@ const Studio = {
|
|||||||
this.renderCases();
|
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() {
|
toggleArchive() {
|
||||||
this._archiveOpen = !this._archiveOpen;
|
this._archiveOpen = !this._archiveOpen;
|
||||||
this.renderCases();
|
this.renderCases();
|
||||||
@@ -137,17 +234,13 @@ const Studio = {
|
|||||||
// Fälle, die der aktuelle Filter zeigt (Archiv nur, wenn aufgeklappt) - genau
|
// Fälle, die der aktuelle Filter zeigt (Archiv nur, wenn aufgeklappt) - genau
|
||||||
// diese Menge trifft "Alle auswählen".
|
// diese Menge trifft "Alle auswählen".
|
||||||
_visibleCases() {
|
_visibleCases() {
|
||||||
const q = this.caseFilter;
|
return this.incidents.filter(i => this._caseMatches(i) && (i.status === 'active' || this._archiveOpen));
|
||||||
const match = (i) => !q || (i.title || '').toLowerCase().includes(q);
|
|
||||||
return this.incidents.filter(i => match(i) && (i.status === 'active' || this._archiveOpen));
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderCases() {
|
renderCases() {
|
||||||
const list = document.getElementById('cases-list');
|
const list = document.getElementById('cases-list');
|
||||||
if (!list) return;
|
if (!list) return;
|
||||||
const q = this.caseFilter;
|
const all = this.incidents.filter(i => this._caseMatches(i));
|
||||||
const match = (i) => !q || (i.title || '').toLowerCase().includes(q);
|
|
||||||
const all = this.incidents.filter(match);
|
|
||||||
const cur = this.incident ? String(this.incident.id) : null;
|
const cur = this.incident ? String(this.incident.id) : null;
|
||||||
|
|
||||||
const groups = [
|
const groups = [
|
||||||
@@ -189,8 +282,9 @@ const Studio = {
|
|||||||
}
|
}
|
||||||
list.innerHTML = html; // ohne Treffer bleibt die Liste bewusst leer (kein Hinweistext)
|
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');
|
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();
|
this._renderBulkBar();
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -2047,9 +2141,21 @@ const Studio = {
|
|||||||
if (next === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
if (next === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
||||||
else document.documentElement.removeAttribute('data-theme');
|
else document.documentElement.removeAttribute('data-theme');
|
||||||
localStorage.setItem('osint_theme', next);
|
localStorage.setItem('osint_theme', next);
|
||||||
|
this._syncThemeSwitch();
|
||||||
if (UI.updateMapTheme) UI.updateMapTheme();
|
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 ===
|
// === Zeit-Helfer ===
|
||||||
_ts(a) {
|
_ts(a) {
|
||||||
const d = parseUTC(a.published_at || a.collected_at || a.created_at);
|
const d = parseUTC(a.published_at || a.collected_at || a.created_at);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.css">
|
||||||
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
<link rel="stylesheet" href="/static/vendor/MarkerCluster.Default.css">
|
||||||
<link rel="stylesheet" href="/static/css/style.css?v=20260501h">
|
<link rel="stylesheet" href="/static/css/style.css?v=20260501h">
|
||||||
<link rel="stylesheet" href="/static/css/studio.css?v=20260714b">
|
<link rel="stylesheet" href="/static/css/studio.css?v=20260725a">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="studio">
|
<div class="studio">
|
||||||
@@ -37,11 +37,53 @@
|
|||||||
<span class="studio-status-timer" id="live-status-timer"></span>
|
<span class="studio-status-timer" id="live-status-timer"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Gleicher Aufbau wie im klassischen Dashboard: Ansichtswechsel, Barrierefreiheit
|
||||||
|
(wird von a11y.js hier eingehaengt), Theme, Konto, Abmelden. -->
|
||||||
<div class="studio-top-right">
|
<div class="studio-top-right">
|
||||||
<button class="studio-theme-toggle" onclick="Studio.toggleTheme()" title="Hell/Dunkel" aria-label="Theme wechseln">
|
<a href="/dashboard" class="btn btn-secondary btn-small" id="classic-view-link" title="Zur klassischen Ansicht wechseln" style="text-decoration:none;display:inline-flex;align-items:center;gap:6px;">
|
||||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M6.3 17.7l-1.4 1.4M19.1 4.9l-1.4 1.4"/></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/></svg>
|
||||||
</button>
|
<span>Klassische Ansicht</span>
|
||||||
<a href="/dashboard">Klassische Ansicht</a>
|
</a>
|
||||||
|
<div class="theme-switch" id="theme-toggle" onclick="Studio.toggleTheme()" 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>
|
||||||
|
<div class="header-user-info">
|
||||||
|
<button class="header-user-btn" id="header-user-btn" aria-expanded="false" aria-haspopup="true">
|
||||||
|
<span class="header-user" id="header-user"></span>
|
||||||
|
<span class="header-user-chevron" aria-hidden="true">▾</span>
|
||||||
|
</button>
|
||||||
|
<div class="header-user-dropdown" id="header-user-dropdown" role="menu">
|
||||||
|
<div class="header-dropdown-row">
|
||||||
|
<span class="header-dropdown-label">Organisation</span>
|
||||||
|
<span class="header-dropdown-value" id="header-org-name">-</span>
|
||||||
|
</div>
|
||||||
|
<div class="header-dropdown-row">
|
||||||
|
<span class="header-dropdown-label">Lizenz</span>
|
||||||
|
<span class="header-dropdown-value" id="header-license-info">-</span>
|
||||||
|
</div>
|
||||||
|
<div id="credits-section" class="credits-section" style="display: none;">
|
||||||
|
<div class="credits-divider"></div>
|
||||||
|
<div class="credits-label">Credits</div>
|
||||||
|
<div class="credits-bar-container">
|
||||||
|
<div id="credits-bar" class="credits-bar"></div>
|
||||||
|
</div>
|
||||||
|
<div class="credits-info">
|
||||||
|
<span><span id="credits-remaining">0</span> von <span id="credits-total">0</span></span>
|
||||||
|
<span class="credits-percent" id="credits-percent"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="credits-divider"></div>
|
||||||
|
<button class="header-dropdown-action" type="button" onclick="AIDisclaimer && AIDisclaimer.show()">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>
|
||||||
|
<span>Über KI-Inhalte</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-secondary btn-small" id="logout-btn" type="button" onclick="Studio.logout()">Abmelden</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
@@ -62,6 +104,11 @@
|
|||||||
|
|
||||||
<!-- Reiter: Fälle -->
|
<!-- Reiter: Fälle -->
|
||||||
<div class="left-pane" id="pane-cases" hidden>
|
<div class="left-pane" id="pane-cases" hidden>
|
||||||
|
<!-- Gleicher Umfang wie die Seitenleiste im klassischen Dashboard -->
|
||||||
|
<div class="sidebar-filter case-scope">
|
||||||
|
<button class="sidebar-filter-btn active" data-scope="all" onclick="Studio.setCaseScope('all')" aria-pressed="true">Alle</button>
|
||||||
|
<button class="sidebar-filter-btn" data-scope="mine" onclick="Studio.setCaseScope('mine')" aria-pressed="false">Eigene</button>
|
||||||
|
</div>
|
||||||
<div class="src-search">
|
<div class="src-search">
|
||||||
<span class="src-search-icon"><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"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
<span class="src-search-icon"><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"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg></span>
|
||||||
<input type="search" id="case-search-input" placeholder="Fälle durchsuchen …" aria-label="Fälle durchsuchen" oninput="Studio.filterCases(this.value)">
|
<input type="search" id="case-search-input" placeholder="Fälle durchsuchen …" aria-label="Fälle durchsuchen" oninput="Studio.filterCases(this.value)">
|
||||||
@@ -517,6 +564,8 @@
|
|||||||
<script src="/static/js/api.js?v=20260714a"></script>
|
<script src="/static/js/api.js?v=20260714a"></script>
|
||||||
<script src="/static/js/ws.js?v=20260316b"></script>
|
<script src="/static/js/ws.js?v=20260316b"></script>
|
||||||
<script src="/static/js/components.js?v=20260514e"></script>
|
<script src="/static/js/components.js?v=20260514e"></script>
|
||||||
<script src="/static/js/studio.js?v=20260714b"></script>
|
<script src="/static/js/a11y.js?v=20260725a"></script>
|
||||||
|
<script src="/static/js/ai-disclaimer.js?v=20260725a"></script>
|
||||||
|
<script src="/static/js/studio.js?v=20260725a"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren