Commits vergleichen
17 Commits
develop
...
eaffd70575
| Autor | SHA1 | Datum | |
|---|---|---|---|
| eaffd70575 | |||
| 46864c5457 | |||
| 1f4d7b1837 | |||
| d7711711aa | |||
|
|
74d76d2e50 | ||
| 0775a475a4 | |||
| 8b8e31e3cd | |||
| 4a2d85d3b8 | |||
| d24205841f | |||
| b9985b8e35 | |||
| b3bc96c580 | |||
| dc75b89618 | |||
| 44de6616f1 | |||
| bfa4d5fd78 | |||
| c57ac6c6d8 | |||
| d9e5733cfb | |||
| 9574308c29 |
@@ -1,13 +1,4 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"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",
|
"version": "2026-04-30T23:12Z",
|
||||||
"date": "2026-04-30",
|
"date": "2026-04-30",
|
||||||
|
|||||||
@@ -489,9 +489,6 @@ class AgentOrchestrator:
|
|||||||
|
|
||||||
logger.info(f"Lage {incident_id} aus Warteschlange entfernt (removed={removed})")
|
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
|
# Send cancelled event
|
||||||
if self._ws_manager:
|
if self._ws_manager:
|
||||||
try:
|
try:
|
||||||
@@ -642,28 +639,6 @@ class AgentOrchestrator:
|
|||||||
finally:
|
finally:
|
||||||
await db.close()
|
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):
|
async def _mark_refresh_failed(self, incident_id: int, error: str):
|
||||||
"""Markiert den laufenden Refresh-Log-Eintrag als error."""
|
"""Markiert den laufenden Refresh-Log-Eintrag als error."""
|
||||||
from database import get_db
|
from database import get_db
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import logging
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from agents.claude_client import call_claude, ClaudeUsage, UsageAccumulator
|
from agents.claude_client import call_claude, ClaudeUsage, UsageAccumulator
|
||||||
from config import CLAUDE_MODEL_FAST, TRANSLATOR_ENABLED
|
from config import CLAUDE_MODEL_FAST
|
||||||
|
|
||||||
logger = logging.getLogger("osint.translator")
|
logger = logging.getLogger("osint.translator")
|
||||||
|
|
||||||
@@ -230,13 +230,6 @@ async def translate_articles(
|
|||||||
if not articles:
|
if not articles:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
if not TRANSLATOR_ENABLED:
|
|
||||||
logger.info(
|
|
||||||
"Translator deaktiviert (TRANSLATOR_ENABLED=false), %d Artikel uebersprungen",
|
|
||||||
len(articles),
|
|
||||||
)
|
|
||||||
return []
|
|
||||||
|
|
||||||
all_translations = []
|
all_translations = []
|
||||||
for i in range(0, len(articles), batch_size):
|
for i in range(0, len(articles), batch_size):
|
||||||
batch = articles[i : i + batch_size]
|
batch = articles[i : i + batch_size]
|
||||||
|
|||||||
@@ -41,10 +41,6 @@ OUTPUT_LANGUAGE = "Deutsch"
|
|||||||
# In Kundenversion auf False setzen oder Env-Variable entfernen
|
# In Kundenversion auf False setzen oder Env-Variable entfernen
|
||||||
DEV_MODE = os.environ.get("DEV_MODE", "true").lower() == "true"
|
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 (Fallback, primär aus DB geladen)
|
||||||
RSS_FEEDS = {
|
RSS_FEEDS = {
|
||||||
"deutsch": [
|
"deutsch": [
|
||||||
|
|||||||
@@ -124,7 +124,7 @@ async def check_auto_refresh():
|
|||||||
|
|
||||||
# Letzten abgeschlossenen oder laufenden Refresh pruefen
|
# Letzten abgeschlossenen oder laufenden Refresh pruefen
|
||||||
cursor = await db.execute(
|
cursor = await db.execute(
|
||||||
"SELECT started_at, status FROM refresh_log WHERE incident_id = ? AND status IN ('completed', 'running', 'cancelled', 'error') ORDER BY id DESC LIMIT 1",
|
"SELECT started_at, status FROM refresh_log WHERE incident_id = ? AND status IN ('completed', 'running') ORDER BY id DESC LIMIT 1",
|
||||||
(incident_id,),
|
(incident_id,),
|
||||||
)
|
)
|
||||||
last_refresh = await cursor.fetchone()
|
last_refresh = await cursor.fetchone()
|
||||||
|
|||||||
@@ -123,14 +123,8 @@
|
|||||||
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
|
<div id="archived-incidents" aria-live="polite" style="display:none;"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sidebar-sources-link">
|
<div class="sidebar-sources-link">
|
||||||
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()" title="Quellen verwalten">
|
<button class="btn btn-secondary btn-full btn-small" onclick="App.openSourceManagement()">Quellen verwalten</button>
|
||||||
<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>
|
<button class="btn btn-secondary btn-full btn-small sidebar-feedback-btn" onclick="App.openFeedback()">Feedback senden</button>
|
||||||
<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:
|
<!-- 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>
|
<button class="btn btn-secondary btn-full btn-small" onclick="Tutorial.start()" title="Interaktiven Rundgang starten">Rundgang starten</button>
|
||||||
-->
|
-->
|
||||||
|
|||||||
@@ -618,10 +618,6 @@ const App = {
|
|||||||
const inc = this.incidents.find(i => i.id === id);
|
const inc = this.incidents.find(i => i.id === id);
|
||||||
const isFirst = inc && !inc.has_summary;
|
const isFirst = inc && !inc.has_summary;
|
||||||
UI.showProgress('queued', { queue_position: idx + 1 }, id, isFirst);
|
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);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1930,11 +1926,6 @@ async handleRefresh() {
|
|||||||
this._updateRefreshButton(true);
|
this._updateRefreshButton(true);
|
||||||
// showProgress called via handleStatusUpdate
|
// showProgress called via handleStatusUpdate
|
||||||
const result = await API.refreshIncident(this.currentIncidentId);
|
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') {
|
if (result && result.status === 'skipped') {
|
||||||
UI.showToast('Aktualisierung ist in der Warteschlange und wird ausgefuehrt, sobald die aktuelle Recherche abgeschlossen ist.', 'info');
|
UI.showToast('Aktualisierung ist in der Warteschlange und wird ausgefuehrt, sobald die aktuelle Recherche abgeschlossen ist.', 'info');
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -354,22 +354,9 @@ const UI = {
|
|||||||
const minBtn = document.getElementById('progress-popup-minimize');
|
const minBtn = document.getElementById('progress-popup-minimize');
|
||||||
if (minBtn) minBtn.style.display = state.isFirst ? 'none' : '';
|
if (minBtn) minBtn.style.display = state.isFirst ? 'none' : '';
|
||||||
|
|
||||||
// Title - haengt von Status ab (queued = wartet, cancelling = bricht ab, sonst laeuft)
|
// Title
|
||||||
const titleEl = document.getElementById('progress-popup-title');
|
const titleEl = document.getElementById('progress-popup-title');
|
||||||
if (titleEl) {
|
if (titleEl) titleEl.textContent = state.isFirst ? 'Erste Recherche l\u00e4uft' : 'Aktualisierung l\u00e4uft';
|
||||||
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
|
// Multi-pass info
|
||||||
const passEl = document.getElementById('progress-popup-pass');
|
const passEl = document.getElementById('progress-popup-pass');
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ const Pipeline = {
|
|||||||
_incidentId: null,
|
_incidentId: null,
|
||||||
_definition: null, // PIPELINE_STEPS vom Backend
|
_definition: null, // PIPELINE_STEPS vom Backend
|
||||||
_stateByKey: {}, // step_key -> {status, count_value, count_secondary, pass_number}
|
_stateByKey: {}, // step_key -> {status, count_value, count_secondary, pass_number}
|
||||||
_snapshotState: null, // deep-copy von _stateByKey vor Refresh-Start (fuer Cancel-Restore)
|
|
||||||
_isResearch: false,
|
_isResearch: false,
|
||||||
_passTotal: 1,
|
_passTotal: 1,
|
||||||
_lastRefreshHeader: null,
|
_lastRefreshHeader: null,
|
||||||
@@ -43,11 +42,10 @@ const Pipeline = {
|
|||||||
if (this._wsBound) return;
|
if (this._wsBound) return;
|
||||||
if (typeof WS !== 'undefined' && WS.on) {
|
if (typeof WS !== 'undefined' && WS.on) {
|
||||||
WS.on('pipeline_step', (msg) => this._onWsStep(msg));
|
WS.on('pipeline_step', (msg) => this._onWsStep(msg));
|
||||||
// Erfolg: API-State neu laden (finaler Stand sichtbar)
|
// Bei Refresh-Complete den finalen Stand neu laden, damit Zahlen gefroren sichtbar bleiben
|
||||||
WS.on('refresh_complete', (msg) => this._onRefreshDoneSuccess(msg));
|
WS.on('refresh_complete', (msg) => this._onRefreshDone(msg));
|
||||||
// Cancel/Error: vor-Refresh-Snapshot zurueckspielen, damit Pipeline nicht im Mix-Zustand stehen bleibt
|
WS.on('refresh_cancelled', (msg) => this._onRefreshDone(msg));
|
||||||
WS.on('refresh_cancelled', (msg) => this._onRefreshDoneCancel(msg));
|
WS.on('refresh_error', (msg) => this._onRefreshDone(msg));
|
||||||
WS.on('refresh_error', (msg) => this._onRefreshDoneError(msg));
|
|
||||||
this._wsBound = true;
|
this._wsBound = true;
|
||||||
}
|
}
|
||||||
// Hover-Tooltip-Element vorbereiten
|
// Hover-Tooltip-Element vorbereiten
|
||||||
@@ -70,7 +68,6 @@ const Pipeline = {
|
|||||||
async bindToIncident(incidentId) {
|
async bindToIncident(incidentId) {
|
||||||
this._incidentId = incidentId;
|
this._incidentId = incidentId;
|
||||||
this._stateByKey = {};
|
this._stateByKey = {};
|
||||||
this._snapshotState = null; // Snapshot ist immer lagen-spezifisch
|
|
||||||
this._isResearch = false;
|
this._isResearch = false;
|
||||||
this._passTotal = 1;
|
this._passTotal = 1;
|
||||||
this._lastRefreshHeader = null;
|
this._lastRefreshHeader = null;
|
||||||
@@ -104,20 +101,6 @@ const Pipeline = {
|
|||||||
|
|
||||||
this._render();
|
this._render();
|
||||||
this._renderMini();
|
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) {
|
} catch (e) {
|
||||||
console.warn('Pipeline laden fehlgeschlagen:', e);
|
console.warn('Pipeline laden fehlgeschlagen:', e);
|
||||||
this._renderEmpty('Pipeline-Daten konnten nicht geladen werden.');
|
this._renderEmpty('Pipeline-Daten konnten nicht geladen werden.');
|
||||||
@@ -183,65 +166,14 @@ 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;
|
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
|
// Daten frisch nachladen, damit Header (Dauer) und finale Zahlen passen
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (this._incidentId != null) this.bindToIncident(this._incidentId);
|
if (this._incidentId != null) this.bindToIncident(this._incidentId);
|
||||||
}, 600);
|
}, 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. */
|
/** Vollbild-Pipeline (Tab "Analysepipeline") als 3x3-Snake rendern. */
|
||||||
_render() {
|
_render() {
|
||||||
const stage = document.getElementById('pipeline-stage');
|
const stage = document.getElementById('pipeline-stage');
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren