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_x: bool = False
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):
@@ -77,6 +80,10 @@ class IncidentUpdate(BaseModel):
include_telegram: Optional[bool] = None
include_x: Optional[bool] = None
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):
@@ -108,6 +115,7 @@ class IncidentResponse(BaseModel):
international_sources: bool = True
include_telegram: bool = False
include_x: bool = False
ai_backend: Optional[str] = None
created_by: int
created_by_username: str = ""
created_at: str
@@ -137,6 +145,7 @@ class IncidentListItem(BaseModel):
international_sources: bool = True
include_telegram: bool = False
include_x: bool = False
ai_backend: Optional[str] = None
created_by: int
created_by_username: str = ""
created_at: str

Datei anzeigen

@@ -22,6 +22,7 @@ router = APIRouter(prefix="/api/incidents", tags=["incidents"])
INCIDENT_UPDATE_COLUMNS = {
"title", "description", "type", "status", "refresh_mode",
"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 = (
"SELECT id, title, description, type, status, refresh_mode, refresh_interval, "
"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 "
"FROM incidents WHERE tenant_id = ? AND (visibility = 'public' OR created_by = ?)"
)
@@ -121,8 +122,8 @@ async def create_incident(
cursor = await db.execute(
"""INSERT INTO incidents (title, description, type, refresh_mode, refresh_interval,
refresh_start_time, retention_days, international_sources, include_telegram, include_x, visibility,
tenant_id, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
ai_backend, tenant_id, created_by, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
data.title,
data.description,
@@ -135,6 +136,7 @@ async def create_incident(
1 if data.include_telegram else 0,
1 if data.include_x else 0,
data.visibility,
data.ai_backend or None,
tenant_id,
current_user["id"],
now,
@@ -385,6 +387,9 @@ async def update_incident(
continue
if field in ("international_sources", "include_telegram", "include_x"):
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:
updates[field] = value

Datei anzeigen

@@ -6180,3 +6180,18 @@ body.tutorial-active .tutorial-cursor {
.pipeline-block.status-active { box-shadow: var(--glow-accent); }
.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-left">
<h2 class="incident-header-title" id="incident-title"></h2>
<span class="incident-eu-badge" id="incident-eu-badge" hidden>EU</span>
</div>
<div class="incident-header-actions">
<button class="btn btn-primary btn-small" id="refresh-btn" data-i18n="action.refresh">Aktualisieren</button>
@@ -411,6 +412,14 @@
</label>
</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">
<label for="inc-refresh-mode" data-i18n="modal.field.refresh">Aktualisierung</label>
<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.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
const _tI18n = (k, fb) => (typeof T === 'function') ? T(k, fb) : fb;
const _lbLabel = incident.type === 'research'
@@ -1726,6 +1731,7 @@ const App = {
include_telegram: document.getElementById('inc-telegram').checked,
include_x: document.getElementById('inc-x').checked,
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-visibility'); if (_e) _e.checked = incident.visibility !== 'private'; }
{ const _e = document.getElementById('inc-ai-backend'); if (_e) _e.value = incident.ai_backend || ''; }
updateVisibilityHint();
updateSourcesHint();
toggleTypeDefaults(true);