feat(eu-umbau): Umschalter fuer den KI-Weg in der Oberflaeche

Im Anlege- und Bearbeiten-Dialog einer Lage laesst sich jetzt waehlen, ob sie
ueber den heutigen Weg (Anthropic) oder den EU-Weg (Bedrock Frankfurt + staan)
laeuft; Standard bleibt die Vorgabe der Organisation. Dazu ai_backend in den
Schemas und den Lagen-Endpunkten (Create, Update, Ausgabe), Leerstring setzt
bewusst auf Standard zurueck. In der Lagen-Kopfzeile zeigt ein Kennzeichen
'EU' an, dass die Verarbeitung europaeisch laeuft.

UI-Sync: bewusst NUR online. Der Lokal-Fork hat kein Bedrock-Backend, dort
waere das Feld funktionslos (wie X-Zugaenge und Studio-Gating).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dieser Commit ist enthalten in:
claude-dev
2026-07-30 21:25:59 +00:00
Ursprung b32c54b22c
Commit 8f5c0e9e5d
5 geänderte Dateien mit 48 neuen und 3 gelöschten Zeilen

Datei anzeigen

@@ -62,6 +62,9 @@ class IncidentCreate(BaseModel):
include_telegram: bool = False include_telegram: bool = False
include_x: bool = False include_x: bool = False
visibility: str = Field(default="public", pattern="^(public|private)$") visibility: str = Field(default="public", pattern="^(public|private)$")
# KI-Weg dieser Lage (EU-Umbau). Leer/None = Vorgabe der Organisation
# bzw. globaler Default. 'cli' = Anthropic, 'bedrock' = EU (Frankfurt + staan).
ai_backend: Optional[str] = Field(default=None, pattern="^(cli|bedrock)$")
class IncidentUpdate(BaseModel): class IncidentUpdate(BaseModel):
@@ -77,6 +80,10 @@ class IncidentUpdate(BaseModel):
include_telegram: Optional[bool] = None include_telegram: Optional[bool] = None
include_x: Optional[bool] = None include_x: Optional[bool] = None
visibility: Optional[str] = Field(default=None, pattern="^(public|private)$") visibility: Optional[str] = Field(default=None, pattern="^(public|private)$")
# KI-Weg. Leerstring "" setzt bewusst auf "Vorgabe der Organisation" zurueck,
# deshalb hier auch der Leerstring erlaubt (exclude_none im Router laesst
# None durchfallen, "" kommt an und wird zu NULL).
ai_backend: Optional[str] = Field(default=None, pattern="^(cli|bedrock|)$")
class DescriptionEnhanceRequest(BaseModel): class DescriptionEnhanceRequest(BaseModel):
@@ -108,6 +115,7 @@ class IncidentResponse(BaseModel):
international_sources: bool = True international_sources: bool = True
include_telegram: bool = False include_telegram: bool = False
include_x: bool = False include_x: bool = False
ai_backend: Optional[str] = None
created_by: int created_by: int
created_by_username: str = "" created_by_username: str = ""
created_at: str created_at: str
@@ -137,6 +145,7 @@ class IncidentListItem(BaseModel):
international_sources: bool = True international_sources: bool = True
include_telegram: bool = False include_telegram: bool = False
include_x: bool = False include_x: bool = False
ai_backend: Optional[str] = None
created_by: int created_by: int
created_by_username: str = "" created_by_username: str = ""
created_at: str created_at: str

Datei anzeigen

@@ -22,6 +22,7 @@ router = APIRouter(prefix="/api/incidents", tags=["incidents"])
INCIDENT_UPDATE_COLUMNS = { INCIDENT_UPDATE_COLUMNS = {
"title", "description", "type", "status", "refresh_mode", "title", "description", "type", "status", "refresh_mode",
"refresh_interval", "refresh_start_time", "retention_days", "international_sources", "include_telegram", "include_x", "visibility", "refresh_interval", "refresh_start_time", "retention_days", "international_sources", "include_telegram", "include_x", "visibility",
"ai_backend",
} }
@@ -89,7 +90,7 @@ async def list_incidents(
query = ( query = (
"SELECT id, title, description, type, status, refresh_mode, refresh_interval, " "SELECT id, title, description, type, status, refresh_mode, refresh_interval, "
"refresh_start_time, retention_days, visibility, " "refresh_start_time, retention_days, visibility, "
"international_sources, include_telegram, include_x, created_by, created_at, updated_at, " "international_sources, include_telegram, include_x, ai_backend, created_by, created_at, updated_at, "
"CASE WHEN summary IS NOT NULL AND summary != '' THEN 1 ELSE 0 END AS has_summary " "CASE WHEN summary IS NOT NULL AND summary != '' THEN 1 ELSE 0 END AS has_summary "
"FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)" "FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)"
) )
@@ -121,8 +122,8 @@ async def create_incident(
cursor = await db.execute( cursor = await db.execute(
"""INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval, """INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval,
refresh_start_time, retention_days, international_sources, include_telegram, include_x, visibility, refresh_start_time, retention_days, international_sources, include_telegram, include_x, visibility,
tenant_id, created_by, created_at, updated_at) ai_backend, tenant_id, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
( (
data.title, data.title,
data.description, data.description,
@@ -135,6 +136,7 @@ async def create_incident(
1 if data.include_telegram else 0, 1 if data.include_telegram else 0,
1 if data.include_x else 0, 1 if data.include_x else 0,
data.visibility, data.visibility,
data.ai_backend or None,
tenant_id, tenant_id,
current_user["id"], current_user["id"],
now, now,
@@ -385,6 +387,9 @@ async def update_incident(
continue continue
if field in ("international_sources", "include_telegram", "include_x"): if field in ("international_sources", "include_telegram", "include_x"):
updates[field] = 1 if value else 0 updates[field] = 1 if value else 0
elif field == "ai_backend":
# Leerstring = "Vorgabe der Organisation", wird als NULL gespeichert.
updates[field] = value or None
else: else:
updates[field] = value updates[field] = value

Datei anzeigen

@@ -6180,3 +6180,18 @@ body.tutorial-active .tutorial-cursor {
.pipeline-block.status-active { box-shadow: var(--glow-accent); } .pipeline-block.status-active { box-shadow: var(--glow-accent); }
.pipeline-stage.is-looping .pipeline-loop { animation: none !important; opacity: 1; } .pipeline-stage.is-looping .pipeline-loop { animation: none !important; opacity: 1; }
} }
/* EU-Kennzeichen in der Lagen-Kopfzeile (EU-Umbau) */
.incident-eu-badge {
display: inline-flex;
align-items: center;
padding: 2px 8px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.5px;
background: rgba(16, 185, 129, 0.16);
color: #10B981;
flex-shrink: 0;
}
.incident-eu-badge[hidden] { display: none; }

Datei anzeigen

@@ -164,6 +164,7 @@
<div class="incident-header-row1"> <div class="incident-header-row1">
<div class="incident-header-left"> <div class="incident-header-left">
<h2 class="incident-header-title" id="incident-title"></h2> <h2 class="incident-header-title" id="incident-title"></h2>
<span class="incident-eu-badge" id="incident-eu-badge" hidden>EU</span>
</div> </div>
<div class="incident-header-actions"> <div class="incident-header-actions">
<button class="btn btn-primary btn-small" id="refresh-btn" data-i18n="action.refresh">Aktualisieren</button> <button class="btn btn-primary btn-small" id="refresh-btn" data-i18n="action.refresh">Aktualisieren</button>
@@ -411,6 +412,14 @@
</label> </label>
</div> </div>
</div> </div>
<div class="form-group">
<label for="inc-ai-backend"><span data-i18n="modal.field.ai_backend">KI-Verarbeitung</span> <span class="info-icon tooltip-below" data-tooltip="Steuert, wo die KI-Verarbeitung dieser Lage stattfindet.&#10;&#10;Standard: es gilt die Vorgabe der Organisation.&#10;Anthropic: heutiger Weg mit eingebauter Websuche.&#10;EU: Verarbeitung ueber AWS Bedrock in Frankfurt, Recherche ueber den europaeischen Suchindex staan. Prompts und Suchanfragen verlassen den europaeischen Rechtsraum nicht."><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"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg></span></label>
<select id="inc-ai-backend">
<option value="" data-i18n="modal.option.ai_backend_default">Standard (Vorgabe der Organisation)</option>
<option value="cli" data-i18n="modal.option.ai_backend_cli">Anthropic (heutiger Weg)</option>
<option value="bedrock" data-i18n="modal.option.ai_backend_eu">EU (Frankfurt, DSGVO-Fassung)</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="inc-refresh-mode" data-i18n="modal.field.refresh">Aktualisierung</label> <label for="inc-refresh-mode" data-i18n="modal.field.refresh">Aktualisierung</label>
<select id="inc-refresh-mode" onchange="toggleRefreshInterval()"> <select id="inc-refresh-mode" onchange="toggleRefreshInterval()">

Datei anzeigen

@@ -926,6 +926,11 @@ const App = {
typeBadge.className = 'incident-type-badge ' + (incident.type === 'research' ? 'type-research' : 'type-adhoc'); typeBadge.className = 'incident-type-badge ' + (incident.type === 'research' ? 'type-research' : 'type-adhoc');
typeBadge.textContent = incident.type === 'research' ? 'Analyse' : 'Live'; typeBadge.textContent = incident.type === 'research' ? 'Analyse' : 'Live';
// EU-Kennzeichen: nur bei ausdruecklich auf den EU-Weg gestellten Lagen
{ const _eu = document.getElementById('incident-eu-badge');
if (_eu) { _eu.hidden = incident.ai_backend !== 'bedrock';
_eu.title = 'KI-Verarbeitung in der EU (AWS Bedrock Frankfurt), Recherche ueber den europaeischen Suchindex staan'; } }
// Kachel-Label: 'Recherchebericht' fuer Recherche-Lagen, 'Lagebild' fuer Live-Monitoring // Kachel-Label: 'Recherchebericht' fuer Recherche-Lagen, 'Lagebild' fuer Live-Monitoring
const _tI18n = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb; const _tI18n = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
const _lbLabel = incident.type === 'research' const _lbLabel = incident.type === 'research'
@@ -1726,6 +1731,7 @@ const App = {
include_telegram: document.getElementById('inc-telegram').checked, include_telegram: document.getElementById('inc-telegram').checked,
include_x: document.getElementById('inc-x').checked, include_x: document.getElementById('inc-x').checked,
visibility: document.getElementById('inc-visibility').checked ? 'public' : 'private', visibility: document.getElementById('inc-visibility').checked ? 'public' : 'private',
ai_backend: (document.getElementById('inc-ai-backend') || {}).value || '',
}; };
}, },
@@ -2163,6 +2169,7 @@ async handleRefresh() {
{ const _e = document.getElementById('inc-x'); if (_e) _e.checked = !!incident.include_x; } { const _e = document.getElementById('inc-x'); if (_e) _e.checked = !!incident.include_x; }
{ const _e = document.getElementById('inc-visibility'); if (_e) _e.checked = incident.visibility !== 'private'; } { const _e = document.getElementById('inc-visibility'); if (_e) _e.checked = incident.visibility !== 'private'; }
{ const _e = document.getElementById('inc-ai-backend'); if (_e) _e.value = incident.ai_backend || ''; }
updateVisibilityHint(); updateVisibilityHint();
updateSourcesHint(); updateSourcesHint();
toggleTypeDefaults(true); toggleTypeDefaults(true);