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_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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
@@ -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. 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">
|
<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()">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
In neuem Issue referenzieren
Einen Benutzer sperren