6 Commits

Autor SHA1 Nachricht Datum
70133188a6 Promote develop → main (2026-07-23 21:28 UTC) 2026-07-23 23:28:34 +02:00
31e885254a Release-Notes: Header bleibt während der Suche bedienbar 2026-07-23 23:10:10 +02:00
db6f847daf Release-Notes: Fix Headerzeilenbedienbarkeit beim ersten Falldurchlauf 2026-07-23 22:59:38 +02:00
claude-dev
e3b4e25429 fix(ui): Header während der ersten Recherche bedienbar halten
Das blockierende Fortschritts-Overlay (.progress-overlay.blocking) liegt
position:fixed über dem gesamten Viewport und schluckt damit auch die
Klicks auf den globalen Header. Beim ersten Durchlauf eines Falls waren so
Barrierefreiheit, Theme-Wechsel, Konto-Menü inkl. Org-Switcher und
Abmelden nicht erreichbar.

Neue Body-Klasse first-refresh-blocking hebt den Header per z-index über
das Overlay, solange dieses blockierend sichtbar ist. UI._syncHeaderAccess()
spiegelt den Overlay-Zustand an allen Stellen, die das Overlay ein- oder
ausblenden.

Die fallbezogenen Aktionen (Aktualisieren, Bearbeiten, ...) bleiben
unverändert gesperrt: dafür sorgen weiterhin #incident-view.refresh-blurred
und .incident-header-actions.first-refresh-locked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:56:00 +00:00
Claude Code
3a3076c4cf Orchestrator: eigener Worker pro Organisation (parallele Lanes)
Ersetzt die globale Warteschlange mit einem einzelnen Worker durch
Per-Tenant-Lanes: jede Organisation bekommt eine eigene Queue und einen
eigenen Worker-Task, die unabhaengig und parallel laufen. Lanes werden
lazy angelegt und bei Leerlauf (60s) wieder beendet.

- Skalarer Zustand (_current_task, _cancel_event) -> Maps pro incident_id
  (_current_tasks, _cancel_events); _queued_ids/_cancel_requested bleiben
  global (IDs sind lane-uebergreifend eindeutig).
- enqueue_refresh/cancel_refresh: Signatur unveraendert, routen intern per
  tenant_id in die passende Lane. Oeffentliche Lagen (tenant_id NULL) in
  fester PUBLIC_LANE.
- Optionales Ventil ORCHESTRATOR_MAX_PARALLEL (Default 0 = unbegrenzt)
  begrenzt bei Bedarf gleichzeitige Recherchen ueber alle Lanes.
- /refreshing-Endpoint auf _current_tasks-Map umgestellt, Response-Format
  unveraendert (pro Tenant laeuft weiterhin hoechstens eine Lage).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-23 20:12:03 +00:00
e0107a1bb1 Merge pull request 'Hotfix: E-Mail-Versand mit Retry (IONOS-Farm-Stoerung)' (#50) from fix/email-retry-live into main 2026-07-10 12:20:21 +02:00
7 geänderte Dateien mit 170 neuen und 68 gelöschten Zeilen

Datei anzeigen

@@ -1,4 +1,18 @@
[ [
{
"version": "2026-07-23T21:10Z",
"date": "2026-07-23",
"title": "Header bleibt während der Suche bedienbar",
"items": [
"Der Header ist jetzt auch während einer laufenden Recherche vollständig bedienbar."
]
},
{
"version": "2026-07-23T20:59Z",
"date": "2026-07-23",
"title": "Fix Headerzeilenbedienbarkeit beim ersten Falldurchlauf",
"items": []
},
{ {
"version": "2026-05-22T19:10Z", "version": "2026-05-22T19:10Z",
"date": "2026-05-22", "date": "2026-05-22",

Datei anzeigen

@@ -2,6 +2,7 @@
import asyncio import asyncio
import json import json
import logging import logging
import os
import re import re
from datetime import datetime from datetime import datetime
from config import TIMEZONE from config import TIMEZONE
@@ -401,48 +402,83 @@ async def _send_email_notifications_for_incident(
class AgentOrchestrator: class AgentOrchestrator:
"""Verwaltet die Claude-Agenten-Queue und koordiniert Recherche-Zyklen.""" """Koordiniert die Recherche-Zyklen: pro Organisation eine unabhaengige,
parallel laufende Worker-Lane."""
# Lane-Schluessel fuer oeffentliche/systemweite Lagen (tenant_id IS NULL).
PUBLIC_LANE = 0
# Sekunden ohne Auftrag, nach denen eine Lane sich beendet und aufraeumt.
IDLE_TIMEOUT = 60
def __init__(self): def __init__(self):
self._queue: asyncio.Queue = asyncio.Queue()
self._running = False self._running = False
self._current_task: Optional[int] = None
# Session-Start des aktuellen Tasks (UTC ISO mit 'Z'). Ueberspannt Multi-Pass
# und Retries innerhalb derselben Queue-Abarbeitung — verhindert, dass der
# Frontend-Timer beim Seiten-Reload auf den Pass/Retry-Start zurueckspringt.
self._current_task_started_at: Optional[str] = None
self._ws_manager = None self._ws_manager = None
self._queued_ids: set[int] = set() # Pro Organisation (tenant_id) eine eigene Queue + ein eigener Worker-Task.
# So arbeiten Organisationen unabhaengig und parallel, statt sich eine
# globale Warteschlange zu teilen. Lanes werden bei Bedarf angelegt und bei
# Leerlauf wieder beendet. Der Lock schuetzt das Anlegen/Beenden gegen
# gleichzeitiges Einreihen.
self._lanes: dict[int, asyncio.Queue] = {}
self._lane_workers: dict[int, asyncio.Task] = {}
self._lanes_lock = asyncio.Lock()
# Globale Zustaende — incident-IDs sind lane-uebergreifend eindeutig.
self._queued_ids: set[int] = set() # eingereiht (in irgendeiner Lane)
# incident_id -> Session-Start (UTC ISO mit 'Z'). Ersetzt den frueheren
# Einzelwert; haelt den Frontend-Timer ueber Multi-Pass/Retry stabil.
self._current_tasks: dict[int, str] = {}
self._cancel_requested: set[int] = set() self._cancel_requested: set[int] = set()
self._cancel_event: asyncio.Event | None = None # incident_id -> Cancel-Event des laufenden Tasks. cancel_refresh laeuft in
# einem anderen async-Kontext als der Worker und kann die ContextVar nicht
# nutzen, daher diese direkte Zuordnung.
self._cancel_events: dict[int, asyncio.Event] = {}
# Optionales Sicherheitsventil gegen Ueberlastung des gemeinsamen Claude-
# Kontos: begrenzt die Zahl GLEICHZEITIG laufender Recherchen ueber alle
# Lanes. Default 0 = unbegrenzt (jede Organisation voellig unabhaengig).
_max = int(os.getenv("ORCHESTRATOR_MAX_PARALLEL", "0") or "0")
self._global_sem: Optional[asyncio.Semaphore] = asyncio.Semaphore(_max) if _max > 0 else None
def set_ws_manager(self, ws_manager): def set_ws_manager(self, ws_manager):
"""WebSocket-Manager setzen für Echtzeit-Updates.""" """WebSocket-Manager setzen für Echtzeit-Updates."""
self._ws_manager = ws_manager self._ws_manager = ws_manager
async def start(self): async def start(self):
"""Queue-Worker starten.""" """Orchestrator aktivieren. Worker-Lanes werden pro Organisation bei der
ersten Anfrage angelegt (lazy)."""
self._running = True self._running = True
asyncio.create_task(self._worker())
logger.info("Agenten-Orchestrator gestartet") logger.info("Agenten-Orchestrator gestartet")
async def stop(self): async def stop(self):
"""Queue-Worker stoppen.""" """Orchestrator stoppen und alle Lane-Worker beenden."""
self._running = False self._running = False
async with self._lanes_lock:
workers = list(self._lane_workers.values())
self._lane_workers.clear()
self._lanes.clear()
for task in workers:
task.cancel()
logger.info("Agenten-Orchestrator gestoppt") logger.info("Agenten-Orchestrator gestoppt")
async def enqueue_refresh(self, incident_id: int, trigger_type: str = "manual", user_id: int = None) -> bool: async def enqueue_refresh(self, incident_id: int, trigger_type: str = "manual", user_id: int = None) -> bool:
"""Refresh-Auftrag in die Queue stellen. Gibt False zurueck wenn bereits in Queue/aktiv.""" """Refresh-Auftrag in die Lane der Organisation stellen. Gibt False zurueck
if incident_id in self._queued_ids or self._current_task == incident_id: wenn die Lage dort bereits wartet oder gerade laeuft."""
if incident_id in self._queued_ids or incident_id in self._current_tasks:
logger.info(f"Refresh fuer Lage {incident_id} uebersprungen: bereits aktiv/in Queue") logger.info(f"Refresh fuer Lage {incident_id} uebersprungen: bereits aktiv/in Queue")
return False return False
visibility, created_by, tenant_id = await self._get_incident_visibility(incident_id) visibility, created_by, tenant_id = await self._get_incident_visibility(incident_id)
lane_key = tenant_id if tenant_id else self.PUBLIC_LANE
async with self._lanes_lock:
queue = self._lanes.get(lane_key)
if queue is None:
queue = asyncio.Queue()
self._lanes[lane_key] = queue
self._lane_workers[lane_key] = asyncio.create_task(self._worker(lane_key, queue))
logger.info(f"Neue Worker-Lane fuer Organisation {lane_key} gestartet")
self._queued_ids.add(incident_id) self._queued_ids.add(incident_id)
await self._queue.put((incident_id, trigger_type, user_id)) queue.put_nowait((incident_id, trigger_type, user_id))
queue_size = self._queue.qsize() queue_size = queue.qsize()
logger.info(f"Refresh fuer Lage {incident_id} eingereiht (Queue: {queue_size}, Trigger: {trigger_type})") logger.info(f"Refresh fuer Lage {incident_id} eingereiht (Lane {lane_key}, Queue: {queue_size}, Trigger: {trigger_type})")
if self._ws_manager: if self._ws_manager:
await self._ws_manager.broadcast_for_incident({ await self._ws_manager.broadcast_for_incident({
@@ -455,11 +491,12 @@ class AgentOrchestrator:
async def cancel_refresh(self, incident_id: int) -> bool: async def cancel_refresh(self, incident_id: int) -> bool:
"""Fordert Abbruch eines laufenden oder wartenden Refreshes an.""" """Fordert Abbruch eines laufenden oder wartenden Refreshes an."""
# Check if it's the currently running task # Laeuft die Lage gerade?
if self._current_task == incident_id: if incident_id in self._current_tasks:
self._cancel_requested.add(incident_id) self._cancel_requested.add(incident_id)
if self._cancel_event: ev = self._cancel_events.get(incident_id)
self._cancel_event.set() if ev:
ev.set()
logger.info(f"Cancel angefordert fuer laufende Lage {incident_id}") logger.info(f"Cancel angefordert fuer laufende Lage {incident_id}")
if self._ws_manager: if self._ws_manager:
try: try:
@@ -473,25 +510,33 @@ class AgentOrchestrator:
}, vis, cb, tid) }, vis, cb, tid)
return True return True
# Check if it's in the queue (not yet started) # Wartet die Lage noch in ihrer Lane?
if incident_id in self._queued_ids: if incident_id in self._queued_ids:
self._queued_ids.discard(incident_id) self._queued_ids.discard(incident_id)
# Remove from asyncio queue (rebuild without this ID) # Betroffene Lane bestimmen und die ID aus deren Queue entfernen.
removed = False
new_items = []
while not self._queue.empty():
try: try:
item = self._queue.get_nowait() _vis, _cb, _tid = await self._get_incident_visibility(incident_id)
except Exception:
_tid = None
lane_key = _tid if _tid else self.PUBLIC_LANE
removed = False
async with self._lanes_lock:
queue = self._lanes.get(lane_key)
if queue is not None:
new_items = []
while not queue.empty():
try:
item = queue.get_nowait()
except Exception:
break
iid = item[0] if isinstance(item, tuple) else item iid = item[0] if isinstance(item, tuple) else item
if iid == incident_id: if iid == incident_id:
removed = True removed = True
self._queue.task_done() queue.task_done()
else: else:
new_items.append(item) new_items.append(item)
except Exception:
break
for item in new_items: for item in new_items:
self._queue.put_nowait(item) queue.put_nowait(item)
logger.info(f"Lage {incident_id} aus Warteschlange entfernt (removed={removed})") logger.info(f"Lage {incident_id} aus Warteschlange entfernt (removed={removed})")
@@ -519,12 +564,22 @@ class AgentOrchestrator:
self._cancel_requested.discard(incident_id) self._cancel_requested.discard(incident_id)
raise asyncio.CancelledError("Vom Nutzer abgebrochen") raise asyncio.CancelledError("Vom Nutzer abgebrochen")
async def _worker(self): async def _worker(self, lane_key: int, queue: asyncio.Queue):
"""Verarbeitet Refresh-Aufträge sequentiell.""" """Verarbeitet die Auftraege EINER Organisation sequentiell. Verschiedene
Organisationen laufen in eigenen Lanes parallel. Bei Leerlauf beendet sich
die Lane selbst und wird beim naechsten Auftrag neu angelegt."""
while self._running: while self._running:
try: try:
item = await asyncio.wait_for(self._queue.get(), timeout=5.0) item = await asyncio.wait_for(queue.get(), timeout=self.IDLE_TIMEOUT)
except asyncio.TimeoutError: except asyncio.TimeoutError:
# Leerlauf: Lane beenden. Unter Lock gegen gleichzeitiges Einreihen,
# damit kein Auftrag in einer verwaisten Queue liegen bleibt.
async with self._lanes_lock:
if queue.empty() and self._lanes.get(lane_key) is queue:
self._lanes.pop(lane_key, None)
self._lane_workers.pop(lane_key, None)
logger.info(f"Worker-Lane fuer Organisation {lane_key} bei Leerlauf beendet")
return
continue continue
if len(item) == 3: if len(item) == 3:
@@ -533,12 +588,12 @@ class AgentOrchestrator:
incident_id, trigger_type = item incident_id, trigger_type = item
user_id = None user_id = None
self._queued_ids.discard(incident_id) self._queued_ids.discard(incident_id)
self._current_task = incident_id
# Session-Start EINMAL setzen — bleibt ueber Multi-Pass/Retry hinweg stabil # Session-Start EINMAL setzen — bleibt ueber Multi-Pass/Retry hinweg stabil
self._current_task_started_at = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ') self._current_tasks[incident_id] = datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')
self._cancel_event = asyncio.Event() cancel_event = asyncio.Event()
_cancel_event_var.set(self._cancel_event) self._cancel_events[incident_id] = cancel_event
logger.info(f"Starte Refresh für Lage {incident_id} (Trigger: {trigger_type})") _cancel_event_var.set(cancel_event)
logger.info(f"Starte Refresh für Lage {incident_id} (Lane {lane_key}, Trigger: {trigger_type})")
RETRY_DELAYS = [0, 120, 300] # Sekunden: sofort, 2min, 5min RETRY_DELAYS = [0, 120, 300] # Sekunden: sofort, 2min, 5min
TRANSIENT_ERRORS = (asyncio.TimeoutError, TimeoutError, ConnectionError, OSError) TRANSIENT_ERRORS = (asyncio.TimeoutError, TimeoutError, ConnectionError, OSError)
@@ -548,6 +603,10 @@ class AgentOrchestrator:
def _is_transient_cli(err: Exception) -> bool: def _is_transient_cli(err: Exception) -> bool:
return isinstance(err, ClaudeCliError) and err.error_type in ("rate_limit", "timeout") return isinstance(err, ClaudeCliError) and err.error_type in ("rate_limit", "timeout")
# Optionales globales Ventil (Default aus): begrenzt gleichzeitige Recherchen.
sem = self._global_sem
if sem is not None:
await sem.acquire()
try: try:
# Research-Lagen: Automatisch 3 Durchläufe nur beim ersten Refresh # Research-Lagen: Automatisch 3 Durchläufe nur beim ersten Refresh
incident_type, has_summary = await self._get_incident_info(incident_id) incident_type, has_summary = await self._get_incident_info(incident_id)
@@ -626,11 +685,13 @@ class AgentOrchestrator:
"data": {"error": str(last_error)}, "data": {"error": str(last_error)},
}, _vis, _cb, _tid) }, _vis, _cb, _tid)
finally: finally:
self._current_task = None if sem is not None:
self._current_task_started_at = None sem.release()
self._cancel_event = None self._current_tasks.pop(incident_id, None)
self._cancel_events.pop(incident_id, None)
self._cancel_requested.discard(incident_id)
_cancel_event_var.set(None) _cancel_event_var.set(None)
self._queue.task_done() queue.task_done()
async def _mark_refresh_cancelled(self, incident_id: int): async def _mark_refresh_cancelled(self, incident_id: int):
"""Markiert den laufenden Refresh-Log-Eintrag als cancelled und schliesst """Markiert den laufenden Refresh-Log-Eintrag als cancelled und schliesst

Datei anzeigen

@@ -165,28 +165,25 @@ async def get_refreshing_incidents(
) )
rows = await cursor.fetchall() rows = await cursor.fetchall()
# Also include queued incidents from orchestrator # Queued- und laufende Lagen aus dem Orchestrator ergaenzen.
from agents.orchestrator import orchestrator from agents.orchestrator import orchestrator
queued_ids = list(orchestrator._queued_ids) if hasattr(orchestrator, '_queued_ids') else [] queued_ids = list(getattr(orchestrator, '_queued_ids', set()))
current_task = orchestrator._current_task if hasattr(orchestrator, '_current_task') else None # incident_id -> stabiler Session-Start (ueber Multi-Pass/Retry hinweg). Ersetzt
# Session-Start des aktuell laufenden Tasks — stabil ueber Multi-Pass/Retry hinweg. # den frueheren Einzelwert, da jetzt mehrere Organisationen parallel laufen.
# Verhindert, dass der Frontend-Timer beim Reload auf den letzten Log-Eintrag current_tasks = getattr(orchestrator, '_current_tasks', {}) or {}
# (pass 2/3 oder retry n) zurueckspringt.
current_started_at = (
orchestrator._current_task_started_at
if hasattr(orchestrator, '_current_task_started_at') else None
)
details = {} details = {}
for row in rows: for row in rows:
iid = row["incident_id"] iid = row["incident_id"]
started_at = ( session_start = current_tasks.get(iid)
current_started_at started_at = session_start if session_start else row["started_at"]
if (iid == current_task and current_started_at)
else row["started_at"]
)
details[str(iid)] = {"started_at": started_at} details[str(iid)] = {"started_at": started_at}
# Pro Organisation laeuft hoechstens eine Lage gleichzeitig; der Endpoint ist
# ohnehin tenant-gefiltert, daher genuegt der erste laufende Treffer.
running_here = [row["incident_id"] for row in rows if row["incident_id"] in current_tasks]
current_task = running_here[0] if running_here else None
return { return {
"refreshing": [row["incident_id"] for row in rows], "refreshing": [row["incident_id"] for row in rows],
"queued": queued_ids, "queued": queued_ids,

Datei anzeigen

@@ -2144,6 +2144,14 @@ a.dev-source-pill:hover {
pointer-events: auto; pointer-events: auto;
background: rgba(0,0,0,0.15); background: rgba(0,0,0,0.15);
} }
/* Der globale Header bleibt auch während der ersten (blockierenden) Recherche
bedienbar. Das Overlay deckt sonst den gesamten Viewport ab und schluckt die
Klicks auf Barrierefreiheit, Theme-Wechsel, Konto-Menü und Abmelden.
Gesperrt bleiben ausschließlich die fallbezogenen Aktionen (Aktualisieren,
Bearbeiten, ...) über #incident-view.refresh-blurred. */
body.first-refresh-blocking .header {
z-index: 9100;
}
.progress-popup { .progress-popup {
pointer-events: auto; pointer-events: auto;
background: var(--bg-primary); background: var(--bg-primary);

Datei anzeigen

@@ -13,7 +13,7 @@
<link rel="stylesheet" href="/static/vendor/leaflet.css"> <link rel="stylesheet" href="/static/vendor/leaflet.css">
<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=20260522c"> <link rel="stylesheet" href="/static/css/style.css?v=20260723a">
<style> <style>
/* Export Modal Radio */ /* Export Modal Radio */
.export-radio { display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:pointer; border-radius:var(--radius-sm); transition:background 0.15s; border:1px solid transparent; margin-bottom:4px; } .export-radio { display:flex; align-items:center; gap:10px; padding:8px 12px; cursor:pointer; border-radius:var(--radius-sm); transition:background 0.15s; border:1px solid transparent; margin-bottom:4px; }
@@ -808,10 +808,10 @@
<script src="/static/js/i18n.js?v=20260513a"></script> <script src="/static/js/i18n.js?v=20260513a"></script>
<script src="/static/js/api.js?v=20260522f"></script> <script src="/static/js/api.js?v=20260522f"></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=20260522d"></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=20260522f"></script> <script src="/static/js/app.js?v=20260723a"></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>

Datei anzeigen

@@ -784,6 +784,7 @@ const App = {
// Hide any popup/mini from previous incident // Hide any popup/mini from previous incident
const prevOverlay = document.getElementById('progress-overlay'); const prevOverlay = document.getElementById('progress-overlay');
if (prevOverlay) prevOverlay.style.display = 'none'; if (prevOverlay) prevOverlay.style.display = 'none';
if (typeof UI !== 'undefined' && UI._syncHeaderAccess) UI._syncHeaderAccess();
const prevMini = document.getElementById('progress-mini'); const prevMini = document.getElementById('progress-mini');
if (prevMini) prevMini.style.display = 'none'; if (prevMini) prevMini.style.display = 'none';
const blurTarget = document.getElementById('incident-view'); const blurTarget = document.getElementById('incident-view');
@@ -2596,6 +2597,7 @@ async handleRefresh() {
// Temporarily hide progress popup so confirm dialog is fully visible // Temporarily hide progress popup so confirm dialog is fully visible
const progressOverlay = document.getElementById('progress-overlay'); const progressOverlay = document.getElementById('progress-overlay');
if (progressOverlay) progressOverlay.style.display = 'none'; if (progressOverlay) progressOverlay.style.display = 'none';
UI._syncHeaderAccess();
const ok = await confirmDialog((typeof T === 'function' ? T('confirm.cancel_running_research', 'Laufende Recherche abbrechen?') : 'Laufende Recherche abbrechen?')); const ok = await confirmDialog((typeof T === 'function' ? T('confirm.cancel_running_research', 'Laufende Recherche abbrechen?') : 'Laufende Recherche abbrechen?'));
@@ -2603,11 +2605,13 @@ async handleRefresh() {
if (!ok) { if (!ok) {
const state = UI._progressState[this.currentIncidentId]; const state = UI._progressState[this.currentIncidentId];
if (state && progressOverlay) progressOverlay.style.display = 'flex'; if (state && progressOverlay) progressOverlay.style.display = 'flex';
UI._syncHeaderAccess();
return; return;
} }
// Show cancelling state in popup // Show cancelling state in popup
if (progressOverlay) progressOverlay.style.display = 'flex'; if (progressOverlay) progressOverlay.style.display = 'flex';
UI._syncHeaderAccess();
const btn = document.getElementById('progress-cancel-btn'); const btn = document.getElementById('progress-cancel-btn');
if (btn) { if (btn) {
btn.textContent = (typeof T === 'function' ? T('action.cancelling', 'Wird abgebrochen...') : 'Wird abgebrochen...'); btn.textContent = (typeof T === 'function' ? T('action.cancelling', 'Wird abgebrochen...') : 'Wird abgebrochen...');

Datei anzeigen

@@ -355,6 +355,20 @@ const UI = {
this._showPopupProgress(status, extra, state); this._showPopupProgress(status, extra, state);
}, },
/**
* Hält den globalen Header bedienbar, solange das blockierende
* Fortschritts-Overlay sichtbar ist. Die Body-Klasse hebt den Header per
* z-index über das Overlay: Barrierefreiheit, Theme, Konto-Menü und
* Abmelden müssen jederzeit erreichbar sein.
*/
_syncHeaderAccess() {
const overlay = document.getElementById('progress-overlay');
const blocking = !!overlay
&& overlay.classList.contains('blocking')
&& overlay.style.display !== 'none';
document.body.classList.toggle('first-refresh-blocking', blocking);
},
_showPopupProgress(status, extra, state) { _showPopupProgress(status, extra, state) {
const overlay = document.getElementById('progress-overlay'); const overlay = document.getElementById('progress-overlay');
const popup = document.getElementById('progress-popup'); const popup = document.getElementById('progress-popup');
@@ -381,6 +395,7 @@ const UI = {
} else { } else {
overlay.classList.remove('blocking'); overlay.classList.remove('blocking');
} }
this._syncHeaderAccess();
// Minimize button: only for updates (not first) // Minimize button: only for updates (not first)
const minBtn = document.getElementById('progress-popup-minimize'); const minBtn = document.getElementById('progress-popup-minimize');
@@ -485,6 +500,7 @@ const UI = {
// Hide popup // Hide popup
const overlay = document.getElementById('progress-overlay'); const overlay = document.getElementById('progress-overlay');
if (overlay) overlay.style.display = 'none'; if (overlay) overlay.style.display = 'none';
this._syncHeaderAccess();
}, },
minimizeProgress(incidentId) { minimizeProgress(incidentId) {
@@ -528,6 +544,7 @@ const UI = {
overlay.style.display = 'flex'; overlay.style.display = 'flex';
overlay.classList.remove('blocking'); overlay.classList.remove('blocking');
} }
this._syncHeaderAccess();
// Mark all steps done // Mark all steps done
document.querySelectorAll('.progress-check-item').forEach(item => { document.querySelectorAll('.progress-check-item').forEach(item => {
@@ -620,6 +637,7 @@ const UI = {
if (incidentId === App.currentIncidentId) { if (incidentId === App.currentIncidentId) {
const overlay = document.getElementById('progress-overlay'); const overlay = document.getElementById('progress-overlay');
if (overlay) { overlay.style.display = 'none'; overlay.classList.remove('blocking'); } if (overlay) { overlay.style.display = 'none'; overlay.classList.remove('blocking'); }
this._syncHeaderAccess();
const mini = document.getElementById('progress-mini'); const mini = document.getElementById('progress-mini');
if (mini) mini.style.display = 'none'; if (mini) mini.style.display = 'none';
} }