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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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. Standard: es gilt die Vorgabe der Organisation. Anthropic: heutiger Weg mit eingebauter Websuche. 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()">
|
||||
|
||||
@@ -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);
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren