Commits vergleichen

9 Commits

Autor SHA1 Nachricht Datum
Claude Code
f73c21235e feat(translator): Feature-Flag TRANSLATOR_ENABLED zum Abschalten (siehe main) 2026-05-03 20:43:40 +00:00
Claude Code
9078489d0a fix(orchestrator): Auto-Refresh nicht direkt nach Cancel/Error neu einreihen
- main.py: Auto-Refresh-Filter beruecksichtigt jetzt auch cancelled und error
- orchestrator.py: Queue-Cancels schreiben jetzt einen cancelled-Eintrag ins
  refresh_log via _log_queued_cancellation

Wirkung: Nach Cancel oder Error startet die Lage erst beim naechsten
regulaeren Slot wieder. refresh_mode bleibt unveraendert.

(Identisch zu Commit auf main, develop nachgezogen.)
2026-05-03 19:30:04 +00:00
24d7500152 Release-Notes: Übersichtlichere Navigation in der Seitenleiste 2026-05-03 17:21:37 +02:00
Claude Code
f0fe35b279 Sidebar Feedback-Button: mail-Icon (Brief) statt message-square 2026-05-03 15:14:59 +00:00
Claude Code
fb6e9fff19 Sidebar: Quellen+Feedback-Buttons mit Lucide-Icons + kuerzerem Text
Quellen verwalten -> Quellen (mit database-Icon)
Feedback senden  -> Feedback (mit message-square-Icon)
Tooltip behaelt den vollen Text fuer Mouseover.
2026-05-03 15:14:05 +00:00
Claude Code
b1a0e97a34 Pipeline: bei Lagen-Wechsel auf bereits-queued Lage automatisch beginQueue
Wenn der User in der Sidebar auf eine Lage klickt, die schon in Queue
wartet, ruft bindToIncident() die API auf und kriegt den letzten
gespeicherten Pipeline-Stand (alles done = gruen). Das ist falsch fuer
queued-Status.

Fix: nach API-Load pruefen, ob die Lage in App._refreshingIncidents ist
UND in UI._progressState mit step=queued -> beginQueue() selbst ausloesen.
Damit zeigt die Pipeline grau, sobald man auf die queued-Lage wechselt.
2026-05-03 14:27:20 +00:00
Claude Code
77797f6027 Refresh-Modal: Titel je nach Status (queued/cancelling/laeuft)
Bisher hing der Titel nur an state.isFirst -> stand auch "Aktualisierung
laeuft" wenn die Lage tatsaechlich noch in der Queue wartete.

Jetzt:
- queued    -> "In Warteschlange" (mit Position #N falls vorhanden)
- cancelling -> "Wird abgebrochen…"
- isFirst   -> "Erste Recherche laeuft"
- sonst     -> "Aktualisierung laeuft"
2026-05-03 14:18:17 +00:00
Claude Code
dc51ecafe8 Pipeline-Snapshot: Mini-Pipeline auch zuruecksetzen
beginQueue() und _restoreSnapshot() haben bisher nur _render() aufgerufen,
aber NICHT _renderMini(). Daher blieben die kleinen Pipeline-Icons im
"Aktualisierung laeuft"-Modal gruen, obwohl die Lage in Queue war.
Fix: an beiden Stellen auch _renderMini() aufrufen.
2026-05-03 14:15:27 +00:00
Claude Code
31fa17465a Pipeline-Icons: Snapshot/Restore bei Queue + Cancel
Vorher:
- Lage refreshen -> Lage geht in Queue, aber Pipeline-Icons bleiben gruen
  mit Haekchen vom letzten Refresh (suggeriert faelschlich "alles fertig")
- Cancel/Error -> Pipeline bleibt im Mix-Zustand (teils active, teils pending)

Nachher:
- pipeline.beginQueue(id): macht Snapshot des aktuellen _stateByKey und
  setzt alle Steps auf pending. Ausgeloest aus app.js handleRefresh()
  und _restoreRefreshingState() (auch nach F5).
- _onRefreshDoneSuccess: Snapshot verwerfen + API-Reload (wie bisher).
- _onRefreshDoneCancel: Snapshot zurueckspielen -> vorheriger gruener
  Stand sichtbar.
- _onRefreshDoneError: gleiches Verhalten wie Cancel.
- bindToIncident: Snapshot mitloeschen (lagen-spezifisch).
- Bei zweitem Refresh ohne Cancel dazwischen wird Snapshot bewusst
  ueberschrieben.
2026-05-03 14:10:56 +00:00
9 geänderte Dateien mit 152 neuen und 11 gelöschten Zeilen

Datei anzeigen

@@ -1,4 +1,13 @@
[
{
"version": "2026-05-03T15:21Z",
"date": "2026-05-03",
"title": "Übersichtlichere Navigation in der Seitenleiste",
"items": [
"Schaltflächen in der Seitenleiste haben jetzt klarere Icons und kürzere Beschriftungen",
"Der Feedback-Button zeigt nun ein Brief-Symbol für bessere Erkennbarkeit"
]
},
{
"version": "2026-04-30T23:12Z",
"date": "2026-04-30",

Datei anzeigen

@@ -489,6 +489,9 @@ class AgentOrchestrator:
logger.info(f"Lage {incident_id} aus Warteschlange entfernt (removed={removed})")
# refresh_log-Eintrag schreiben, damit Auto-Refresh nicht im naechsten Tick erneut einreiht
await self._log_queued_cancellation(incident_id)
# Send cancelled event
if self._ws_manager:
try:
@@ -639,6 +642,28 @@ class AgentOrchestrator:
finally:
await db.close()
async def _log_queued_cancellation(self, incident_id: int):
"""Schreibt einen cancelled-Eintrag fuer einen Queue-Abbruch (Lage war noch nicht laufend).
Verhindert, dass der Auto-Refresh-Scheduler im naechsten Tick sofort wieder einreiht."""
from database import get_db
db = await get_db()
try:
cur = await db.execute("SELECT tenant_id FROM incidents WHERE id = ?", (incident_id,))
row = await cur.fetchone()
tid = row["tenant_id"] if row else None
now_str = datetime.now(TIMEZONE).strftime("%Y-%m-%d %H:%M:%S")
await db.execute(
"""INSERT INTO refresh_log (incident_id, started_at, completed_at, status,
trigger_type, error_message, tenant_id)
VALUES (?, ?, ?, 'cancelled', 'manual', 'Aus Warteschlange entfernt', ?)""",
(incident_id, now_str, now_str, tid),
)
await db.commit()
except Exception as e:
logger.warning(f"Konnte Queue-Cancel nicht in refresh_log loggen: {e}")
finally:
await db.close()
async def _mark_refresh_failed(self, incident_id: int, error: str):
"""Markiert den laufenden Refresh-Log-Eintrag als error."""
from database import get_db

Datei anzeigen

@@ -12,7 +12,7 @@ import logging
import re
from agents.claude_client import call_claude, ClaudeUsage, UsageAccumulator
from config import CLAUDE_MODEL_FAST
from config import CLAUDE_MODEL_FAST, TRANSLATOR_ENABLED
logger = logging.getLogger("osint.translator")
@@ -230,6 +230,13 @@ async def translate_articles(
if not articles:
return []
if not TRANSLATOR_ENABLED:
logger.info(
"Translator deaktiviert (TRANSLATOR_ENABLED=false), %d Artikel uebersprungen",
len(articles),
)
return []
all_translations = []
for i in range(0, len(articles), batch_size):
batch = articles[i : i + batch_size]

Datei anzeigen

@@ -41,6 +41,10 @@ OUTPUT_LANGUAGE = "Deutsch"
# In Kundenversion auf False setzen oder Env-Variable entfernen
DEV_MODE = os.environ.get("DEV_MODE", "true").lower() == "true"
# Feature-Flag: Translator-Agent (Haiku) komplett deaktivieren.
# False = keine Uebersetzungen mehr, fremdsprachige Artikel bleiben unuebersetzt.
TRANSLATOR_ENABLED = os.environ.get("TRANSLATOR_ENABLED", "true").lower() == "true"
# RSS-Feeds (Fallback, primär aus DB geladen)
RSS_FEEDS = {
"deutsch": [

Datei anzeigen

@@ -124,7 +124,7 @@ async def check_auto_refresh():
# Letzten abgeschlossenen oder laufenden Refresh pruefen
cursor = await db.execute(
"SELECT started_at, status FROM refresh_log WHERE incident_id = ? AND status IN ('completed', 'running') ORDER BY id DESC LIMIT 1",
"SELECT started_at, status FROM refresh_log WHERE incident_id = ? AND status IN ('completed', 'running', 'cancelled', 'error') ORDER BY id DESC LIMIT 1",
(incident_id,),
)
last_refresh = await cursor.fetchone()

Datei anzeigen

@@ -123,8 +123,14 @@
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
</div>
<div class="sidebar-sources-link">
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()">Quellen verwalten</button>
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()">Feedback senden</button>
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()" title="Quellen verwalten">
<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"><ellipse cx="12" cy="5" rx="9" ry="3"/><path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5"/><path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3"/></svg>
<span>Quellen</span>
</button>
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()" title="Feedback senden">
<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"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-10 5L2 7"/></svg>
<span>Feedback</span>
</button>
<!-- Tutorial-Einstieg temporaer deaktiviert (Ueberarbeitung) - reaktivieren durch Entfernen der Kommentarzeichen:
<button class="btn btn-secondary btn-full btn-small" onclick="Tutorial.start()" title="Interaktiven Rundgang starten">Rundgang starten</button>
-->

Datei anzeigen

@@ -618,6 +618,10 @@ const App = {
const inc = this.incidents.find(i => i.id === id);
const isFirst = inc && !inc.has_summary;
UI.showProgress('queued', { queue_position: idx + 1 }, id, isFirst);
// Pipeline-Reset auch nach F5: aktive Lage in Queue -> Icons grau
if (id === this.currentIncidentId && typeof Pipeline !== 'undefined' && Pipeline.beginQueue) {
Pipeline.beginQueue(id);
}
});
}
@@ -1926,6 +1930,11 @@ async handleRefresh() {
this._updateRefreshButton(true);
// showProgress called via handleStatusUpdate
const result = await API.refreshIncident(this.currentIncidentId);
// Pipeline auf "pending" setzen, damit alte gruene Haekchen nicht
// faelschlich "schon fertig" suggerieren waehrend die Lage in der Queue steht
if (typeof Pipeline !== 'undefined' && Pipeline.beginQueue) {
Pipeline.beginQueue(this.currentIncidentId);
}
if (result && result.status === 'skipped') {
UI.showToast('Aktualisierung ist in der Warteschlange und wird ausgefuehrt, sobald die aktuelle Recherche abgeschlossen ist.', 'info');
} else {

Datei anzeigen

@@ -354,9 +354,22 @@ const UI = {
const minBtn = document.getElementById('progress-popup-minimize');
if (minBtn) minBtn.style.display = state.isFirst ? 'none' : '';
// Title
// Title - haengt von Status ab (queued = wartet, cancelling = bricht ab, sonst laeuft)
const titleEl = document.getElementById('progress-popup-title');
if (titleEl) titleEl.textContent = state.isFirst ? 'Erste Recherche l\u00e4uft' : 'Aktualisierung l\u00e4uft';
if (titleEl) {
let title;
if (status === 'queued') {
const pos = (state && state._queuePos) ? ' (#' + state._queuePos + ')' : '';
title = 'In Warteschlange' + pos;
} else if (status === 'cancelling') {
title = 'Wird abgebrochen\u2026';
} else if (state.isFirst) {
title = 'Erste Recherche l\u00e4uft';
} else {
title = 'Aktualisierung l\u00e4uft';
}
titleEl.textContent = title;
}
// Multi-pass info
const passEl = document.getElementById('progress-popup-pass');

Datei anzeigen

@@ -19,6 +19,7 @@ const Pipeline = {
_incidentId: null,
_definition: null, // PIPELINE_STEPS vom Backend
_stateByKey: {}, // step_key -> {status, count_value, count_secondary, pass_number}
_snapshotState: null, // deep-copy von _stateByKey vor Refresh-Start (fuer Cancel-Restore)
_isResearch: false,
_passTotal: 1,
_lastRefreshHeader: null,
@@ -42,10 +43,11 @@ const Pipeline = {
if (this._wsBound) return;
if (typeof WS !== 'undefined' && WS.on) {
WS.on('pipeline_step', (msg) => this._onWsStep(msg));
// Bei Refresh-Complete den finalen Stand neu laden, damit Zahlen gefroren sichtbar bleiben
WS.on('refresh_complete', (msg) => this._onRefreshDone(msg));
WS.on('refresh_cancelled', (msg) => this._onRefreshDone(msg));
WS.on('refresh_error', (msg) => this._onRefreshDone(msg));
// Erfolg: API-State neu laden (finaler Stand sichtbar)
WS.on('refresh_complete', (msg) => this._onRefreshDoneSuccess(msg));
// Cancel/Error: vor-Refresh-Snapshot zurueckspielen, damit Pipeline nicht im Mix-Zustand stehen bleibt
WS.on('refresh_cancelled', (msg) => this._onRefreshDoneCancel(msg));
WS.on('refresh_error', (msg) => this._onRefreshDoneError(msg));
this._wsBound = true;
}
// Hover-Tooltip-Element vorbereiten
@@ -68,6 +70,7 @@ const Pipeline = {
async bindToIncident(incidentId) {
this._incidentId = incidentId;
this._stateByKey = {};
this._snapshotState = null; // Snapshot ist immer lagen-spezifisch
this._isResearch = false;
this._passTotal = 1;
this._lastRefreshHeader = null;
@@ -101,6 +104,20 @@ const Pipeline = {
this._render();
this._renderMini();
// Edge-Case: Lage ist gerade in Queue (z.B. via Lagen-Wechsel beim
// Klick in der Sidebar). API liefert den LETZTEN gespeicherten Stand
// (alles done = gruen), aber tatsaechlich wartet ein neuer Refresh.
// -> beginQueue() selbst ausloesen, damit Icons grau zeigen.
try {
if (typeof App !== 'undefined' && App._refreshingIncidents
&& App._refreshingIncidents.has(incidentId)
&& typeof UI !== 'undefined' && UI._progressState
&& UI._progressState[incidentId]
&& UI._progressState[incidentId].step === 'queued') {
this.beginQueue(incidentId);
}
} catch (e) { /* tolerant */ }
} catch (e) {
console.warn('Pipeline laden fehlgeschlagen:', e);
this._renderEmpty('Pipeline-Daten konnten nicht geladen werden.');
@@ -166,14 +183,65 @@ const Pipeline = {
}
},
_onRefreshDone(msg) {
/**
* Wird vom Frontend gerufen, wenn ein Refresh angestossen wurde (queued).
* Macht einen Snapshot des aktuellen Pipeline-Stands (zur spaeteren Wiederherstellung
* bei Cancel/Error) und setzt dann alle Steps auf "pending" - damit der User sieht:
* "neuer Refresh laeuft an, alte gruene Haekchen sind nicht mehr aktuell".
*/
beginQueue(incidentId) {
if (this._incidentId !== incidentId) return; // andere Lage offen
if (!this._definition) return; // noch keine Pipeline-Definition geladen
// Aktuellen Stand sichern (deep-copy). Bei Mehrfach-Refresh ohne Cancel
// dazwischen wird der Snapshot bewusst ueberschrieben - er soll immer
// der "Stand kurz vor diesem Refresh" sein.
this._snapshotState = JSON.parse(JSON.stringify(this._stateByKey));
// Alle Steps auf pending setzen
this._definition.forEach(s => {
if (this._stateByKey[s.key]) {
this._stateByKey[s.key].status = 'pending';
} else {
this._stateByKey[s.key] = { status: 'pending', count_value: null, count_secondary: null, pass_number: 1 };
}
});
this._render();
this._renderMini();
},
/** Restauriert den letzten Snapshot. Rueckgabe: true bei Erfolg, false wenn keiner da war. */
_restoreSnapshot() {
if (!this._snapshotState) return false;
this._stateByKey = this._snapshotState;
this._snapshotState = null;
this._render();
this._renderMini();
return true;
},
_onRefreshDoneSuccess(msg) {
if (this._incidentId == null || (msg && msg.incident_id !== this._incidentId)) return;
this._snapshotState = null; // verworfen, neuer Stand wird vom API geladen
// Daten frisch nachladen, damit Header (Dauer) und finale Zahlen passen
setTimeout(() => {
if (this._incidentId != null) this.bindToIncident(this._incidentId);
}, 600);
},
_onRefreshDoneCancel(msg) {
if (this._incidentId == null || (msg && msg.incident_id !== this._incidentId)) return;
if (!this._restoreSnapshot()) {
// Kein Snapshot vorhanden (z.B. Page-Reload mitten im Refresh) -> wie bisher API-Reload
setTimeout(() => {
if (this._incidentId != null) this.bindToIncident(this._incidentId);
}, 600);
}
},
_onRefreshDoneError(msg) {
// Wie Cancel: vorheriger Stand zurueck (nicht im Mix-Zustand stehenbleiben)
this._onRefreshDoneCancel(msg);
},
/** Vollbild-Pipeline (Tab "Analysepipeline") als 3x3-Snake rendern. */
_render() {
const stage = document.getElementById('pipeline-stage');