From be9e47caadab09d9cdabeb76b743bfdb5e9746a6 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Fri, 10 Apr 2026 12:33:27 +0200
Subject: [PATCH] phase 2 i18n clean
---
app.py | 19 +
modules/datamodels/datamodelAudit.py | 84 +-
modules/datamodels/datamodelBase.py | 20 +-
modules/datamodels/datamodelBilling.py | 234 ++---
modules/datamodels/datamodelChat.py | 927 ++++--------------
modules/datamodels/datamodelDataSource.py | 89 +-
modules/datamodels/datamodelDocref.py | 73 +-
.../datamodels/datamodelFeatureDataSource.py | 77 +-
modules/datamodels/datamodelFeatures.py | 61 +-
modules/datamodels/datamodelFileFolder.py | 46 +-
modules/datamodels/datamodelFiles.py | 164 ++--
modules/datamodels/datamodelInvitation.py | 68 +-
modules/datamodels/datamodelKnowledge.py | 322 +++---
modules/datamodels/datamodelMembership.py | 82 +-
modules/datamodels/datamodelMessaging.py | 312 +++---
modules/datamodels/datamodelNotification.py | 108 +-
modules/datamodels/datamodelRbac.py | 70 +-
modules/datamodels/datamodelSecurity.py | 152 +--
modules/datamodels/datamodelSubscription.py | 297 ++++--
modules/datamodels/datamodelUam.py | 313 +++---
modules/datamodels/datamodelUiLanguage.py | 29 +-
modules/datamodels/datamodelUtils.py | 110 ++-
modules/datamodels/datamodelWorkflow.py | 358 ++-----
.../datamodels/datamodelWorkflowActions.py | 112 ++-
modules/features/chatbot/mainChatbot.py | 35 +-
.../features/chatbot/routeFeatureChatbot.py | 6 +-
modules/features/commcoach/mainCommcoach.py | 61 +-
.../commcoach/routeFeatureCommcoach.py | 58 +-
.../commcoach/tests/test_mainCommcoach.py | 13 +-
.../datamodelFeatureGraphicalEditor.py | 232 ++---
.../features/graphicalEditor/entryPoints.py | 14 +-
.../graphicalEditor/mainGraphicalEditor.py | 37 +-
.../graphicalEditor/nodeDefinitions/ai.py | 46 +-
.../nodeDefinitions/clickup.py | 100 +-
.../graphicalEditor/nodeDefinitions/data.py | 18 +-
.../graphicalEditor/nodeDefinitions/email.py | 54 +-
.../graphicalEditor/nodeDefinitions/file.py | 18 +-
.../graphicalEditor/nodeDefinitions/flow.py | 28 +-
.../graphicalEditor/nodeDefinitions/input.py | 66 +-
.../nodeDefinitions/sharepoint.py | 54 +-
.../nodeDefinitions/triggers.py | 28 +-
.../nodeDefinitions/trustee.py | 56 +-
.../features/graphicalEditor/nodeRegistry.py | 20 +-
modules/features/graphicalEditor/portTypes.py | 78 +-
.../routeFeatureGraphicalEditor.py | 78 +-
.../datamodelFeatureNeutralizer.py | 195 ++--
.../neutralization/mainNeutralization.py | 39 +-
.../neutralization/routeFeatureNeutralizer.py | 20 +-
.../mainServiceNeutralization.py | 2 +-
.../serviceNeutralization/subProcessList.py | 12 +-
.../realEstate/datamodelFeatureRealEstate.py | 54 +-
modules/features/realEstate/mainRealEstate.py | 35 +-
.../realEstate/routeFeatureRealEstate.py | 60 +-
modules/features/teamsbot/mainTeamsbot.py | 47 +-
.../features/teamsbot/routeFeatureTeamsbot.py | 50 +-
.../trustee/datamodelFeatureTrustee.py | 479 +++------
modules/features/trustee/mainTrustee.py | 390 ++++++--
.../features/trustee/routeFeatureTrustee.py | 124 ++-
.../workspace/datamodelFeatureWorkspace.py | 45 +-
modules/features/workspace/mainWorkspace.py | 49 +-
.../workspace/routeFeatureWorkspace.py | 22 +-
modules/interfaces/interfaceBootstrap.py | 15 +-
modules/interfaces/interfaceFeatures.py | 74 +-
modules/routes/routeAdmin.py | 12 +-
modules/routes/routeAdminFeatures.py | 56 +-
modules/routes/routeAdminRbacExport.py | 19 +-
modules/routes/routeAdminRbacRules.py | 52 +-
.../routes/routeAdminUserAccessOverview.py | 14 +-
modules/routes/routeAttributes.py | 7 +-
modules/routes/routeBilling.py | 53 +-
modules/routes/routeClickup.py | 8 +-
modules/routes/routeDataConnections.py | 10 +-
modules/routes/routeDataFiles.py | 22 +-
modules/routes/routeDataMandates.py | 30 +-
modules/routes/routeDataPrompts.py | 6 +-
modules/routes/routeDataSources.py | 4 +-
modules/routes/routeDataUsers.py | 38 +-
modules/routes/routeGdpr.py | 6 +-
modules/routes/routeI18n.py | 237 +++--
modules/routes/routeInvitations.py | 34 +-
modules/routes/routeMessaging.py | 14 +-
modules/routes/routeNotifications.py | 32 +-
modules/routes/routeRealEstate.py | 54 +-
modules/routes/routeRealEstateScraping.py | 26 +-
modules/routes/routeSecurityAdmin.py | 34 +-
modules/routes/routeSecurityClickup.py | 10 +-
modules/routes/routeSecurityGoogle.py | 32 +-
modules/routes/routeSecurityLocal.py | 44 +-
modules/routes/routeSecurityMsft.py | 34 +-
modules/routes/routeSharepoint.py | 10 +-
modules/routes/routeStore.py | 12 +-
modules/routes/routeSubscription.py | 24 +-
modules/routes/routeSystem.py | 106 +-
modules/routes/routeVoiceGoogle.py | 8 +-
modules/routes/routeVoiceUser.py | 6 +-
modules/routes/routeWorkflowDashboard.py | 9 +-
modules/security/rbacCatalog.py | 6 +-
modules/serviceCenter/registry.py | 28 +-
.../services/serviceAi/subStructureFilling.py | 45 +-
modules/shared/attributeUtils.py | 79 +-
modules/shared/frontendTypes.py | 35 +
modules/shared/i18nRegistry.py | 666 +++++++++++++
modules/shared/jsonContinuation.py | 100 +-
modules/system/mainSystem.py | 118 +--
.../workflows/automation2/executionEngine.py | 78 ++
.../processing/modes/modeAutomation.py | 14 +-
.../workflows/processing/modes/modeBase.py | 4 +-
.../workflows/processing/modes/modeDynamic.py | 6 +-
.../workflows/processing/workflowProcessor.py | 4 +-
modules/workflows/workflowManager.py | 3 +-
tests/unit/datamodels/test_workflow_models.py | 2 +-
111 files changed, 4819 insertions(+), 4371 deletions(-)
create mode 100644 modules/shared/i18nRegistry.py
diff --git a/app.py b/app.py
index 4b08dbff..fa92e882 100644
--- a/app.py
+++ b/app.py
@@ -317,6 +317,15 @@ async def lifespan(app: FastAPI):
except Exception as e:
logger.error(f"Feature catalog registration failed: {e}")
+ # Sync gateway i18n registry to DB and load translation cache
+ try:
+ from modules.shared.i18nRegistry import _syncRegistryToDb, _loadCache
+ await _syncRegistryToDb()
+ await _loadCache()
+ logger.info("i18n registry sync + cache load completed")
+ except Exception as e:
+ logger.warning(f"i18n registry sync failed (non-critical): {e}")
+
# Pre-warm service center modules (avoids first-request import latency)
try:
from modules.serviceCenter import preWarm
@@ -481,6 +490,16 @@ from modules.auth import (
ProactiveTokenRefreshMiddleware,
)
+# i18n language detection middleware (sets per-request language from Accept-Language header)
+from modules.shared.i18nRegistry import _setLanguage
+
+@app.middleware("http")
+async def _i18nMiddleware(request: Request, call_next):
+ acceptLang = request.headers.get("Accept-Language", "")
+ lang = acceptLang[:2].lower() if len(acceptLang) >= 2 and acceptLang[:2].isalpha() else "de"
+ _setLanguage(lang)
+ return await call_next(request)
+
app.add_middleware(CSRFMiddleware)
# Token refresh middleware (silent refresh for expired OAuth tokens)
diff --git a/modules/datamodels/datamodelAudit.py b/modules/datamodels/datamodelAudit.py
index 76c9ecfb..f95b213d 100644
--- a/modules/datamodels/datamodelAudit.py
+++ b/modules/datamodels/datamodelAudit.py
@@ -20,7 +20,7 @@ from enum import Enum
import uuid
from modules.shared.timeUtils import getUtcTimestamp
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
class AuditCategory(str, Enum):
@@ -82,6 +82,7 @@ class AuditAction(str, Enum):
CONFIG_CHANGE = "config_change"
+@i18nModel("Audit-Log-Eintrag")
class AuditLogEntry(BaseModel):
"""
Audit log entry for database storage.
@@ -92,117 +93,94 @@ class AuditLogEntry(BaseModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique identifier for the audit entry",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
# Timestamp
timestamp: float = Field(
default_factory=getUtcTimestamp,
description="UTC timestamp when the event occurred",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Zeitstempel", "frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True}
)
-
+
# Actor identification
userId: str = Field(
description="ID of the user who performed the action (or 'system' for system events)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
-
+
username: Optional[str] = Field(
default=None,
description="Username at the time of the event (for historical reference)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Benutzername", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
# Context
mandateId: Optional[str] = Field(
default=None,
description="Mandate context (if applicable)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
featureInstanceId: Optional[str] = Field(
default=None,
description="Feature instance context (if applicable)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
# Event classification
category: str = Field(
description="Event category (access, key, data, security, gdpr, permission, system)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Kategorie", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
-
+
action: str = Field(
description="Specific action performed",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Aktion", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
-
+
# Event details
resourceType: Optional[str] = Field(
default=None,
description="Type of resource affected (e.g., 'User', 'ChatWorkflow', 'TrusteeContract')",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Ressourcentyp", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
resourceId: Optional[str] = Field(
default=None,
description="ID of the affected resource",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Ressourcen-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
details: Optional[str] = Field(
default=None,
description="Additional details about the event",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Details", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
)
-
+
# Request metadata
ipAddress: Optional[str] = Field(
default=None,
description="IP address of the client",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "IP-Adresse", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
userAgent: Optional[str] = Field(
default=None,
description="User agent string from the request",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "User-Agent", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
+
# Outcome
success: bool = Field(
default=True,
description="Whether the action was successful",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Erfolgreich", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
)
-
+
errorMessage: Optional[str] = Field(
default=None,
description="Error message if the action failed",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Fehlermeldung", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
)
-
-# Register labels for internationalization
-registerModelLabels(
- "AuditLogEntry",
- {"en": "Audit Log Entry", "de": "Audit-Log-Eintrag", "fr": "Entrée du journal d'audit"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "timestamp": {"en": "Timestamp", "de": "Zeitstempel", "fr": "Horodatage"},
- "userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
- "username": {"en": "Username", "de": "Benutzername", "fr": "Nom d'utilisateur"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID", "fr": "ID de l'instance"},
- "category": {"en": "Category", "de": "Kategorie", "fr": "Catégorie"},
- "action": {"en": "Action", "de": "Aktion", "fr": "Action"},
- "resourceType": {"en": "Resource Type", "de": "Ressourcentyp", "fr": "Type de ressource"},
- "resourceId": {"en": "Resource ID", "de": "Ressourcen-ID", "fr": "ID de ressource"},
- "details": {"en": "Details", "de": "Details", "fr": "Détails"},
- "ipAddress": {"en": "IP Address", "de": "IP-Adresse", "fr": "Adresse IP"},
- "userAgent": {"en": "User Agent", "de": "User-Agent", "fr": "Agent utilisateur"},
- "success": {"en": "Success", "de": "Erfolgreich", "fr": "Succès"},
- "errorMessage": {"en": "Error Message", "de": "Fehlermeldung", "fr": "Message d'erreur"},
- },
-)
diff --git a/modules/datamodels/datamodelBase.py b/modules/datamodels/datamodelBase.py
index 862f177b..854be75e 100644
--- a/modules/datamodels/datamodelBase.py
+++ b/modules/datamodels/datamodelBase.py
@@ -6,14 +6,17 @@ from typing import Optional
from pydantic import BaseModel, Field
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
+@i18nModel("Basisdatensatz")
class PowerOnModel(BaseModel):
+ """Basis-Datenmodell mit System-Audit-Feldern fuer alle DB-Tabellen."""
sysCreatedAt: Optional[float] = Field(
default=None,
description="Record creation timestamp (UTC, set by system)",
json_schema_extra={
+ "label": "Erstellt am",
"frontend_type": "timestamp",
"frontend_readonly": True,
"frontend_required": False,
@@ -25,6 +28,7 @@ class PowerOnModel(BaseModel):
default=None,
description="User ID who created this record (set by system)",
json_schema_extra={
+ "label": "Erstellt von",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -36,6 +40,7 @@ class PowerOnModel(BaseModel):
default=None,
description="Record last modification timestamp (UTC, set by system)",
json_schema_extra={
+ "label": "Geaendert am",
"frontend_type": "timestamp",
"frontend_readonly": True,
"frontend_required": False,
@@ -47,6 +52,7 @@ class PowerOnModel(BaseModel):
default=None,
description="User ID who last modified this record (set by system)",
json_schema_extra={
+ "label": "Geaendert von",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -54,15 +60,3 @@ class PowerOnModel(BaseModel):
"system": True,
},
)
-
-
-registerModelLabels(
- "PowerOnModel",
- {"en": "Base Record", "de": "Basisdatensatz"},
- {
- "sysCreatedAt": {"en": "Created At", "de": "Erstellt am", "fr": "Cree le"},
- "sysCreatedBy": {"en": "Created By", "de": "Erstellt von", "fr": "Cree par"},
- "sysModifiedAt": {"en": "Modified At", "de": "Geaendert am", "fr": "Modifie le"},
- "sysModifiedBy": {"en": "Modified By", "de": "Geaendert von", "fr": "Modifie par"},
- },
-)
diff --git a/modules/datamodels/datamodelBilling.py b/modules/datamodels/datamodelBilling.py
index ccf1f4a1..fb1a1061 100644
--- a/modules/datamodels/datamodelBilling.py
+++ b/modules/datamodels/datamodelBilling.py
@@ -7,7 +7,7 @@ from enum import Enum
from datetime import date, datetime, timezone
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
# End-customer price for storage above plan-included volume (CHF per GB per month).
@@ -38,203 +38,170 @@ class PeriodTypeEnum(str, Enum):
YEAR = "YEAR"
+@i18nModel("Abrechnungskonto")
class BillingAccount(PowerOnModel):
"""Billing account for mandate or user-mandate combination."""
id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
)
- mandateId: str = Field(..., description="Foreign key to Mandate")
- userId: Optional[str] = Field(None, description="Foreign key to User (None = mandate pool account, set = user audit account)")
- balance: float = Field(default=0.0, description="Current balance in CHF")
- warningThreshold: float = Field(default=0.0, description="Warning threshold in CHF")
- lastWarningAt: Optional[datetime] = Field(None, description="Last warning sent timestamp")
- enabled: bool = Field(default=True, description="Account is active")
-
-
-registerModelLabels(
- "BillingAccount",
- {"en": "Billing Account", "de": "Abrechnungskonto"},
- {
- "id": {"en": "ID", "de": "ID"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
- "userId": {"en": "User ID", "de": "Benutzer-ID"},
- "balance": {"en": "Balance (CHF)", "de": "Guthaben (CHF)"},
- "warningThreshold": {"en": "Warning Threshold (CHF)", "de": "Warnschwelle (CHF)"},
- "lastWarningAt": {"en": "Last Warning", "de": "Letzte Warnung"},
- "enabled": {"en": "Enabled", "de": "Aktiv"},
- },
-)
+ mandateId: str = Field(..., description="Foreign key to Mandate", json_schema_extra={"label": "Mandanten-ID"})
+ userId: Optional[str] = Field(
+ None,
+ description="Foreign key to User (None = mandate pool account, set = user audit account)",
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ balance: float = Field(default=0.0, description="Current balance in CHF", json_schema_extra={"label": "Guthaben (CHF)"})
+ warningThreshold: float = Field(
+ default=0.0,
+ description="Warning threshold in CHF",
+ json_schema_extra={"label": "Warnschwelle (CHF)"},
+ )
+ lastWarningAt: Optional[datetime] = Field(
+ None,
+ description="Last warning sent timestamp",
+ json_schema_extra={"label": "Letzte Warnung"},
+ )
+ enabled: bool = Field(default=True, description="Account is active", json_schema_extra={"label": "Aktiv"})
+@i18nModel("Transaktion")
class BillingTransaction(PowerOnModel):
"""Single billing transaction (credit, debit, adjustment)."""
id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
)
- accountId: str = Field(..., description="Foreign key to BillingAccount")
- transactionType: TransactionTypeEnum = Field(..., description="Transaction type")
- amount: float = Field(..., description="Amount in CHF (always positive)")
- description: str = Field(..., description="Transaction description")
-
+ accountId: str = Field(..., description="Foreign key to BillingAccount", json_schema_extra={"label": "Konto-ID"})
+ transactionType: TransactionTypeEnum = Field(..., description="Transaction type", json_schema_extra={"label": "Typ"})
+ amount: float = Field(..., description="Amount in CHF (always positive)", json_schema_extra={"label": "Betrag (CHF)"})
+ description: str = Field(..., description="Transaction description", json_schema_extra={"label": "Beschreibung"})
+
# Reference to source
- referenceType: Optional[ReferenceTypeEnum] = Field(None, description="Reference type")
- referenceId: Optional[str] = Field(None, description="Reference ID")
-
+ referenceType: Optional[ReferenceTypeEnum] = Field(None, description="Reference type", json_schema_extra={"label": "Referenztyp"})
+ referenceId: Optional[str] = Field(None, description="Reference ID", json_schema_extra={"label": "Referenz-ID"})
+
# Context for workflow transactions
- workflowId: Optional[str] = Field(None, description="Workflow ID (for WORKFLOW transactions)")
- featureInstanceId: Optional[str] = Field(None, description="Feature instance ID")
- featureCode: Optional[str] = Field(None, description="Feature code (e.g., automation)")
- aicoreProvider: Optional[str] = Field(None, description="AICore provider (anthropic, openai, etc.)")
- aicoreModel: Optional[str] = Field(None, description="AICore model name (e.g., claude-4-sonnet, gpt-4o)")
- createdByUserId: Optional[str] = Field(None, description="User who created/caused this transaction")
-
+ workflowId: Optional[str] = Field(None, description="Workflow ID (for WORKFLOW transactions)", json_schema_extra={"label": "Workflow-ID"})
+ featureInstanceId: Optional[str] = Field(None, description="Feature instance ID", json_schema_extra={"label": "Feature-Instanz-ID"})
+ featureCode: Optional[str] = Field(None, description="Feature code (e.g., automation)", json_schema_extra={"label": "Feature-Code"})
+ aicoreProvider: Optional[str] = Field(None, description="AICore provider (anthropic, openai, etc.)", json_schema_extra={"label": "AI-Anbieter"})
+ aicoreModel: Optional[str] = Field(None, description="AICore model name (e.g., claude-4-sonnet, gpt-4o)", json_schema_extra={"label": "AI-Modell"})
+ createdByUserId: Optional[str] = Field(None, description="User who created/caused this transaction", json_schema_extra={"label": "Erstellt von Benutzer"})
+
# AI call metadata (for per-call analytics)
- processingTime: Optional[float] = Field(None, description="Processing time in seconds")
- bytesSent: Optional[int] = Field(None, description="Bytes sent to AI model")
- bytesReceived: Optional[int] = Field(None, description="Bytes received from AI model")
- errorCount: Optional[int] = Field(None, description="Number of errors in this call")
-
-
-registerModelLabels(
- "BillingTransaction",
- {"en": "Billing Transaction", "de": "Transaktion"},
- {
- "id": {"en": "ID", "de": "ID"},
- "accountId": {"en": "Account ID", "de": "Konto-ID"},
- "transactionType": {"en": "Type", "de": "Typ"},
- "amount": {"en": "Amount (CHF)", "de": "Betrag (CHF)"},
- "description": {"en": "Description", "de": "Beschreibung"},
- "referenceType": {"en": "Reference Type", "de": "Referenztyp"},
- "referenceId": {"en": "Reference ID", "de": "Referenz-ID"},
- "workflowId": {"en": "Workflow ID", "de": "Workflow-ID"},
- "featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID"},
- "featureCode": {"en": "Feature Code", "de": "Feature-Code"},
- "aicoreProvider": {"en": "AI Provider", "de": "AI-Anbieter"},
- "aicoreModel": {"en": "AI Model", "de": "AI-Modell"},
- "createdByUserId": {"en": "Created By User", "de": "Erstellt von Benutzer"},
- },
-)
+ processingTime: Optional[float] = Field(None, description="Processing time in seconds", json_schema_extra={"label": "Verarbeitungszeit (s)"})
+ bytesSent: Optional[int] = Field(None, description="Bytes sent to AI model", json_schema_extra={"label": "Gesendete Bytes"})
+ bytesReceived: Optional[int] = Field(None, description="Bytes received from AI model", json_schema_extra={"label": "Empfangene Bytes"})
+ errorCount: Optional[int] = Field(None, description="Number of errors in this call", json_schema_extra={"label": "Fehleranzahl"})
+@i18nModel("Abrechnungseinstellungen")
class BillingSettings(BaseModel):
"""Billing settings per mandate. Only PREPAY_MANDATE model."""
id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ mandateId: str = Field(..., description="Foreign key to Mandate (UNIQUE)", json_schema_extra={"label": "Mandanten-ID"})
+
+ warningThresholdPercent: float = Field(
+ default=10.0,
+ description="Warning threshold as percentage",
+ json_schema_extra={"label": "Warnschwelle (%)"},
)
- mandateId: str = Field(..., description="Foreign key to Mandate (UNIQUE)")
- warningThresholdPercent: float = Field(default=10.0, description="Warning threshold as percentage")
-
# Stripe
- stripeCustomerId: Optional[str] = Field(None, description="Stripe Customer ID (cus_xxx) — one per mandate")
+ stripeCustomerId: Optional[str] = Field(
+ None,
+ description="Stripe Customer ID (cus_xxx) — one per mandate",
+ json_schema_extra={"label": "Stripe-Kunden-ID"},
+ )
# Auto-Recharge for AI budget
- autoRechargeEnabled: bool = Field(default=False, description="Auto-buy AI budget when low")
- rechargeAmountCHF: float = Field(default=10.0, description="Amount per auto-recharge (CHF, prepaid via Stripe)")
- rechargeMaxPerMonth: int = Field(default=3, description="Max auto-recharges per month")
- rechargesThisMonth: int = Field(default=0, description="Counter: auto-recharges used this month")
- monthResetAt: Optional[datetime] = Field(None, description="When rechargesThisMonth was last reset")
+ autoRechargeEnabled: bool = Field(default=False, description="Auto-buy AI budget when low", json_schema_extra={"label": "Auto-Nachladung"})
+ rechargeAmountCHF: float = Field(
+ default=10.0,
+ description="Amount per auto-recharge (CHF, prepaid via Stripe)",
+ json_schema_extra={"label": "Nachladebetrag (CHF)"},
+ )
+ rechargeMaxPerMonth: int = Field(default=3, description="Max auto-recharges per month", json_schema_extra={"label": "Max. Nachladungen/Monat"})
+ rechargesThisMonth: int = Field(default=0, description="Counter: auto-recharges used this month", json_schema_extra={"label": "Nachladungen diesen Monat"})
+ monthResetAt: Optional[datetime] = Field(None, description="When rechargesThisMonth was last reset", json_schema_extra={"label": "Monats-Reset"})
# Notifications
notifyEmails: List[str] = Field(
default_factory=list,
description="Email addresses for billing alerts (pool exhausted, warnings, etc.)",
+ json_schema_extra={"label": "E-Mails fuer Billing-Alerts (Inhaber/Admin)"},
)
- notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached")
+ notifyOnWarning: bool = Field(default=True, description="Send email when warning threshold is reached", json_schema_extra={"label": "Bei Warnung benachrichtigen"})
# Storage overage (high-watermark within subscription period; resets on new period)
storageHighWatermarkMB: float = Field(
- default=0.0, description="Peak indexed data volume MB this billing period"
+ default=0.0,
+ description="Peak indexed data volume MB this billing period",
+ json_schema_extra={"label": "Speicher-Peak (MB)"},
)
storagePeriodStartAt: Optional[datetime] = Field(
- None, description="Subscription billing period start used for storage reset"
+ None,
+ description="Subscription billing period start used for storage reset",
+ json_schema_extra={"label": "Speicher-Periodenbeginn"},
)
storageBilledUpToMB: float = Field(
default=0.0,
description="Overage MB already debited this period (above plan-included volume)",
+ json_schema_extra={"label": "Speicher abgerechneter Überhang (MB)"},
)
-registerModelLabels(
- "BillingSettings",
- {"en": "Billing Settings", "de": "Abrechnungseinstellungen"},
- {
- "id": {"en": "ID", "de": "ID"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
- "warningThresholdPercent": {"en": "Warning Threshold (%)", "de": "Warnschwelle (%)"},
- "stripeCustomerId": {"en": "Stripe Customer ID", "de": "Stripe-Kunden-ID"},
- "autoRechargeEnabled": {"en": "Auto-Recharge", "de": "Auto-Nachladung"},
- "rechargeAmountCHF": {"en": "Recharge Amount (CHF)", "de": "Nachladebetrag (CHF)"},
- "rechargeMaxPerMonth": {"en": "Max Recharges/Month", "de": "Max. Nachladungen/Monat"},
- "notifyEmails": {
- "en": "Billing notification emails (owner / admin)",
- "de": "E-Mails fuer Billing-Alerts (Inhaber/Admin)",
- },
- "notifyOnWarning": {"en": "Notify on Warning", "de": "Bei Warnung benachrichtigen"},
- "storageHighWatermarkMB": {"en": "Storage peak (MB)", "de": "Speicher-Peak (MB)"},
- "storagePeriodStartAt": {"en": "Storage period start", "de": "Speicher-Periodenbeginn"},
- "storageBilledUpToMB": {
- "en": "Storage billed overage (MB)",
- "de": "Speicher abgerechneter Überhang (MB)",
- },
- },
-)
-
-
class StripeWebhookEvent(BaseModel):
"""Stores processed Stripe webhook event IDs for idempotency."""
id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
)
event_id: str = Field(..., description="Stripe event ID (evt_xxx)")
processed_at: datetime = Field(
default_factory=lambda: datetime.now(timezone.utc),
- description="When the event was processed"
+ description="When the event was processed",
)
+@i18nModel("Nutzungsstatistik")
class UsageStatistics(BaseModel):
"""Aggregated usage statistics for quick retrieval."""
id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
)
- accountId: str = Field(..., description="Foreign key to BillingAccount")
- periodType: PeriodTypeEnum = Field(..., description="Period type")
- periodStart: date = Field(..., description="Period start date")
-
+ accountId: str = Field(..., description="Foreign key to BillingAccount", json_schema_extra={"label": "Konto-ID"})
+ periodType: PeriodTypeEnum = Field(..., description="Period type", json_schema_extra={"label": "Periodentyp"})
+ periodStart: date = Field(..., description="Period start date", json_schema_extra={"label": "Periodenbeginn"})
+
# Aggregated values
- totalCostCHF: float = Field(default=0.0, description="Total cost in CHF")
- transactionCount: int = Field(default=0, description="Number of transactions")
-
+ totalCostCHF: float = Field(default=0.0, description="Total cost in CHF", json_schema_extra={"label": "Gesamtkosten (CHF)"})
+ transactionCount: int = Field(default=0, description="Number of transactions", json_schema_extra={"label": "Anzahl Transaktionen"})
+
# Breakdown by provider
costByProvider: Dict[str, float] = Field(
- default_factory=dict,
- description="Cost breakdown by provider (e.g., {'anthropic': 12.50, 'openai': 8.30})"
+ default_factory=dict,
+ description="Cost breakdown by provider (e.g., {'anthropic': 12.50, 'openai': 8.30})",
+ json_schema_extra={"label": "Kosten nach Anbieter"},
)
-
+
# Breakdown by feature
costByFeature: Dict[str, float] = Field(
default_factory=dict,
- description="Cost breakdown by feature (e.g., {'automation': 5.80, 'workspace': 3.20})"
+ description="Cost breakdown by feature (e.g., {'automation': 5.80, 'workspace': 3.20})",
+ json_schema_extra={"label": "Kosten nach Feature"},
)
-registerModelLabels(
- "UsageStatistics",
- {"en": "Usage Statistics", "de": "Nutzungsstatistik"},
- {
- "id": {"en": "ID", "de": "ID"},
- "accountId": {"en": "Account ID", "de": "Konto-ID"},
- "periodType": {"en": "Period Type", "de": "Periodentyp"},
- "periodStart": {"en": "Period Start", "de": "Periodenbeginn"},
- "totalCostCHF": {"en": "Total Cost (CHF)", "de": "Gesamtkosten (CHF)"},
- "transactionCount": {"en": "Transaction Count", "de": "Anzahl Transaktionen"},
- "costByProvider": {"en": "Cost by Provider", "de": "Kosten nach Anbieter"},
- "costByFeature": {"en": "Cost by Feature", "de": "Kosten nach Feature"},
- },
-)
-
-
# ============================================================================
# Response Models for API
# ============================================================================
@@ -277,4 +244,3 @@ class BillingCheckResult(BaseModel):
subscriptionUiPath: Optional[str] = None
userAction: Optional[str] = None
-
diff --git a/modules/datamodels/datamodelChat.py b/modules/datamodels/datamodelChat.py
index 7154e57e..f1dc720b 100644
--- a/modules/datamodels/datamodelChat.py
+++ b/modules/datamodels/datamodelChat.py
@@ -6,282 +6,119 @@ from typing import List, Dict, Any, Optional
from enum import Enum
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.shared.timeUtils import getUtcTimestamp
import uuid
-
+@i18nModel("Chat-Protokoll")
class ChatLog(PowerOnModel):
"""Log entries for chat workflows. User-owned, no mandate context."""
- id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
- )
- workflowId: str = Field(description="Foreign key to workflow")
- message: str = Field(description="Log message")
- type: str = Field(description="Log type (info, warning, error, etc.)")
- timestamp: float = Field(
- default_factory=getUtcTimestamp,
- description="When the log entry was created (UTC timestamp in seconds)",
- )
- status: Optional[str] = Field(None, description="Status of the log entry")
- progress: Optional[float] = Field(
- None, description="Progress indicator (0.0 to 1.0)"
- )
- performance: Optional[Dict[str, Any]] = Field(
- None, description="Performance metrics"
- )
- parentId: Optional[str] = Field(
- None, description="Parent operation ID (operationId of parent operation) for hierarchical display"
- )
- operationId: Optional[str] = Field(
- None, description="Operation ID to group related log entries"
- )
- roundNumber: Optional[int] = Field(None, description="Round number in workflow")
- taskNumber: Optional[int] = Field(None, description="Task number within round")
- actionNumber: Optional[int] = Field(None, description="Action number within task")
-
-
-registerModelLabels(
- "ChatLog",
- {"en": "Chat Log", "fr": "Journal de chat"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
- "message": {"en": "Message", "fr": "Message"},
- "type": {"en": "Type", "fr": "Type"},
- "timestamp": {"en": "Timestamp", "fr": "Horodatage"},
- "status": {"en": "Status", "fr": "Statut"},
- "progress": {"en": "Progress", "fr": "Progression"},
- "performance": {"en": "Performance", "fr": "Performance"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"label": "ID"})
+ workflowId: str = Field(description="Foreign key to workflow", json_schema_extra={"label": "Workflow-ID"})
+ message: str = Field(description="Log message", json_schema_extra={"label": "Nachricht"})
+ type: str = Field(description="Log type (info, warning, error, etc.)", json_schema_extra={"label": "Typ"})
+ timestamp: float = Field(default_factory=getUtcTimestamp,
+ description="When the log entry was created (UTC timestamp in seconds)", json_schema_extra={"label": "Zeitstempel"})
+ status: Optional[str] = Field(None, description="Status of the log entry", json_schema_extra={"label": "Status"})
+ progress: Optional[float] = Field(None, description="Progress indicator (0.0 to 1.0)", json_schema_extra={"label": "Fortschritt"})
+ performance: Optional[Dict[str, Any]] = Field(None, description="Performance metrics", json_schema_extra={"label": "Leistung"})
+ parentId: Optional[str] = Field(None, description="Parent operation ID (operationId of parent operation) for hierarchical display", json_schema_extra={"label": "Übergeordnete ID"})
+ operationId: Optional[str] = Field(None, description="Operation ID to group related log entries", json_schema_extra={"label": "Vorgangs-ID"})
+ roundNumber: Optional[int] = Field(None, description="Round number in workflow", json_schema_extra={"label": "Rundennummer"})
+ taskNumber: Optional[int] = Field(None, description="Task number within round", json_schema_extra={"label": "Aufgabennummer"})
+ actionNumber: Optional[int] = Field(None, description="Action number within task", json_schema_extra={"label": "Aktionsnummer"})
+@i18nModel("Chat-Dokument")
class ChatDocument(PowerOnModel):
"""Documents attached to chat messages. User-owned, no mandate context."""
- id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
- )
- messageId: str = Field(description="Foreign key to message")
- fileId: str = Field(description="Foreign key to file")
- fileName: str = Field(description="Name of the file")
- fileSize: int = Field(description="Size of the file")
- mimeType: str = Field(description="MIME type of the file")
- roundNumber: Optional[int] = Field(None, description="Round number in workflow")
- taskNumber: Optional[int] = Field(None, description="Task number within round")
- actionNumber: Optional[int] = Field(None, description="Action number within task")
- actionId: Optional[str] = Field(
- None, description="ID of the action that created this document"
- )
-
-
-registerModelLabels(
- "ChatDocument",
- {"en": "Chat Document", "fr": "Document de chat"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "messageId": {"en": "Message ID", "fr": "ID du message"},
- "fileId": {"en": "File ID", "fr": "ID du fichier"},
- "fileName": {"en": "File Name", "fr": "Nom du fichier"},
- "fileSize": {"en": "File Size", "fr": "Taille du fichier"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- "roundNumber": {"en": "Round Number", "fr": "Numéro de tour"},
- "taskNumber": {"en": "Task Number", "fr": "Numéro de tâche"},
- "actionNumber": {"en": "Action Number", "fr": "Numéro d'action"},
- "actionId": {"en": "Action ID", "fr": "ID de l'action"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"label": "ID"})
+ messageId: str = Field(description="Foreign key to message", json_schema_extra={"label": "Nachrichten-ID"})
+ fileId: str = Field(description="Foreign key to file", json_schema_extra={"label": "Datei-ID"})
+ fileName: str = Field(description="Name of the file", json_schema_extra={"label": "Dateiname"})
+ fileSize: int = Field(description="Size of the file", json_schema_extra={"label": "Dateigröße"})
+ mimeType: str = Field(description="MIME type of the file", json_schema_extra={"label": "MIME-Typ"})
+ roundNumber: Optional[int] = Field(None, description="Round number in workflow", json_schema_extra={"label": "Rundennummer"})
+ taskNumber: Optional[int] = Field(None, description="Task number within round", json_schema_extra={"label": "Aufgabennummer"})
+ actionNumber: Optional[int] = Field(None, description="Action number within task", json_schema_extra={"label": "Aktionsnummer"})
+ actionId: Optional[str] = Field(None, description="ID of the action that created this document", json_schema_extra={"label": "Aktions-ID"})
+@i18nModel("Inhalts-Metadaten")
class ContentMetadata(BaseModel):
- size: int = Field(description="Content size in bytes")
- pages: Optional[int] = Field(
- None, description="Number of pages for multi-page content"
- )
- error: Optional[str] = Field(None, description="Processing error if any")
- width: Optional[int] = Field(None, description="Width in pixels for images/videos")
- height: Optional[int] = Field(
- None, description="Height in pixels for images/videos"
- )
- colorMode: Optional[str] = Field(None, description="Color mode")
- fps: Optional[float] = Field(None, description="Frames per second for videos")
- durationSec: Optional[float] = Field(
- None, description="Duration in seconds for media"
- )
- mimeType: str = Field(description="MIME type of the content")
- base64Encoded: bool = Field(description="Whether the data is base64 encoded")
-
-
-registerModelLabels(
- "ContentMetadata",
- {"en": "Content Metadata", "fr": "Métadonnées du contenu"},
- {
- "size": {"en": "Size", "fr": "Taille"},
- "pages": {"en": "Pages", "fr": "Pages"},
- "error": {"en": "Error", "fr": "Erreur"},
- "width": {"en": "Width", "fr": "Largeur"},
- "height": {"en": "Height", "fr": "Hauteur"},
- "colorMode": {"en": "Color Mode", "fr": "Mode de couleur"},
- "fps": {"en": "FPS", "fr": "IPS"},
- "durationSec": {"en": "Duration", "fr": "Durée"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- "base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"},
- },
-)
-
+ size: int = Field(description="Content size in bytes", json_schema_extra={"label": "Größe"})
+ pages: Optional[int] = Field(None, description="Number of pages for multi-page content", json_schema_extra={"label": "Seiten"})
+ error: Optional[str] = Field(None, description="Processing error if any", json_schema_extra={"label": "Fehler"})
+ width: Optional[int] = Field(None, description="Width in pixels for images/videos", json_schema_extra={"label": "Breite"})
+ height: Optional[int] = Field(None, description="Height in pixels for images/videos", json_schema_extra={"label": "Höhe"})
+ colorMode: Optional[str] = Field(None, description="Color mode", json_schema_extra={"label": "Farbmodus"})
+ fps: Optional[float] = Field(None, description="Frames per second for videos", json_schema_extra={"label": "FPS"})
+ durationSec: Optional[float] = Field(None, description="Duration in seconds for media", json_schema_extra={"label": "Dauer"})
+ mimeType: str = Field(description="MIME type of the content", json_schema_extra={"label": "MIME-Typ"})
+ base64Encoded: bool = Field(description="Whether the data is base64 encoded", json_schema_extra={"label": "Base64-kodiert"})
+@i18nModel("Inhaltselement")
class ContentItem(BaseModel):
- label: str = Field(description="Content label")
- data: str = Field(description="Extracted text content")
- metadata: ContentMetadata = Field(description="Content metadata")
-
-
-registerModelLabels(
- "ContentItem",
- {"en": "Content Item", "fr": "Élément de contenu"},
- {
- "label": {"en": "Label", "fr": "Étiquette"},
- "data": {"en": "Data", "fr": "Données"},
- "metadata": {"en": "Metadata", "fr": "Métadonnées"},
- },
-)
-
+ label: str = Field(description="Content label", json_schema_extra={"label": "Bezeichnung"})
+ data: str = Field(description="Extracted text content", json_schema_extra={"label": "Daten"})
+ metadata: ContentMetadata = Field(description="Content metadata", json_schema_extra={"label": "Metadaten"})
+@i18nModel("Extrahierter Inhalt")
class ChatContentExtracted(BaseModel):
- id: str = Field(description="Reference to source ChatDocument")
- contents: List[ContentItem] = Field(
- default_factory=list, description="List of content items"
- )
-
-
-registerModelLabels(
- "ChatContentExtracted",
- {"en": "Extracted Content", "fr": "Contenu extrait"},
- {
- "id": {"en": "Object ID", "fr": "ID de l'objet"},
- "contents": {"en": "Contents", "fr": "Contenus"},
- },
-)
-
+ id: str = Field(description="Reference to source ChatDocument", json_schema_extra={"label": "Objekt-ID"})
+ contents: List[ContentItem] = Field(default_factory=list, description="List of content items", json_schema_extra={"label": "Inhalte"})
+@i18nModel("Chat-Nachricht")
class ChatMessage(PowerOnModel):
"""Messages in chat workflows. User-owned, no mandate context."""
- id: str = Field(
- default_factory=lambda: str(uuid.uuid4()), description="Primary key"
- )
- workflowId: str = Field(description="Foreign key to workflow")
- parentMessageId: Optional[str] = Field(
- None, description="Parent message ID for threading"
- )
- documents: List[ChatDocument] = Field(
- default_factory=list, description="Associated documents"
- )
- documentsLabel: Optional[str] = Field(
- None, description="Label for the set of documents"
- )
- message: Optional[str] = Field(None, description="Message content")
- summary: Optional[str] = Field(
- None, description="Short summary of this message for planning/history"
- )
- role: str = Field(description="Role of the message sender")
- status: str = Field(description="Status of the message (first, step, last)")
- sequenceNr: Optional[int] = Field(
- default=0,
- description="Sequence number of the message (set automatically)"
- )
- publishedAt: Optional[float] = Field(
- default=None,
- description="When the message was published (UTC timestamp in seconds)",
- )
- success: Optional[bool] = Field(
- None, description="Whether the message processing was successful"
- )
- actionId: Optional[str] = Field(
- None, description="ID of the action that produced this message"
- )
- actionMethod: Optional[str] = Field(
- None, description="Method of the action that produced this message"
- )
- actionName: Optional[str] = Field(
- None, description="Name of the action that produced this message"
- )
- roundNumber: Optional[int] = Field(None, description="Round number in workflow")
- taskNumber: Optional[int] = Field(None, description="Task number within round")
- actionNumber: Optional[int] = Field(None, description="Action number within task")
- taskProgress: Optional[str] = Field(
- None, description="Task progress status: pending, running, success, fail, retry"
- )
- actionProgress: Optional[str] = Field(
- None, description="Action progress status: pending, running, success, fail"
- )
-
-
-registerModelLabels(
- "ChatMessage",
- {"en": "Chat Message", "fr": "Message de chat"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "workflowId": {"en": "Workflow ID", "fr": "ID du flux de travail"},
- "parentMessageId": {"en": "Parent Message ID", "fr": "ID du message parent"},
- "documents": {"en": "Documents", "fr": "Documents"},
- "documentsLabel": {"en": "Documents Label", "fr": "Label des documents"},
- "message": {"en": "Message", "fr": "Message"},
- "summary": {"en": "Summary", "fr": "Résumé"},
- "role": {"en": "Role", "fr": "Rôle"},
- "status": {"en": "Status", "fr": "Statut"},
- "sequenceNr": {"en": "Sequence Number", "fr": "Numéro de séquence"},
- "publishedAt": {"en": "Published At", "fr": "Publié le"},
- "success": {"en": "Success", "fr": "Succès"},
- "actionId": {"en": "Action ID", "fr": "ID de l'action"},
- "actionMethod": {"en": "Action Method", "fr": "Méthode de l'action"},
- "actionName": {"en": "Action Name", "fr": "Nom de l'action"},
- "roundNumber": {"en": "Round Number", "fr": "Numéro de tour"},
- "taskNumber": {"en": "Task Number", "fr": "Numéro de tâche"},
- "actionNumber": {"en": "Action Number", "fr": "Numéro d'action"},
- "taskProgress": {"en": "Task Progress", "fr": "Progression de la tâche"},
- "actionProgress": {"en": "Action Progress", "fr": "Progression de l'action"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"label": "ID"})
+ workflowId: str = Field(description="Foreign key to workflow", json_schema_extra={"label": "Workflow-ID"})
+ parentMessageId: Optional[str] = Field(None, description="Parent message ID for threading", json_schema_extra={"label": "Übergeordnete Nachrichten-ID"})
+ documents: List[ChatDocument] = Field(default_factory=list, description="Associated documents", json_schema_extra={"label": "Dokumente"})
+ documentsLabel: Optional[str] = Field(None, description="Label for the set of documents", json_schema_extra={"label": "Dokumenten-Label"})
+ message: Optional[str] = Field(None, description="Message content", json_schema_extra={"label": "Nachricht"})
+ summary: Optional[str] = Field(None, description="Short summary of this message for planning/history", json_schema_extra={"label": "Zusammenfassung"})
+ role: str = Field(description="Role of the message sender", json_schema_extra={"label": "Rolle"})
+ status: str = Field(description="Status of the message (first, step, last)", json_schema_extra={"label": "Status"})
+ sequenceNr: Optional[int] = Field(default=0,
+ description="Sequence number of the message (set automatically)", json_schema_extra={"label": "Sequenznummer"})
+ publishedAt: Optional[float] = Field(default=None,
+ description="When the message was published (UTC timestamp in seconds)", json_schema_extra={"label": "Veröffentlicht am"})
+ success: Optional[bool] = Field(None, description="Whether the message processing was successful", json_schema_extra={"label": "Erfolg"})
+ actionId: Optional[str] = Field(None, description="ID of the action that produced this message", json_schema_extra={"label": "Aktions-ID"})
+ actionMethod: Optional[str] = Field(None, description="Method of the action that produced this message", json_schema_extra={"label": "Aktionsmethode"})
+ actionName: Optional[str] = Field(None, description="Name of the action that produced this message", json_schema_extra={"label": "Aktionsname"})
+ roundNumber: Optional[int] = Field(None, description="Round number in workflow", json_schema_extra={"label": "Rundennummer"})
+ taskNumber: Optional[int] = Field(None, description="Task number within round", json_schema_extra={"label": "Aufgabennummer"})
+ actionNumber: Optional[int] = Field(None, description="Action number within task", json_schema_extra={"label": "Aktionsnummer"})
+ taskProgress: Optional[str] = Field(None, description="Task progress status: pending, running, success, fail, retry", json_schema_extra={"label": "Aufgabenfortschritt"})
+ actionProgress: Optional[str] = Field(None, description="Action progress status: pending, running, success, fail", json_schema_extra={"label": "Aktionsfortschritt"})
class WorkflowModeEnum(str, Enum):
WORKFLOW_DYNAMIC = "Dynamic"
WORKFLOW_AUTOMATION = "Automation"
WORKFLOW_CHATBOT = "Chatbot"
-
-registerModelLabels(
- "WorkflowModeEnum",
- {"en": "Workflow Mode", "fr": "Mode de workflow"},
- {
- "WORKFLOW_DYNAMIC": {"en": "Dynamic", "fr": "Dynamique"},
- "WORKFLOW_AUTOMATION": {"en": "Automation", "fr": "Automatisation"},
- "WORKFLOW_CHATBOT": {"en": "Chatbot", "fr": "Chatbot"},
- },
-)
-
-
+@i18nModel("Chat-Workflow")
class ChatWorkflow(PowerOnModel):
"""Chat workflow container. User-owned, no mandate context."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- featureInstanceId: Optional[str] = Field(None, description="Feature instance ID for multi-tenancy isolation", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ featureInstanceId: Optional[str] = Field(None, description="Feature instance ID for multi-tenancy isolation", json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
status: str = Field(default="running", description="Current status of the workflow", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "running", "label": {"en": "Running", "fr": "En cours"}},
{"value": "completed", "label": {"en": "Completed", "fr": "Terminé"}},
{"value": "stopped", "label": {"en": "Stopped", "fr": "Arrêté"}},
{"value": "error", "label": {"en": "Error", "fr": "Erreur"}},
]})
- name: Optional[str] = Field(None, description="Name of the workflow", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
- currentRound: int = Field(default=0, description="Current round number", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
- currentTask: int = Field(default=0, description="Current task number", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
- currentAction: int = Field(default=0, description="Current action number", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
- totalTasks: int = Field(default=0, description="Total number of tasks in the workflow", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
- totalActions: int = Field(default=0, description="Total number of actions in the workflow", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
- lastActivity: float = Field(default_factory=getUtcTimestamp, description="Timestamp of last activity (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
- startedAt: float = Field(default_factory=getUtcTimestamp, description="When the workflow started (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
- logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- tasks: list = Field(default_factory=list, description="List of tasks in the workflow", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ name: Optional[str] = Field(None, description="Name of the workflow", json_schema_extra={"label": "Name", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
+ currentRound: int = Field(default=0, description="Current round number", json_schema_extra={"label": "Aktuelle Runde", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
+ currentTask: int = Field(default=0, description="Current task number", json_schema_extra={"label": "Aktuelle Aufgabe", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
+ currentAction: int = Field(default=0, description="Current action number", json_schema_extra={"label": "Aktuelle Aktion", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
+ totalTasks: int = Field(default=0, description="Total number of tasks in the workflow", json_schema_extra={"label": "Aufgaben gesamt", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
+ totalActions: int = Field(default=0, description="Total number of actions in the workflow", json_schema_extra={"label": "Aktionen gesamt", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
+ lastActivity: float = Field(default_factory=getUtcTimestamp, description="Timestamp of last activity (UTC timestamp in seconds)", json_schema_extra={"label": "Letzte Aktivität", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
+ startedAt: float = Field(default_factory=getUtcTimestamp, description="When the workflow started (UTC timestamp in seconds)", json_schema_extra={"label": "Gestartet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
+ logs: List[ChatLog] = Field(default_factory=list, description="Workflow logs", json_schema_extra={"label": "Protokolle", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ messages: List[ChatMessage] = Field(default_factory=list, description="Messages in the workflow", json_schema_extra={"label": "Nachrichten", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ tasks: list = Field(default_factory=list, description="List of tasks in the workflow", json_schema_extra={"label": "Aufgaben", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
workflowMode: WorkflowModeEnum = Field(default=WorkflowModeEnum.WORKFLOW_DYNAMIC, description="Workflow mode selector", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{
"value": WorkflowModeEnum.WORKFLOW_DYNAMIC.value,
@@ -296,8 +133,8 @@ class ChatWorkflow(PowerOnModel):
"label": {"en": "Chatbot", "fr": "Chatbot"},
},
]})
- maxSteps: int = Field(default=10, description="Maximum number of iterations in dynamic mode", json_schema_extra={"frontend_type": "integer", "frontend_readonly": False, "frontend_required": False})
- expectedFormats: Optional[List[str]] = Field(None, description="List of expected file format extensions from user request (e.g., ['xlsx', 'pdf']). Extracted during intent analysis.", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ maxSteps: int = Field(default=10, description="Maximum number of iterations in dynamic mode", json_schema_extra={"label": "Max. Schritte", "frontend_type": "integer", "frontend_readonly": False, "frontend_required": False})
+ expectedFormats: Optional[List[str]] = Field(None, description="List of expected file format extensions from user request (e.g., ['xlsx', 'pdf']). Extracted during intent analysis.", json_schema_extra={"label": "Erwartete Formate", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
# Helper methods for execution state management
def getRoundIndex(self) -> int:
@@ -327,80 +164,27 @@ class ChatWorkflow(PowerOnModel):
"""Increment action when executing new action in current task"""
self.currentAction += 1
-
-registerModelLabels(
- "ChatWorkflow",
- {"en": "Chat Workflow", "fr": "Flux de travail de chat"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
- "status": {"en": "Status", "fr": "Statut"},
- "name": {"en": "Name", "fr": "Nom"},
- "currentRound": {"en": "Current Round", "fr": "Tour actuel"},
- "currentTask": {"en": "Current Task", "fr": "Tâche actuelle"},
- "currentAction": {"en": "Current Action", "fr": "Action actuelle"},
- "totalTasks": {"en": "Total Tasks", "fr": "Total des tâches"},
- "totalActions": {"en": "Total Actions", "fr": "Total des actions"},
- "lastActivity": {"en": "Last Activity", "fr": "Dernière activité"},
- "startedAt": {"en": "Started At", "fr": "Démarré le"},
- "logs": {"en": "Logs", "fr": "Journaux"},
- "messages": {"en": "Messages", "fr": "Messages"},
- "stats": {"en": "Statistics", "fr": "Statistiques"},
- "tasks": {"en": "Tasks", "fr": "Tâches"},
- "workflowMode": {"en": "Workflow Mode", "fr": "Mode de workflow"},
- "maxSteps": {"en": "Max Steps", "fr": "Étapes max"},
- "expectedFormats": {"en": "Expected Formats", "fr": "Formats attendus"},
- },
-)
-
-
+@i18nModel("Benutzereingabe")
class UserInputRequest(BaseModel):
- prompt: str = Field(description="Prompt for the user")
- listFileId: List[str] = Field(default_factory=list, description="List of file IDs")
- userLanguage: str = Field(default="en", description="User's preferred language")
- workflowId: Optional[str] = Field(None, description="Optional ID of the workflow to continue")
- allowedProviders: Optional[List[str]] = Field(None, description="List of allowed AI providers (multiselect)")
-
-
-registerModelLabels(
- "UserInputRequest",
- {"en": "User Input Request", "fr": "Demande de saisie utilisateur"},
- {
- "prompt": {"en": "Prompt", "fr": "Invite"},
- "listFileId": {"en": "File IDs", "fr": "IDs des fichiers"},
- "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
- "preferredProvider": {"en": "Preferred Provider", "fr": "Fournisseur préféré"},
- },
-)
-
+ prompt: str = Field(description="Prompt for the user", json_schema_extra={"label": "Eingabeaufforderung"})
+ listFileId: List[str] = Field(default_factory=list, description="List of file IDs", json_schema_extra={"label": "Datei-IDs"})
+ userLanguage: str = Field(default="en", description="User's preferred language", json_schema_extra={"label": "Benutzersprache"})
+ workflowId: Optional[str] = Field(None, description="Optional ID of the workflow to continue", json_schema_extra={"label": "Workflow-ID"})
+ allowedProviders: Optional[List[str]] = Field(None, description="List of allowed AI providers (multiselect)", json_schema_extra={"label": "Erlaubte Anbieter"})
+@i18nModel("Aktions-Dokument")
class ActionDocument(BaseModel):
"""Clear document structure for action results"""
- documentName: str = Field(description="Name of the document")
- documentData: Any = Field(description="Content/data of the document")
- mimeType: str = Field(description="MIME type of the document")
- sourceJson: Optional[Dict[str, Any]] = Field(
- None,
- description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
- )
- validationMetadata: Optional[Dict[str, Any]] = Field(
- None,
- description="Action-specific metadata for content validation (e.g., email recipients, attachments, SharePoint paths)"
- )
-
-
-registerModelLabels(
- "ActionDocument",
- {"en": "Action Document", "fr": "Document d'action"},
- {
- "documentName": {"en": "Document Name", "fr": "Nom du document"},
- "documentData": {"en": "Document Data", "fr": "Données du document"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- },
-)
-
+ documentName: str = Field(description="Name of the document", json_schema_extra={"label": "Dokumentname"})
+ documentData: Any = Field(description="Content/data of the document", json_schema_extra={"label": "Dokumentdaten"})
+ mimeType: str = Field(description="MIME type of the document", json_schema_extra={"label": "MIME-Typ"})
+ sourceJson: Optional[Dict[str, Any]] = Field(None,
+ description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)", json_schema_extra={"label": "Quell-JSON"})
+ validationMetadata: Optional[Dict[str, Any]] = Field(None,
+ description="Action-specific metadata for content validation (e.g., email recipients, attachments, SharePoint paths)", json_schema_extra={"label": "Validierungs-Metadaten"})
+@i18nModel("Aktionsergebnis")
class ActionResult(BaseModel):
"""Clean action result with documents as primary output
@@ -409,15 +193,11 @@ class ActionResult(BaseModel):
from the action plan. This ensures consistent document routing throughout the workflow.
"""
- success: bool = Field(description="Whether execution succeeded")
- error: Optional[str] = Field(None, description="Error message if failed")
- documents: List[ActionDocument] = Field(
- default_factory=list, description="Document outputs"
- )
- resultLabel: Optional[str] = Field(
- None,
- description="Label for document routing (set by action handler, not by action methods)",
- )
+ success: bool = Field(description="Whether execution succeeded", json_schema_extra={"label": "Erfolg"})
+ error: Optional[str] = Field(None, description="Error message if failed", json_schema_extra={"label": "Fehler"})
+ documents: List[ActionDocument] = Field(default_factory=list, description="Document outputs", json_schema_extra={"label": "Dokumente"})
+ resultLabel: Optional[str] = Field(None,
+ description="Label for document routing (set by action handler, not by action methods)", json_schema_extra={"label": "Ergebnis-Label"})
@classmethod
def isSuccess(cls, documents: List[ActionDocument] = None) -> "ActionResult":
@@ -429,76 +209,32 @@ class ActionResult(BaseModel):
) -> "ActionResult":
return cls(success=False, documents=documents or [], error=error)
-
-registerModelLabels(
- "ActionResult",
- {"en": "Action Result", "fr": "Résultat de l'action"},
- {
- "success": {"en": "Success", "fr": "Succès"},
- "error": {"en": "Error", "fr": "Erreur"},
- "documents": {"en": "Documents", "fr": "Documents"},
- "resultLabel": {"en": "Result Label", "fr": "Étiquette du résultat"},
- },
-)
-
-
+@i18nModel("Aktionsauswahl")
class ActionSelection(BaseModel):
- method: str = Field(description="Method to execute (e.g., web, document, ai)")
- name: str = Field(
- description="Action name within the method (e.g., search, extract)"
- )
-
-
-registerModelLabels(
- "ActionSelection",
- {"en": "Action Selection", "fr": "Sélection d'action"},
- {
- "method": {"en": "Method", "fr": "Méthode"},
- "name": {"en": "Action Name", "fr": "Nom de l'action"},
- },
-)
-
+ method: str = Field(description="Method to execute (e.g., web, document, ai)", json_schema_extra={"label": "Methode"})
+ name: str = Field(description="Action name within the method (e.g., search, extract)", json_schema_extra={"label": "Aktionsname"})
+@i18nModel("Aktionsparameter")
class ActionParameters(BaseModel):
- parameters: Dict[str, Any] = Field(
- default_factory=dict, description="Parameters to execute the selected action"
- )
-
-
-registerModelLabels(
- "ActionParameters",
- {"en": "Action Parameters", "fr": "Paramètres d'action"},
- {
- "parameters": {"en": "Parameters", "fr": "Paramètres"},
- },
-)
-
+ parameters: Dict[str, Any] = Field(default_factory=dict, description="Parameters to execute the selected action", json_schema_extra={"label": "Parameter"})
+@i18nModel("Beobachtungs-Vorschau")
+@i18nModel("Beobachtung")
+@i18nModel("Beobachtungs-Vorschau")
+@i18nModel("Beobachtung")
class ObservationPreview(BaseModel):
- name: str = Field(description="Document name or URL label")
- mime: Optional[str] = Field(default=None, description="MIME type or kind (legacy field)")
- snippet: Optional[str] = Field(default=None, description="Short snippet or summary")
+ name: str = Field(description="Document name or URL label", json_schema_extra={"label": "Name"})
+ mime: Optional[str] = Field(default=None, description="MIME type or kind (legacy field)", json_schema_extra={"label": "MIME"})
+ snippet: Optional[str] = Field(default=None, description="Short snippet or summary", json_schema_extra={"label": "Ausschnitt"})
# Extended metadata fields
- mimeType: Optional[str] = Field(default=None, description="MIME type")
- size: Optional[str] = Field(default=None, description="File size")
- created: Optional[str] = Field(default=None, description="Creation timestamp")
- modified: Optional[str] = Field(default=None, description="Modification timestamp")
- typeGroup: Optional[str] = Field(default=None, description="Document type group")
- documentId: Optional[str] = Field(default=None, description="Document ID")
- reference: Optional[str] = Field(default=None, description="Document reference")
- contentSize: Optional[str] = Field(default=None, description="Content size indicator")
-
-
-registerModelLabels(
- "ObservationPreview",
- {"en": "Observation Preview", "fr": "Aperçu d'observation"},
- {
- "name": {"en": "Name", "fr": "Nom"},
- "mime": {"en": "MIME", "fr": "MIME"},
- "snippet": {"en": "Snippet", "fr": "Extrait"},
- },
-)
-
+ mimeType: Optional[str] = Field(default=None, description="MIME type", json_schema_extra={"label": "MIME-Typ"})
+ size: Optional[str] = Field(default=None, description="File size", json_schema_extra={"label": "Größe"})
+ created: Optional[str] = Field(default=None, description="Creation timestamp", json_schema_extra={"label": "Erstellt"})
+ modified: Optional[str] = Field(default=None, description="Modification timestamp", json_schema_extra={"label": "Geändert"})
+ typeGroup: Optional[str] = Field(default=None, description="Document type group", json_schema_extra={"label": "Typgruppe"})
+ documentId: Optional[str] = Field(default=None, description="Document ID", json_schema_extra={"label": "Dokument-ID"})
+ reference: Optional[str] = Field(default=None, description="Document reference", json_schema_extra={"label": "Referenz"})
+ contentSize: Optional[str] = Field(default=None, description="Content size indicator", json_schema_extra={"label": "Inhaltsgröße"})
class Observation(BaseModel):
success: bool = Field(description="Action execution success flag")
@@ -518,20 +254,6 @@ class Observation(BaseModel):
default=None, description="Content analysis results"
)
-
-registerModelLabels(
- "Observation",
- {"en": "Observation", "fr": "Observation"},
- {
- "success": {"en": "Success", "fr": "Succès"},
- "resultLabel": {"en": "Result Label", "fr": "Étiquette du résultat"},
- "documentsCount": {"en": "Documents Count", "fr": "Nombre de documents"},
- "previews": {"en": "Previews", "fr": "Aperçus"},
- "notes": {"en": "Notes", "fr": "Notes"},
- },
-)
-
-
class TaskStatus(str, Enum):
PENDING = "pending"
RUNNING = "running"
@@ -539,64 +261,27 @@ class TaskStatus(str, Enum):
FAILED = "failed"
CANCELLED = "cancelled"
-
-registerModelLabels(
- "TaskStatus",
- {"en": "Task Status", "fr": "Statut de la tâche"},
- {
- "PENDING": {"en": "Pending", "fr": "En attente"},
- "RUNNING": {"en": "Running", "fr": "En cours"},
- "COMPLETED": {"en": "Completed", "fr": "Terminé"},
- "FAILED": {"en": "Failed", "fr": "Échec"},
- "CANCELLED": {"en": "Cancelled", "fr": "Annulé"},
- },
-)
-
-
+@i18nModel("Dokumentaustausch")
class DocumentExchange(BaseModel):
- documentsLabel: str = Field(description="Label for the set of documents")
- documents: List[str] = Field(
- default_factory=list, description="List of document references"
- )
-
-
-registerModelLabels(
- "DocumentExchange",
- {"en": "Document Exchange", "fr": "Échange de documents"},
- {
- "documentsLabel": {"en": "Documents Label", "fr": "Label des documents"},
- "documents": {"en": "Documents", "fr": "Documents"},
- },
-)
-
+ documentsLabel: str = Field(description="Label for the set of documents", json_schema_extra={"label": "Dokumenten-Label"})
+ documents: List[str] = Field(default_factory=list, description="List of document references", json_schema_extra={"label": "Dokumente"})
+@i18nModel("Aufgaben-Aktion")
class ActionItem(BaseModel):
- id: str = Field(..., description="Action ID")
- execMethod: str = Field(..., description="Method to execute")
- execAction: str = Field(..., description="Action to perform")
- execParameters: Dict[str, Any] = Field(
- default_factory=dict, description="Action parameters"
- )
- execResultLabel: Optional[str] = Field(
- None, description="Label for the set of result documents"
- )
- expectedDocumentFormats: Optional[List[Dict[str, str]]] = Field(
- None, description="Expected document formats (optional)"
- )
- userMessage: Optional[str] = Field(
- None, description="User-friendly message in user's language"
- )
- status: TaskStatus = Field(default=TaskStatus.PENDING, description="Action status")
- error: Optional[str] = Field(None, description="Error message if action failed")
- retryCount: int = Field(default=0, description="Number of retries attempted")
- retryMax: int = Field(default=3, description="Maximum number of retries")
- processingTime: Optional[float] = Field(
- None, description="Processing time in seconds"
- )
- timestamp: float = Field(
- ..., description="When the action was executed (UTC timestamp in seconds)"
- )
- result: Optional[str] = Field(None, description="Result of the action")
+ id: str = Field(..., description="Action ID", json_schema_extra={"label": "Aktions-ID"})
+ execMethod: str = Field(..., description="Method to execute", json_schema_extra={"label": "Methode"})
+ execAction: str = Field(..., description="Action to perform", json_schema_extra={"label": "Aktion"})
+ execParameters: Dict[str, Any] = Field(default_factory=dict, description="Action parameters", json_schema_extra={"label": "Parameter"})
+ execResultLabel: Optional[str] = Field(None, description="Label for the set of result documents", json_schema_extra={"label": "Ergebnis-Label"})
+ expectedDocumentFormats: Optional[List[Dict[str, str]]] = Field(None, description="Expected document formats (optional)", json_schema_extra={"label": "Erwartete Dokumentformate"})
+ userMessage: Optional[str] = Field(None, description="User-friendly message in user's language", json_schema_extra={"label": "Benutzernachricht"})
+ status: TaskStatus = Field(default=TaskStatus.PENDING, description="Action status", json_schema_extra={"label": "Status"})
+ error: Optional[str] = Field(None, description="Error message if action failed", json_schema_extra={"label": "Fehler"})
+ retryCount: int = Field(default=0, description="Number of retries attempted", json_schema_extra={"label": "Wiederholungen"})
+ retryMax: int = Field(default=3, description="Maximum number of retries", json_schema_extra={"label": "Max. Wiederholungen"})
+ processingTime: Optional[float] = Field(None, description="Processing time in seconds", json_schema_extra={"label": "Bearbeitungszeit"})
+ timestamp: float = Field(..., description="When the action was executed (UTC timestamp in seconds)", json_schema_extra={"label": "Zeitstempel"})
+ result: Optional[str] = Field(None, description="Result of the action", json_schema_extra={"label": "Ergebnis"})
def setSuccess(self, result: str = None) -> None:
"""Set the action as successful with optional result"""
@@ -610,191 +295,59 @@ class ActionItem(BaseModel):
self.status = TaskStatus.FAILED
self.error = error_message
+@i18nModel("Chat-Aufgabenergebnis")
+class ChatTaskResult(BaseModel):
+ taskId: str = Field(..., description="Task ID", json_schema_extra={"label": "Aufgaben-ID"})
+ status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status", json_schema_extra={"label": "Status"})
+ success: bool = Field(..., description="Whether the task was successful", json_schema_extra={"label": "Erfolg"})
+ feedback: Optional[str] = Field(None, description="Task feedback message", json_schema_extra={"label": "Rückmeldung"})
+ error: Optional[str] = Field(None, description="Error message if task failed", json_schema_extra={"label": "Fehler"})
-registerModelLabels(
- "ActionItem",
- {"en": "Task Action", "fr": "Action de tâche"},
- {
- "id": {"en": "Action ID", "fr": "ID de l'action"},
- "execMethod": {"en": "Method", "fr": "Méthode"},
- "execAction": {"en": "Action", "fr": "Action"},
- "execParameters": {"en": "Parameters", "fr": "Paramètres"},
- "execResultLabel": {"en": "Result Label", "fr": "Label du résultat"},
- "expectedDocumentFormats": {
- "en": "Expected Document Formats",
- "fr": "Formats de documents attendus",
- },
- "userMessage": {"en": "User Message", "fr": "Message utilisateur"},
- "status": {"en": "Status", "fr": "Statut"},
- "error": {"en": "Error", "fr": "Erreur"},
- "retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
- "retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
- "processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
- "timestamp": {"en": "Timestamp", "fr": "Horodatage"},
- "result": {"en": "Result", "fr": "Résultat"},
- },
-)
-
-
-class TaskResult(BaseModel):
- taskId: str = Field(..., description="Task ID")
- status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
- success: bool = Field(..., description="Whether the task was successful")
- feedback: Optional[str] = Field(None, description="Task feedback message")
- error: Optional[str] = Field(None, description="Error message if task failed")
-
-
-registerModelLabels(
- "TaskResult",
- {"en": "Task Result", "fr": "Résultat de tâche"},
- {
- "taskId": {"en": "Task ID", "fr": "ID de la tâche"},
- "status": {"en": "Status", "fr": "Statut"},
- "success": {"en": "Success", "fr": "Succès"},
- "feedback": {"en": "Feedback", "fr": "Retour"},
- "error": {"en": "Error", "fr": "Erreur"},
- },
-)
-
-
+@i18nModel("Aufgabe")
class TaskItem(BaseModel):
- id: str = Field(..., description="Task ID")
- workflowId: str = Field(..., description="Workflow ID")
- userInput: str = Field(..., description="User input that triggered the task")
- status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status")
- error: Optional[str] = Field(None, description="Error message if task failed")
- startedAt: Optional[float] = Field(
- None, description="When the task started (UTC timestamp in seconds)"
- )
- finishedAt: Optional[float] = Field(
- None, description="When the task finished (UTC timestamp in seconds)"
- )
- actionList: List[ActionItem] = Field(
- default_factory=list, description="List of actions to execute"
- )
- retryCount: int = Field(default=0, description="Number of retries attempted")
- retryMax: int = Field(default=3, description="Maximum number of retries")
- rollbackOnFailure: bool = Field(
- default=True, description="Whether to rollback on failure"
- )
- dependencies: List[str] = Field(
- default_factory=list, description="List of task IDs this task depends on"
- )
- feedback: Optional[str] = Field(None, description="Task feedback message")
- processingTime: Optional[float] = Field(
- None, description="Total processing time in seconds"
- )
- resultLabels: Optional[Dict[str, Any]] = Field(
- default_factory=dict, description="Map of result labels to their values"
- )
-
-
-registerModelLabels(
- "TaskItem",
- {"en": "Task", "fr": "Tâche"},
- {
- "id": {"en": "Task ID", "fr": "ID de la tâche"},
- "workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
- "userInput": {"en": "User Input", "fr": "Entrée utilisateur"},
- "status": {"en": "Status", "fr": "Statut"},
- "error": {"en": "Error", "fr": "Erreur"},
- "startedAt": {"en": "Started At", "fr": "Démarré à"},
- "finishedAt": {"en": "Finished At", "fr": "Terminé à"},
- "actionList": {"en": "Actions", "fr": "Actions"},
- "retryCount": {"en": "Retry Count", "fr": "Nombre de tentatives"},
- "retryMax": {"en": "Max Retries", "fr": "Tentatives max"},
- "processingTime": {"en": "Processing Time", "fr": "Temps de traitement"},
- },
-)
-
+ id: str = Field(..., description="Task ID", json_schema_extra={"label": "Aufgaben-ID"})
+ workflowId: str = Field(..., description="Workflow ID", json_schema_extra={"label": "Workflow-ID"})
+ userInput: str = Field(..., description="User input that triggered the task", json_schema_extra={"label": "Benutzereingabe"})
+ status: TaskStatus = Field(default=TaskStatus.PENDING, description="Task status", json_schema_extra={"label": "Status"})
+ error: Optional[str] = Field(None, description="Error message if task failed", json_schema_extra={"label": "Fehler"})
+ startedAt: Optional[float] = Field(None, description="When the task started (UTC timestamp in seconds)", json_schema_extra={"label": "Gestartet am"})
+ finishedAt: Optional[float] = Field(None, description="When the task finished (UTC timestamp in seconds)", json_schema_extra={"label": "Beendet am"})
+ actionList: List[ActionItem] = Field(default_factory=list, description="List of actions to execute", json_schema_extra={"label": "Aktionen"})
+ retryCount: int = Field(default=0, description="Number of retries attempted", json_schema_extra={"label": "Wiederholungen"})
+ retryMax: int = Field(default=3, description="Maximum number of retries", json_schema_extra={"label": "Max. Wiederholungen"})
+ rollbackOnFailure: bool = Field(default=True, description="Whether to rollback on failure", json_schema_extra={"label": "Bei Fehler zurücksetzen"})
+ dependencies: List[str] = Field(default_factory=list, description="List of task IDs this task depends on", json_schema_extra={"label": "Abhängigkeiten"})
+ feedback: Optional[str] = Field(None, description="Task feedback message", json_schema_extra={"label": "Rückmeldung"})
+ processingTime: Optional[float] = Field(None, description="Total processing time in seconds", json_schema_extra={"label": "Bearbeitungszeit"})
+ resultLabels: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Map of result labels to their values", json_schema_extra={"label": "Ergebnis-Labels"})
+@i18nModel("Aufgabenschritt")
class TaskStep(BaseModel):
- id: str
- objective: str
- dependencies: Optional[list[str]] = Field(default_factory=list)
- successCriteria: Optional[list[str]] = Field(default_factory=list)
+ id: str = Field(description="Task identifier", json_schema_extra={"label": "ID"})
+ objective: str = Field(description="Task objective", json_schema_extra={"label": "Ziel"})
+ dependencies: Optional[list[str]] = Field(default_factory=list, json_schema_extra={"label": "ID"})
+ successCriteria: Optional[list[str]] = Field(default_factory=list, json_schema_extra={"label": "Erfolgskriterien"})
estimatedComplexity: Optional[str] = None
- userMessage: Optional[str] = Field(
- None, description="User-friendly message in user's language"
- )
+ userMessage: Optional[str] = Field(None, description="User-friendly message in user's language", json_schema_extra={"label": "Benutzernachricht"})
# Format details extracted from intent analysis
- dataType: Optional[str] = Field(
- None, description="Expected data type (text, numbers, documents, etc.)"
- )
- expectedFormats: Optional[List[str]] = Field(
- None, description="Expected output file format extensions (e.g., ['docx', 'pdf', 'xlsx']). Use actual file extensions, not conceptual terms."
- )
- qualityRequirements: Optional[Dict[str, Any]] = Field(
- None, description="Quality requirements and constraints"
- )
-
-
-registerModelLabels(
- "TaskStep",
- {"en": "Task Step", "fr": "Étape de tâche"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "objective": {"en": "Objective", "fr": "Objectif"},
- "dependencies": {"en": "Dependencies", "fr": "Dépendances"},
- "successCriteria": {"en": "Success Criteria", "fr": "Critères de succès"},
- "estimatedComplexity": {
- "en": "Estimated Complexity",
- "fr": "Complexité estimée",
- },
- "userMessage": {"en": "User Message", "fr": "Message utilisateur"},
- "expectedFormats": {"en": "Expected Formats", "fr": "Formats attendus"},
- },
-)
-
+ dataType: Optional[str] = Field(None, description="Expected data type (text, numbers, documents, etc.)", json_schema_extra={"label": "Datentyp"})
+ expectedFormats: Optional[List[str]] = Field(None, description="Expected output file format extensions (e.g., ['docx', 'pdf', 'xlsx']). Use actual file extensions, not conceptual terms.", json_schema_extra={"label": "Erwartete Formate"})
+ qualityRequirements: Optional[Dict[str, Any]] = Field(None, description="Quality requirements and constraints", json_schema_extra={"label": "Qualitätsanforderungen"})
+@i18nModel("Aufgabenübergabe")
+@i18nModel("Aufgabenübergabe")
class TaskHandover(BaseModel):
- taskId: str = Field(description="Target task ID")
- sourceTask: Optional[str] = Field(None, description="Source task ID")
- inputDocuments: List[DocumentExchange] = Field(
- default_factory=list, description="Available input documents"
- )
- outputDocuments: List[DocumentExchange] = Field(
- default_factory=list, description="Produced output documents"
- )
- context: Dict[str, Any] = Field(default_factory=dict, description="Task context")
- previousResults: List[str] = Field(
- default_factory=list, description="Previous result summaries"
- )
- improvements: List[str] = Field(
- default_factory=list, description="Improvement suggestions"
- )
- workflowSummary: Optional[str] = Field(
- None, description="Summarized workflow context"
- )
- messageHistory: List[str] = Field(
- default_factory=list, description="Key message summaries"
- )
- timestamp: float = Field(
- ..., description="When the handover was created (UTC timestamp in seconds)"
- )
- handoverType: str = Field(
- default="task", description="Type of handover: task, phase, or workflow"
- )
-
-
-registerModelLabels(
- "TaskHandover",
- {"en": "Task Handover", "fr": "Transfert de tâche"},
- {
- "taskId": {"en": "Task ID", "fr": "ID de la tâche"},
- "sourceTask": {"en": "Source Task", "fr": "Tâche source"},
- "inputDocuments": {"en": "Input Documents", "fr": "Documents d'entrée"},
- "outputDocuments": {"en": "Output Documents", "fr": "Documents de sortie"},
- "context": {"en": "Context", "fr": "Contexte"},
- "previousResults": {"en": "Previous Results", "fr": "Résultats précédents"},
- "improvements": {"en": "Improvements", "fr": "Améliorations"},
- "workflowSummary": {"en": "Workflow Summary", "fr": "Résumé du workflow"},
- "messageHistory": {"en": "Message History", "fr": "Historique des messages"},
- "timestamp": {"en": "Timestamp", "fr": "Horodatage"},
- "handoverType": {"en": "Handover Type", "fr": "Type de transfert"},
- },
-)
-
+ taskId: str = Field(description="Target task ID", json_schema_extra={"label": "Aufgaben-ID"})
+ sourceTask: Optional[str] = Field(None, description="Source task ID", json_schema_extra={"label": "Quell-Aufgabe"})
+ inputDocuments: List[DocumentExchange] = Field(default_factory=list, description="Available input documents", json_schema_extra={"label": "Eingabedokumente"})
+ outputDocuments: List[DocumentExchange] = Field(default_factory=list, description="Produced output documents", json_schema_extra={"label": "Ausgabedokumente"})
+ context: Dict[str, Any] = Field(default_factory=dict, description="Task context", json_schema_extra={"label": "Kontext"})
+ previousResults: List[str] = Field(default_factory=list, description="Previous result summaries", json_schema_extra={"label": "Vorherige Ergebnisse"})
+ improvements: List[str] = Field(default_factory=list, description="Improvement suggestions", json_schema_extra={"label": "Verbesserungen"})
+ workflowSummary: Optional[str] = Field(None, description="Summarized workflow context", json_schema_extra={"label": "Workflow-Zusammenfassung"})
+ messageHistory: List[str] = Field(default_factory=list, description="Key message summaries", json_schema_extra={"label": "Nachrichtenverlauf"})
+ timestamp: float = Field(..., description="When the handover was created (UTC timestamp in seconds)", json_schema_extra={"label": "Zeitstempel"})
+ handoverType: str = Field(default="task", description="Type of handover: task, phase, or workflow", json_schema_extra={"label": "Übergabetyp"})
class TaskContext(BaseModel):
taskStep: TaskStep
@@ -849,7 +402,6 @@ class TaskContext(BaseModel):
self.improvements = []
self.improvements.append(improvement)
-
class ReviewContext(BaseModel):
taskStep: TaskStep
taskActions: Optional[list] = Field(default_factory=list)
@@ -858,99 +410,40 @@ class ReviewContext(BaseModel):
workflowId: Optional[str] = None
previousResults: Optional[list[str]] = Field(default_factory=list)
-
+@i18nModel("Prüfergebnis")
+@i18nModel("Prüfergebnis")
class ReviewResult(BaseModel):
status: str
reason: Optional[str] = None
- improvements: Optional[list[str]] = Field(default_factory=list)
- qualityScore: Optional[float] = Field(default=5.0, description="Quality score (0-10)")
- missingOutputs: Optional[list[str]] = Field(default_factory=list)
- metCriteria: Optional[list[str]] = Field(default_factory=list)
- unmetCriteria: Optional[list[str]] = Field(default_factory=list)
+ improvements: Optional[list[str]] = Field(default_factory=list, json_schema_extra={"label": "Verbesserungen"})
+ qualityScore: Optional[float] = Field(default=5.0, description="Quality score (0-10)", json_schema_extra={"label": "Qualitätsscore"})
+ missingOutputs: Optional[list[str]] = Field(default_factory=list, json_schema_extra={"label": "Fehlende Ausgaben"})
+ metCriteria: Optional[list[str]] = Field(default_factory=list, json_schema_extra={"label": "Erfüllte Kriterien"})
+ unmetCriteria: Optional[list[str]] = Field(default_factory=list, json_schema_extra={"label": "Nicht erfüllte Kriterien"})
confidence: Optional[float] = 0.5
- userMessage: Optional[str] = Field(
- None, description="User-friendly message in user's language"
- )
+ userMessage: Optional[str] = Field(None, description="User-friendly message in user's language", json_schema_extra={"label": "Benutzernachricht"})
# NEW: Concrete next action guidance (when status is "continue")
- nextAction: Optional[str] = Field(
- None, description="Specific action to execute next (e.g., 'ai.convert', 'ai.process', 'ai.reformat')"
- )
- nextActionParameters: Optional[Dict[str, Any]] = Field(
- None, description="Parameters for the next action (e.g., {'fromFormat': 'json', 'toFormat': 'csv'})"
- )
- nextActionObjective: Optional[str] = Field(
- None, description="What this specific action will achieve"
- )
-
-
-registerModelLabels(
- "ReviewResult",
- {"en": "Review Result", "fr": "Résultat de l'évaluation"},
- {
- "status": {"en": "Status", "fr": "Statut"},
- "reason": {"en": "Reason", "fr": "Raison"},
- "improvements": {"en": "Improvements", "fr": "Améliorations"},
- "qualityScore": {"en": "Quality Score", "fr": "Score de qualité"},
- "missingOutputs": {"en": "Missing Outputs", "fr": "Sorties manquantes"},
- "metCriteria": {"en": "Met Criteria", "fr": "Critères respectés"},
- "unmetCriteria": {"en": "Unmet Criteria", "fr": "Critères non respectés"},
- "confidence": {"en": "Confidence", "fr": "Confiance"},
- "userMessage": {"en": "User Message", "fr": "Message utilisateur"},
- },
-)
-
+ nextAction: Optional[str] = Field(None, description="Specific action to execute next (e.g., 'ai.convert', 'ai.process', 'ai.reformat')", json_schema_extra={"label": "Nächste Aktion"})
+ nextActionParameters: Optional[Dict[str, Any]] = Field(None, description="Parameters for the next action (e.g., {'fromFormat': 'json', 'toFormat': 'csv'})", json_schema_extra={"label": "Parameter nächste Aktion"})
+ nextActionObjective: Optional[str] = Field(None, description="What this specific action will achieve", json_schema_extra={"label": "Ziel nächste Aktion"})
+@i18nModel("Aufgabenplan")
class TaskPlan(BaseModel):
- overview: str
- tasks: list[TaskStep]
- userMessage: Optional[str] = Field(
- None, description="Overall user-friendly message for the task plan"
- )
-
-
-registerModelLabels(
- "TaskPlan",
- {"en": "Task Plan", "fr": "Plan de tâches"},
- {
- "overview": {"en": "Overview", "fr": "Aperçu"},
- "tasks": {"en": "Tasks", "fr": "Tâches"},
- "userMessage": {"en": "User Message", "fr": "Message utilisateur"},
- },
-)
+ overview: str = Field(json_schema_extra={"label": "Überblick"})
+ tasks: list[TaskStep] = Field(json_schema_extra={"label": "Aufgaben"})
+ userMessage: Optional[str] = Field(None, description="Overall user-friendly message for the task plan", json_schema_extra={"label": "Benutzernachricht"})
# Forward references resolved automatically since ChatWorkflow is defined above
-
+@i18nModel("Prompt-Platzhalter")
class PromptPlaceholder(BaseModel):
- label: str
- content: str
- summaryAllowed: bool = Field(
- default=False,
- description="Whether host may summarize content before sending to AI",
- )
-
-
-registerModelLabels(
- "PromptPlaceholder",
- {"en": "Prompt Placeholder", "fr": "Espace réservé d'invite"},
- {
- "label": {"en": "Label", "fr": "Libellé"},
- "content": {"en": "Content", "fr": "Contenu"},
- "summaryAllowed": {"en": "Summary Allowed", "fr": "Résumé autorisé"},
- },
-)
-
+ label: str = Field(json_schema_extra={"label": "Bezeichnung"})
+ content: str = Field(json_schema_extra={"label": "Inhalt"})
+ summaryAllowed: bool = Field(default=False,
+ description="Whether host may summarize content before sending to AI", json_schema_extra={"label": "Zusammenfassung erlaubt"})
+@i18nModel("Prompt-Paket")
class PromptBundle(BaseModel):
- prompt: str
- placeholders: List[PromptPlaceholder] = Field(default_factory=list)
+ prompt: str = Field(json_schema_extra={"label": "Prompt"})
+ placeholders: List[PromptPlaceholder] = Field(default_factory=list, json_schema_extra={"label": "Prompt"})
-
-registerModelLabels(
- "PromptBundle",
- {"en": "Prompt Bundle", "fr": "Lot d'invite"},
- {
- "prompt": {"en": "Prompt", "fr": "Invite"},
- "placeholders": {"en": "Placeholders", "fr": "Espaces réservés"},
- },
-)
diff --git a/modules/datamodels/datamodelDataSource.py b/modules/datamodels/datamodelDataSource.py
index 1d432041..441d7e7d 100644
--- a/modules/datamodels/datamodelDataSource.py
+++ b/modules/datamodels/datamodelDataSource.py
@@ -9,66 +9,81 @@ Google Drive folder, FTP directory, etc.) for agent-accessible data containers.
from typing import Dict, Any, Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
+@i18nModel("Datenquelle")
class DataSource(PowerOnModel):
- """Configured external data source linked to a UserConnection."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- connectionId: str = Field(description="FK to UserConnection")
- sourceType: str = Field(
- description="sharepointFolder, googleDriveFolder, outlookFolder, ftpFolder, clickupList (path under /team/...)"
+ """Konfigurierte externe Datenquelle verknuepft mit einer UserConnection."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ connectionId: str = Field(
+ description="FK to UserConnection",
+ json_schema_extra={"label": "Verbindungs-ID"},
+ )
+ sourceType: str = Field(
+ description="sharepointFolder, googleDriveFolder, outlookFolder, ftpFolder, clickupList (path under /team/...)",
+ json_schema_extra={"label": "Quellentyp"},
+ )
+ path: str = Field(
+ description="External path (e.g. '/sites/MySite/Documents/Reports')",
+ json_schema_extra={"label": "Pfad"},
+ )
+ label: str = Field(
+ description="User-visible label (often the last path segment)",
+ json_schema_extra={"label": "Bezeichnung"},
)
- path: str = Field(description="External path (e.g. '/sites/MySite/Documents/Reports')")
- label: str = Field(description="User-visible label (often the last path segment)")
displayPath: Optional[str] = Field(
default=None,
description="Human-readable full path for UI (connection-relative, slash-separated)",
+ json_schema_extra={"label": "Anzeigepfad"},
+ )
+ featureInstanceId: Optional[str] = Field(
+ default=None,
+ description="Scoped to feature instance",
+ json_schema_extra={"label": "Feature-Instanz"},
+ )
+ mandateId: Optional[str] = Field(
+ default=None,
+ description="Mandate scope",
+ json_schema_extra={"label": "Mandanten-ID"},
+ )
+ userId: str = Field(
+ default="",
+ description="Owner user ID",
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ autoSync: bool = Field(
+ default=False,
+ description="Automatically sync on schedule",
+ json_schema_extra={"label": "Auto-Sync"},
+ )
+ lastSynced: Optional[float] = Field(
+ default=None,
+ description="Last sync timestamp",
+ json_schema_extra={"label": "Letzter Sync"},
)
- featureInstanceId: Optional[str] = Field(default=None, description="Scoped to feature instance")
- mandateId: Optional[str] = Field(default=None, description="Mandate scope")
- userId: str = Field(default="", description="Owner user ID")
- autoSync: bool = Field(default=False, description="Automatically sync on schedule")
- lastSynced: Optional[float] = Field(default=None, description="Last sync timestamp")
scope: str = Field(
default="personal",
description="Data visibility scope: personal, featureInstance, mandate, global",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
{"value": "global", "label": {"en": "Global", "de": "Global"}},
- ]}
+ ]},
)
neutralize: bool = Field(
default=False,
description="Whether this data source should be neutralized before AI processing",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Neutralisieren", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
)
-registerModelLabels(
- "DataSource",
- {"en": "Data Source", "de": "Datenquelle", "fr": "Source de données"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "connectionId": {"en": "Connection ID", "de": "Verbindungs-ID", "fr": "ID de connexion"},
- "sourceType": {"en": "Source Type", "de": "Quellentyp", "fr": "Type de source"},
- "path": {"en": "Path", "de": "Pfad", "fr": "Chemin"},
- "label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
- "displayPath": {"en": "Display path", "de": "Anzeigepfad", "fr": "Chemin affiché"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance de fonctionnalité"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
- "userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
- "autoSync": {"en": "Auto Sync", "de": "Auto-Sync", "fr": "Synchro auto"},
- "lastSynced": {"en": "Last Synced", "de": "Letzter Sync", "fr": "Dernier sync"},
- "scope": {"en": "Scope", "de": "Sichtbarkeit"},
- "neutralize": {"en": "Neutralize", "de": "Neutralisieren"},
- },
-)
-
-
class ExternalEntry(BaseModel):
"""An item (file or folder) from an external data source."""
name: str = Field(description="Item name")
diff --git a/modules/datamodels/datamodelDocref.py b/modules/datamodels/datamodelDocref.py
index b4c5924e..e4a43bd2 100644
--- a/modules/datamodels/datamodelDocref.py
+++ b/modules/datamodels/datamodelDocref.py
@@ -6,7 +6,7 @@ Document reference models for typed document references in workflows.
from typing import List, Optional
from pydantic import BaseModel, Field
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
class DocumentReference(BaseModel):
@@ -14,11 +14,19 @@ class DocumentReference(BaseModel):
pass
+@i18nModel("Dokumentlisten-Referenz")
class DocumentListReference(DocumentReference):
"""Reference to a document list via message label"""
- messageId: Optional[str] = Field(None, description="Optional message ID for cross-round references")
- label: str = Field(description="Document list label")
-
+ messageId: Optional[str] = Field(
+ None,
+ description="Optional message ID for cross-round references",
+ json_schema_extra={"label": "Nachrichten-ID"},
+ )
+ label: str = Field(
+ description="Document list label",
+ json_schema_extra={"label": "Bezeichnung"},
+ )
+
def to_string(self) -> str:
"""Convert to string format: docList:messageId:label or docList:label"""
if self.messageId:
@@ -26,11 +34,19 @@ class DocumentListReference(DocumentReference):
return f"docList:{self.label}"
+@i18nModel("Dokumentelement-Referenz")
class DocumentItemReference(DocumentReference):
"""Reference to a specific document item"""
- documentId: str = Field(description="Document ID")
- fileName: Optional[str] = Field(None, description="Optional file name")
-
+ documentId: str = Field(
+ description="Document ID",
+ json_schema_extra={"label": "Dokument-ID"},
+ )
+ fileName: Optional[str] = Field(
+ None,
+ description="Optional file name",
+ json_schema_extra={"label": "Dateiname"},
+ )
+
def to_string(self) -> str:
"""Convert to string format: docItem:documentId:fileName or docItem:documentId"""
if self.fileName:
@@ -38,21 +54,23 @@ class DocumentItemReference(DocumentReference):
return f"docItem:{self.documentId}"
+@i18nModel("Dokumentreferenz-Liste")
class DocumentReferenceList(BaseModel):
"""List of document references with conversion methods"""
references: List[DocumentReference] = Field(
default_factory=list,
- description="List of document references"
+ description="List of document references",
+ json_schema_extra={"label": "Referenzen"},
)
-
+
def to_string_list(self) -> List[str]:
"""Convert all references to string list"""
return [ref.to_string() for ref in self.references]
-
+
@classmethod
def from_string_list(cls, stringList: List[str]) -> "DocumentReferenceList":
"""Parse string list to typed references
-
+
Supports formats:
- docList:label
- docList:messageId:label
@@ -60,13 +78,13 @@ class DocumentReferenceList(BaseModel):
- docItem:documentId:fileName
"""
references = []
-
+
for refStr in stringList:
if not refStr or not isinstance(refStr, str):
continue
-
+
refStr = refStr.strip()
-
+
# Parse docList: references
if refStr.startswith("docList:"):
parts = refStr[8:].split(":", 1) # Remove "docList:" prefix
@@ -77,7 +95,7 @@ class DocumentReferenceList(BaseModel):
elif len(parts) == 1 and parts[0]:
# docList:label
references.append(DocumentListReference(label=parts[0]))
-
+
# Parse docItem: references
elif refStr.startswith("docItem:"):
parts = refStr[8:].split(":", 1) # Remove "docItem:" prefix
@@ -88,33 +106,12 @@ class DocumentReferenceList(BaseModel):
elif len(parts) == 1 and parts[0]:
# docItem:documentId
references.append(DocumentItemReference(documentId=parts[0]))
-
+
# Unknown format - skip or log warning
else:
# Try to parse as simple string (backward compatibility)
# Assume it's a label if it doesn't match known patterns
if refStr:
references.append(DocumentListReference(label=refStr))
-
+
return cls(references=references)
-
-
-registerModelLabels(
- "DocumentReference",
- {"en": "Document Reference", "fr": "Référence de document"},
- {
- "messageId": {"en": "Message ID", "fr": "ID du message"},
- "label": {"en": "Label", "fr": "Étiquette"},
- "documentId": {"en": "Document ID", "fr": "ID du document"},
- "fileName": {"en": "File Name", "fr": "Nom du fichier"},
- },
-)
-
-registerModelLabels(
- "DocumentReferenceList",
- {"en": "Document Reference List", "fr": "Liste de références de documents"},
- {
- "references": {"en": "References", "fr": "Références"},
- },
-)
-
diff --git a/modules/datamodels/datamodelFeatureDataSource.py b/modules/datamodels/datamodelFeatureDataSource.py
index 02de0a67..39d03367 100644
--- a/modules/datamodels/datamodelFeatureDataSource.py
+++ b/modules/datamodels/datamodelFeatureDataSource.py
@@ -9,54 +9,69 @@ so the agent can query structured feature data (e.g. TrusteePosition rows).
from typing import Dict, Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
+@i18nModel("Feature-Datenquelle")
class FeatureDataSource(PowerOnModel):
- """A feature-instance table attached as data source in the AI workspace."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- featureInstanceId: str = Field(description="FK to FeatureInstance")
- featureCode: str = Field(description="Feature code (e.g. trustee, commcoach)")
- tableName: str = Field(description="Table name from DATA_OBJECTS meta (e.g. TrusteePosition)")
- objectKey: str = Field(description="RBAC object key (e.g. data.feature.trustee.TrusteePosition)")
- label: str = Field(description="User-visible label")
- mandateId: str = Field(default="", description="Mandate scope")
- userId: str = Field(default="", description="Owner user ID")
- workspaceInstanceId: str = Field(description="Workspace instance where this source is used")
+ """Feature-Instanz-Tabelle als Datenquelle im AI-Workspace."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ featureInstanceId: str = Field(
+ description="FK to FeatureInstance",
+ json_schema_extra={"label": "Feature-Instanz"},
+ )
+ featureCode: str = Field(
+ description="Feature code (e.g. trustee, commcoach)",
+ json_schema_extra={"label": "Feature"},
+ )
+ tableName: str = Field(
+ description="Table name from DATA_OBJECTS meta (e.g. TrusteePosition)",
+ json_schema_extra={"label": "Tabelle"},
+ )
+ objectKey: str = Field(
+ description="RBAC object key (e.g. data.feature.trustee.TrusteePosition)",
+ json_schema_extra={"label": "Objekt-Schluessel"},
+ )
+ label: str = Field(
+ description="User-visible label",
+ json_schema_extra={"label": "Bezeichnung"},
+ )
+ mandateId: str = Field(
+ default="",
+ description="Mandate scope",
+ json_schema_extra={"label": "Mandant"},
+ )
+ userId: str = Field(
+ default="",
+ description="Owner user ID",
+ json_schema_extra={"label": "Benutzer"},
+ )
+ workspaceInstanceId: str = Field(
+ description="Workspace instance where this source is used",
+ json_schema_extra={"label": "Workspace"},
+ )
scope: str = Field(
default="personal",
description="Data visibility scope: personal, featureInstance, mandate, global",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
{"value": "global", "label": {"en": "Global", "de": "Global"}},
- ]}
+ ]},
)
neutralize: bool = Field(
default=False,
description="Whether this data source should be neutralized before AI processing",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Neutralisieren", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
)
recordFilter: Optional[Dict[str, str]] = Field(
default=None,
description="Record-level filter applied when querying this table, e.g. {'sessionId': 'abc-123'}",
+ json_schema_extra={"label": "Datensatzfilter"},
)
-
-
-registerModelLabels(
- "FeatureDataSource",
- {"en": "Feature Data Source", "de": "Feature-Datenquelle", "fr": "Source de données fonctionnalité"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- "featureCode": {"en": "Feature", "de": "Feature", "fr": "Fonctionnalité"},
- "tableName": {"en": "Table", "de": "Tabelle", "fr": "Table"},
- "objectKey": {"en": "Object Key", "de": "Objekt-Schlüssel", "fr": "Clé objet"},
- "label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
- "mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
- "userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
- "workspaceInstanceId": {"en": "Workspace", "de": "Workspace", "fr": "Espace de travail"},
- },
-)
diff --git a/modules/datamodels/datamodelFeatures.py b/modules/datamodels/datamodelFeatures.py
index 3134a18e..93a7fae9 100644
--- a/modules/datamodels/datamodelFeatures.py
+++ b/modules/datamodels/datamodelFeatures.py
@@ -6,85 +6,56 @@ import uuid
from typing import Optional, Dict, Any
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.datamodels.datamodelUtils import TextMultilingual
+@i18nModel("Feature")
class Feature(PowerOnModel):
- """
- Feature-Definition (global, z.B. 'trustee', 'chatbot').
- Features sind die verfügbaren Funktionalitäten der Plattform.
- """
+ """Feature-Definition (global, z.B. 'trustee', 'chatbot'). Verfuegbare Funktionalitaeten der Plattform."""
code: str = Field(
description="Unique feature code (Primary Key), z.B. 'trustee', 'chatbot'",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Code", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
)
label: TextMultilingual = Field(
description="Feature label in multiple languages (I18n)",
- json_schema_extra={"frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Bezeichnung", "frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
)
icon: str = Field(
default="",
description="Icon identifier for the feature",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Symbol", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
)
-registerModelLabels(
- "Feature",
- {"en": "Feature", "de": "Feature", "fr": "Fonctionnalité"},
- {
- "code": {"en": "Code", "de": "Code", "fr": "Code"},
- "label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
- "icon": {"en": "Icon", "de": "Symbol", "fr": "Icône"},
- },
-)
-
-
+@i18nModel("Feature-Instanz")
class FeatureInstance(PowerOnModel):
- """
- Instanz eines Features in einem Mandanten.
- Ein Mandant kann mehrere Instanzen desselben Features haben.
- """
+ """Instanz eines Features in einem Mandanten. Ein Mandant kann mehrere Instanzen desselben Features haben."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the feature instance",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
featureCode: str = Field(
- description="FK → Feature.code",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": True}
+ description="FK -> Feature.code",
+ json_schema_extra={"label": "Feature", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True}
)
mandateId: str = Field(
- description="FK → Mandate.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ description="FK -> Mandate.id (CASCADE DELETE)",
+ json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
label: str = Field(
default="",
description="Instance label, z.B. 'Buchhaltung 2025'",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Bezeichnung", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
)
enabled: bool = Field(
default=True,
description="Whether this feature instance is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
)
config: Optional[Dict[str, Any]] = Field(
default=None,
description="Instance-specific configuration (JSONB). Structure depends on featureCode.",
- json_schema_extra={"frontend_type": "json", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Konfiguration", "frontend_type": "json", "frontend_readonly": False, "frontend_required": False}
)
-
-
-registerModelLabels(
- "FeatureInstance",
- {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance de fonctionnalité"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "featureCode": {"en": "Feature", "de": "Feature", "fr": "Fonctionnalité"},
- "mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
- "label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
- "enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
- "config": {"en": "Configuration", "de": "Konfiguration", "fr": "Configuration"},
- },
-)
diff --git a/modules/datamodels/datamodelFileFolder.py b/modules/datamodels/datamodelFileFolder.py
index 23cd197b..73222e51 100644
--- a/modules/datamodels/datamodelFileFolder.py
+++ b/modules/datamodels/datamodelFileFolder.py
@@ -5,26 +5,34 @@
from typing import Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
+@i18nModel("Dateiordner")
class FileFolder(PowerOnModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- name: str = Field(description="Folder name", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
- parentId: Optional[str] = Field(default=None, description="Parent folder ID (null = root)", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
- mandateId: Optional[str] = Field(default=None, description="Mandate context", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- featureInstanceId: Optional[str] = Field(default=None, description="Feature instance context", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
-
-
-registerModelLabels(
- "FileFolder",
- {"en": "File Folder", "fr": "Dossier de fichiers"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "name": {"en": "Name", "fr": "Nom"},
- "parentId": {"en": "Parent Folder", "fr": "Dossier parent"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
- },
-)
+ """Hierarchischer Ordner fuer die Dateiverwaltung."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ name: str = Field(
+ description="Folder name",
+ json_schema_extra={"label": "Name", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
+ )
+ parentId: Optional[str] = Field(
+ default=None,
+ description="Parent folder ID (null = root)",
+ json_schema_extra={"label": "Uebergeordneter Ordner", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
+ )
+ mandateId: Optional[str] = Field(
+ default=None,
+ description="Mandate context",
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ featureInstanceId: Optional[str] = Field(
+ default=None,
+ description="Feature instance context",
+ json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
diff --git a/modules/datamodels/datamodelFiles.py b/modules/datamodels/datamodelFiles.py
index b8a44d2c..333120d1 100644
--- a/modules/datamodels/datamodelFiles.py
+++ b/modules/datamodels/datamodelFiles.py
@@ -5,66 +5,110 @@
from typing import Dict, Any, List, Optional, Union
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
import base64
+@i18nModel("Datei")
class FileItem(PowerOnModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- mandateId: Optional[str] = Field(default="", description="ID of the mandate this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- featureInstanceId: Optional[str] = Field(default="", description="ID of the feature instance this file belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_fk_source": "/api/features/instances", "frontend_fk_display_field": "label"})
- fileName: str = Field(description="Name of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
- mimeType: str = Field(description="MIME type of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- fileHash: str = Field(description="Hash of the file", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- fileSize: int = Field(description="Size of the file in bytes", json_schema_extra={"frontend_type": "integer", "frontend_readonly": True, "frontend_required": False})
- tags: Optional[List[str]] = Field(default=None, description="Tags for categorization and search", json_schema_extra={"frontend_type": "tags", "frontend_readonly": False, "frontend_required": False})
- folderId: Optional[str] = Field(default=None, description="ID of the parent folder", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
- description: Optional[str] = Field(default=None, description="User-provided description of the file", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False})
- status: Optional[str] = Field(default=None, description="Processing status: pending, extracted, embedding, indexed, failed", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
+ """Metadaten einer gespeicherten Datei."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ mandateId: Optional[str] = Field(
+ default="",
+ description="ID of the mandate this file belongs to",
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ featureInstanceId: Optional[str] = Field(
+ default="",
+ description="ID of the feature instance this file belongs to",
+ json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "frontend_fk_source": "/api/features/instances", "frontend_fk_display_field": "label"},
+ )
+ fileName: str = Field(
+ description="Name of the file",
+ json_schema_extra={"label": "Dateiname", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
+ )
+ mimeType: str = Field(
+ description="MIME type of the file",
+ json_schema_extra={"label": "MIME-Typ", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ fileHash: str = Field(
+ description="Hash of the file",
+ json_schema_extra={"label": "Datei-Hash", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ fileSize: int = Field(
+ description="Size of the file in bytes",
+ json_schema_extra={"label": "Dateigroesse", "frontend_type": "integer", "frontend_readonly": True, "frontend_required": False},
+ )
+ tags: Optional[List[str]] = Field(
+ default=None,
+ description="Tags for categorization and search",
+ json_schema_extra={"label": "Tags", "frontend_type": "tags", "frontend_readonly": False, "frontend_required": False},
+ )
+ folderId: Optional[str] = Field(
+ default=None,
+ description="ID of the parent folder",
+ json_schema_extra={"label": "Ordner-ID", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
+ )
+ description: Optional[str] = Field(
+ default=None,
+ description="User-provided description of the file",
+ json_schema_extra={"label": "Beschreibung", "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False},
+ )
+ status: Optional[str] = Field(
+ default=None,
+ description="Processing status: pending, extracted, embedding, indexed, failed",
+ json_schema_extra={"label": "Status", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
scope: str = Field(
default="personal",
description="Data visibility scope: personal, featureInstance, mandate, global",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
{"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
{"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
{"value": "global", "label": {"en": "Global", "de": "Global"}},
- ]}
+ ]},
)
neutralize: bool = Field(
default=False,
description="Whether this file should be neutralized before AI processing",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Neutralisieren", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
)
-registerModelLabels(
- "FileItem",
- {"en": "File Item", "fr": "Élément de fichier"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité"},
- "fileName": {"en": "fileName", "fr": "Nom de fichier"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- "fileHash": {"en": "File Hash", "fr": "Hash du fichier"},
- "fileSize": {"en": "File Size", "fr": "Taille du fichier"},
- "tags": {"en": "Tags", "fr": "Tags"},
- "folderId": {"en": "Folder ID", "fr": "ID du dossier"},
- "description": {"en": "Description", "fr": "Description"},
- "status": {"en": "Status", "fr": "Statut"},
- "scope": {"en": "Scope", "de": "Sichtbarkeit"},
- "neutralize": {"en": "Neutralize", "de": "Neutralisieren"},
- },
-)
+@i18nModel("Datei-Vorschau")
class FilePreview(BaseModel):
- content: Union[str, bytes] = Field(description="File content (text or binary)")
- mimeType: str = Field(description="MIME type of the file")
- fileName: str = Field(description="Original fileName")
- isText: bool = Field(description="Whether the content is text (True) or binary (False)")
- encoding: Optional[str] = Field(None, description="Text encoding if content is text")
- size: int = Field(description="Size of the content in bytes")
+ """Vorschau-Inhalt einer Datei fuer die Anzeige."""
+ content: Union[str, bytes] = Field(
+ description="File content (text or binary)",
+ json_schema_extra={"label": "Inhalt"},
+ )
+ mimeType: str = Field(
+ description="MIME type of the file",
+ json_schema_extra={"label": "MIME-Typ"},
+ )
+ fileName: str = Field(
+ description="Original fileName",
+ json_schema_extra={"label": "Dateiname"},
+ )
+ isText: bool = Field(
+ description="Whether the content is text (True) or binary (False)",
+ json_schema_extra={"label": "Ist Text"},
+ )
+ encoding: Optional[str] = Field(
+ None,
+ description="Text encoding if content is text",
+ json_schema_extra={"label": "Kodierung"},
+ )
+ size: int = Field(
+ description="Size of the content in bytes",
+ json_schema_extra={"label": "Groesse"},
+ )
def toDictWithBase64Encoding(self) -> Dict[str, Any]:
"""Convert to dictionary with base64 encoding for binary content."""
@@ -72,29 +116,21 @@ class FilePreview(BaseModel):
if isinstance(data.get("content"), bytes):
data["content"] = base64.b64encode(data["content"]).decode("utf-8")
return data
-registerModelLabels(
- "FilePreview",
- {"en": "File Preview", "fr": "Aperçu du fichier"},
- {
- "content": {"en": "Content", "fr": "Contenu"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- "fileName": {"en": "fileName", "fr": "Nom de fichier"},
- "isText": {"en": "Is Text", "fr": "Est du texte"},
- "encoding": {"en": "Encoding", "fr": "Encodage"},
- "size": {"en": "Size", "fr": "Taille"},
- },
-)
+
+@i18nModel("Dateidaten")
class FileData(PowerOnModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- data: str = Field(description="File data content")
- base64Encoded: bool = Field(description="Whether the data is base64 encoded")
-registerModelLabels(
- "FileData",
- {"en": "File Data", "fr": "Données de fichier"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "data": {"en": "Data", "fr": "Données"},
- "base64Encoded": {"en": "Base64 Encoded", "fr": "Encodé en Base64"},
- },
-)
+ """Rohdaten einer Datei (z.B. Base64)."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ data: str = Field(
+ description="File data content",
+ json_schema_extra={"label": "Daten"},
+ )
+ base64Encoded: bool = Field(
+ description="Whether the data is base64 encoded",
+ json_schema_extra={"label": "Base64-kodiert"},
+ )
diff --git a/modules/datamodels/datamodelInvitation.py b/modules/datamodels/datamodelInvitation.py
index 709e5021..4808bd55 100644
--- a/modules/datamodels/datamodelInvitation.py
+++ b/modules/datamodels/datamodelInvitation.py
@@ -10,9 +10,10 @@ import secrets
from typing import Optional, List
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
+@i18nModel("Einladung")
class Invitation(PowerOnModel):
"""
Einladungs-Token für neue User.
@@ -21,103 +22,76 @@ class Invitation(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the invitation",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
token: str = Field(
default_factory=lambda: secrets.token_urlsafe(32),
description="Secure invitation token",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Token", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
- # Ziel der Einladung
+
mandateId: str = Field(
description="FK → Mandate.id - Target mandate for the invitation",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Mandant", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
featureInstanceId: Optional[str] = Field(
default=None,
description="Optional FK → FeatureInstance.id - Direct access to specific feature",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Feature-Instanz", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
roleIds: List[str] = Field(
default_factory=list,
description="List of Role IDs to assign to the invited user",
- json_schema_extra={"frontend_type": "multiselect", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Rollen", "frontend_type": "multiselect", "frontend_readonly": False, "frontend_required": True}
)
-
- # Einladungs-Details
+
targetUsername: Optional[str] = Field(
default=None,
description="Username of the invited user (must match on acceptance)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Ziel-Benutzername", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
)
email: Optional[str] = Field(
default=None,
description="Email address to send invitation link (optional)",
- json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "E-Mail (optional)", "frontend_type": "email", "frontend_readonly": False, "frontend_required": False}
)
expiresAt: float = Field(
description="When the invitation expires (UTC timestamp)",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Gueltig bis", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": True}
)
-
- # Status
+
usedBy: Optional[str] = Field(
default=None,
description="User ID of the person who used the invitation",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Verwendet von", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
usedAt: Optional[float] = Field(
default=None,
description="When the invitation was used (UTC timestamp)",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Verwendet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
)
revokedAt: Optional[float] = Field(
default=None,
description="When the invitation was revoked (UTC timestamp)",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Widerrufen am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
)
-
- # Email-Status
+
emailSent: Optional[bool] = Field(
default=False,
description="Whether the invitation email was successfully sent",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "E-Mail gesendet", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
)
-
- # Einschränkungen
+
maxUses: int = Field(
default=1,
ge=1,
le=100,
description="Maximum number of times this invitation can be used",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Max. Verwendungen", "frontend_type": "number", "frontend_readonly": False, "frontend_required": False}
)
currentUses: int = Field(
default=0,
ge=0,
description="Current number of times this invitation has been used",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Aktuelle Verwendungen", "frontend_type": "number", "frontend_readonly": True, "frontend_required": False}
)
-
-
-registerModelLabels(
- "Invitation",
- {"en": "Invitation", "de": "Einladung", "fr": "Invitation"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "token": {"en": "Token", "de": "Token", "fr": "Jeton"},
- "mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- "roleIds": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
- "targetUsername": {"en": "Target Username", "de": "Ziel-Benutzername", "fr": "Nom d'utilisateur cible"},
- "email": {"en": "Email (optional)", "de": "E-Mail (optional)", "fr": "Email (optionnel)"},
- "expiresAt": {"en": "Expires At", "de": "Gültig bis", "fr": "Expire le"},
- "usedBy": {"en": "Used By", "de": "Verwendet von", "fr": "Utilisé par"},
- "usedAt": {"en": "Used At", "de": "Verwendet am", "fr": "Utilisé le"},
- "revokedAt": {"en": "Revoked At", "de": "Widerrufen am", "fr": "Révoqué le"},
- "emailSent": {"en": "Email Sent", "de": "E-Mail gesendet", "fr": "Email envoyé"},
- "maxUses": {"en": "Max Uses", "de": "Max. Verwendungen", "fr": "Utilisations max"},
- "currentUses": {"en": "Current Uses", "de": "Aktuelle Verwendungen", "fr": "Utilisations actuelles"},
- },
-)
diff --git a/modules/datamodels/datamodelKnowledge.py b/modules/datamodels/datamodelKnowledge.py
index 7ac12c15..7432a30c 100644
--- a/modules/datamodels/datamodelKnowledge.py
+++ b/modules/datamodels/datamodelKnowledge.py
@@ -15,173 +15,231 @@ Vector fields use json_schema_extra={"db_type": "vector(1536)"} for pgvector.
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.shared.timeUtils import getUtcTimestamp
import uuid
+@i18nModel("Datei-Inhaltsindex")
class FileContentIndex(PowerOnModel):
- """Structural index of a file's content objects. Created without AI.
- Scope is mirrored from FileItem (poweron_management) at indexing time."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key (typically = fileId)")
- userId: str = Field(description="Owner user ID")
- featureInstanceId: str = Field(default="", description="Feature instance scope")
- mandateId: str = Field(default="", description="Mandate scope")
- fileName: str = Field(description="Original file name")
- mimeType: str = Field(description="MIME type of the file")
- containerPath: Optional[str] = Field(default=None, description="Path within a container (e.g. 'archive.zip/folder/report.pdf')")
- totalObjects: int = Field(default=0, description="Total number of content objects extracted")
- totalSize: int = Field(default=0, description="Total size of all content objects in bytes")
- structure: Dict[str, Any] = Field(default_factory=dict, description="Structural overview (pages, sections, hierarchy)")
- objectSummary: List[Dict[str, Any]] = Field(default_factory=list, description="Compact summary per content object")
- extractedAt: float = Field(default_factory=getUtcTimestamp, description="Extraction timestamp")
- status: str = Field(default="pending", description="Processing status: pending, extracted, embedding, indexed, failed")
+ """Struktureller Index der Inhaltsobjekte einer Datei."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key (typically = fileId)",
+ json_schema_extra={"label": "ID"},
+ )
+ userId: str = Field(
+ description="Owner user ID",
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ featureInstanceId: str = Field(
+ default="",
+ description="Feature instance scope",
+ json_schema_extra={"label": "Feature-Instanz-ID"},
+ )
+ mandateId: str = Field(
+ default="",
+ description="Mandate scope",
+ json_schema_extra={"label": "Mandanten-ID"},
+ )
+ fileName: str = Field(
+ description="Original file name",
+ json_schema_extra={"label": "Dateiname"},
+ )
+ mimeType: str = Field(
+ description="MIME type of the file",
+ json_schema_extra={"label": "MIME-Typ"},
+ )
+ containerPath: Optional[str] = Field(
+ default=None,
+ description="Path within a container (e.g. 'archive.zip/folder/report.pdf')",
+ json_schema_extra={"label": "Container-Pfad"},
+ )
+ totalObjects: int = Field(
+ default=0,
+ description="Total number of content objects extracted",
+ json_schema_extra={"label": "Anzahl Objekte"},
+ )
+ totalSize: int = Field(
+ default=0,
+ description="Total size of all content objects in bytes",
+ json_schema_extra={"label": "Gesamtgroesse"},
+ )
+ structure: Dict[str, Any] = Field(
+ default_factory=dict,
+ description="Structural overview (pages, sections, hierarchy)",
+ json_schema_extra={"label": "Struktur"},
+ )
+ objectSummary: List[Dict[str, Any]] = Field(
+ default_factory=list,
+ description="Compact summary per content object",
+ json_schema_extra={"label": "Objekt-Zusammenfassung"},
+ )
+ extractedAt: float = Field(
+ default_factory=getUtcTimestamp,
+ description="Extraction timestamp",
+ json_schema_extra={"label": "Extrahiert am"},
+ )
+ status: str = Field(
+ default="pending",
+ description="Processing status: pending, extracted, embedding, indexed, failed",
+ json_schema_extra={"label": "Status"},
+ )
scope: str = Field(
default="personal",
description="Data visibility scope: personal, featureInstance, mandate, global",
+ json_schema_extra={"label": "Sichtbarkeit"},
)
neutralizationStatus: Optional[str] = Field(
default=None,
description="Neutralization status: completed, failed, skipped, None = not required",
+ json_schema_extra={"label": "Neutralisierungsstatus"},
)
isNeutralized: bool = Field(
default=False,
description="True if content was neutralized before indexing",
+ json_schema_extra={"label": "Neutralisiert"},
)
-registerModelLabels(
- "FileContentIndex",
- {"en": "File Content Index", "fr": "Index du contenu de fichier"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "fileName": {"en": "File Name", "fr": "Nom de fichier"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- "containerPath": {"en": "Container Path", "fr": "Chemin du conteneur"},
- "totalObjects": {"en": "Total Objects", "fr": "Nombre total d'objets"},
- "totalSize": {"en": "Total Size", "fr": "Taille totale"},
- "structure": {"en": "Structure", "fr": "Structure"},
- "objectSummary": {"en": "Object Summary", "fr": "Résumé des objets"},
- "extractedAt": {"en": "Extracted At", "fr": "Extrait le"},
- "status": {"en": "Status", "fr": "Statut"},
- "scope": {"en": "Scope", "de": "Sichtbarkeit"},
- "neutralizationStatus": {"en": "Neutralization Status", "de": "Neutralisierungsstatus"},
- "isNeutralized": {"en": "Is Neutralized", "de": "Neutralisiert"},
- },
-)
-
-
+@i18nModel("Inhalts-Chunk")
class ContentChunk(PowerOnModel):
- """Persisted content chunk with embedding vector. Reusable across workflows.
- Scalar content object (or chunk thereof) with pgvector embedding."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- contentObjectId: str = Field(description="Reference to the content object within FileContentIndex")
- fileId: str = Field(description="FK to the source file")
- userId: str = Field(description="Owner user ID")
- featureInstanceId: str = Field(default="", description="Feature instance scope")
- contentType: str = Field(description="Content type: text, image, videostream, audiostream, other")
- data: str = Field(description="Content data (text, base64, URL)")
- contextRef: Dict[str, Any] = Field(default_factory=dict, description="Context reference (page, position, label)")
- summary: Optional[str] = Field(default=None, description="AI-generated summary (on demand)")
- chunkMetadata: Dict[str, Any] = Field(default_factory=dict, description="Additional metadata")
+ """Persistierter Inhalts-Chunk mit Embedding-Vektor."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ contentObjectId: str = Field(
+ description="Reference to the content object within FileContentIndex",
+ json_schema_extra={"label": "Inhaltsobjekt-ID"},
+ )
+ fileId: str = Field(
+ description="FK to the source file",
+ json_schema_extra={"label": "Datei-ID"},
+ )
+ userId: str = Field(
+ description="Owner user ID",
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ featureInstanceId: str = Field(
+ default="",
+ description="Feature instance scope",
+ json_schema_extra={"label": "Feature-Instanz-ID"},
+ )
+ contentType: str = Field(
+ description="Content type: text, image, videostream, audiostream, other",
+ json_schema_extra={"label": "Inhaltstyp"},
+ )
+ data: str = Field(
+ description="Content data (text, base64, URL)",
+ json_schema_extra={"label": "Daten"},
+ )
+ contextRef: Dict[str, Any] = Field(
+ default_factory=dict,
+ description="Context reference (page, position, label)",
+ json_schema_extra={"label": "Kontext-Referenz"},
+ )
+ summary: Optional[str] = Field(
+ default=None,
+ description="AI-generated summary (on demand)",
+ json_schema_extra={"label": "Zusammenfassung"},
+ )
+ chunkMetadata: Dict[str, Any] = Field(
+ default_factory=dict,
+ description="Additional metadata",
+ json_schema_extra={"label": "Metadaten"},
+ )
embedding: Optional[List[float]] = Field(
- default=None, description="pgvector embedding (NOT NULL for text chunks)",
- json_schema_extra={"db_type": "vector(1536)"}
+ default=None,
+ description="pgvector embedding (NOT NULL for text chunks)",
+ json_schema_extra={"label": "Embedding", "db_type": "vector(1536)"},
)
-registerModelLabels(
- "ContentChunk",
- {"en": "Content Chunk", "fr": "Fragment de contenu"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "contentObjectId": {"en": "Content Object ID", "fr": "ID de l'objet de contenu"},
- "fileId": {"en": "File ID", "fr": "ID du fichier"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
- "contentType": {"en": "Content Type", "fr": "Type de contenu"},
- "data": {"en": "Data", "fr": "Données"},
- "contextRef": {"en": "Context Reference", "fr": "Référence contextuelle"},
- "summary": {"en": "Summary", "fr": "Résumé"},
- "chunkMetadata": {"en": "Metadata", "fr": "Métadonnées"},
- "embedding": {"en": "Embedding", "fr": "Vecteur d'embedding"},
- },
-)
-
-
+@i18nModel("Runden-Speicher")
class RoundMemory(PowerOnModel):
- """Persistent per-round memory for agent tool results, file refs, and decisions.
-
- Stored after each agent round so that RAG can retrieve relevant context
- even after the ConversationManager summarises older messages away.
- """
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- workflowId: str = Field(description="FK to the workflow")
- roundNumber: int = Field(default=0, description="Agent round that produced this memory")
- memoryType: str = Field(
- description="Category: file_ref, tool_result, decision, data_source_ref"
+ """Persistenter Speicher pro Agenten-Runde."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ workflowId: str = Field(
+ description="FK to the workflow",
+ json_schema_extra={"label": "Workflow-ID"},
+ )
+ roundNumber: int = Field(
+ default=0,
+ description="Agent round that produced this memory",
+ json_schema_extra={"label": "Rundennummer"},
+ )
+ memoryType: str = Field(
+ description="Category: file_ref, tool_result, decision, data_source_ref",
+ json_schema_extra={"label": "Speichertyp"},
+ )
+ key: str = Field(
+ description="Dedup key, e.g. 'readFile:' or 'plan'",
+ json_schema_extra={"label": "Schluessel"},
+ )
+ summary: str = Field(
+ default="",
+ description="Compact summary (max ~2000 chars)",
+ json_schema_extra={"label": "Zusammenfassung"},
)
- key: str = Field(description="Dedup key, e.g. 'readFile:' or 'plan'")
- summary: str = Field(default="", description="Compact summary (max ~2000 chars)")
fullData: Optional[str] = Field(
default=None,
description="Full tool output when small enough (max ~8000 chars)",
+ json_schema_extra={"label": "Volldaten"},
+ )
+ fileIds: List[str] = Field(
+ default_factory=list,
+ description="Referenced file IDs",
+ json_schema_extra={"label": "Datei-IDs"},
)
- fileIds: List[str] = Field(default_factory=list, description="Referenced file IDs")
embedding: Optional[List[float]] = Field(
default=None,
description="Embedding of summary for semantic retrieval",
- json_schema_extra={"db_type": "vector(1536)"},
+ json_schema_extra={"label": "Embedding", "db_type": "vector(1536)"},
)
-registerModelLabels(
- "RoundMemory",
- {"en": "Round Memory", "fr": "Mémoire de tour"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
- "roundNumber": {"en": "Round Number", "fr": "Numéro de tour"},
- "memoryType": {"en": "Memory Type", "fr": "Type de mémoire"},
- "key": {"en": "Key", "fr": "Clé"},
- "summary": {"en": "Summary", "fr": "Résumé"},
- "fullData": {"en": "Full Data", "fr": "Données complètes"},
- "fileIds": {"en": "File IDs", "fr": "IDs de fichier"},
- "embedding": {"en": "Embedding", "fr": "Vecteur d'embedding"},
- },
-)
-
-
+@i18nModel("Workflow-Speicher")
class WorkflowMemory(PowerOnModel):
- """Workflow-scoped key-value cache for entities and facts.
- Extracted during agent rounds, persisted for cross-round and cross-workflow reuse."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- workflowId: str = Field(description="FK to the workflow")
- userId: str = Field(description="Owner user ID")
- featureInstanceId: str = Field(default="", description="Feature instance scope")
- key: str = Field(description="Key identifier (e.g. 'entity:companyName')")
- value: str = Field(description="Extracted value")
- source: str = Field(default="extraction", description="Origin: extraction, tool, conversation, summary")
- embedding: Optional[List[float]] = Field(
- default=None, description="Optional embedding for semantic lookup",
- json_schema_extra={"db_type": "vector(1536)"}
+ """Workflow-spezifischer Key-Value-Cache fuer Entitaeten und Fakten."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ workflowId: str = Field(
+ description="FK to the workflow",
+ json_schema_extra={"label": "Workflow-ID"},
+ )
+ userId: str = Field(
+ description="Owner user ID",
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ featureInstanceId: str = Field(
+ default="",
+ description="Feature instance scope",
+ json_schema_extra={"label": "Feature-Instanz-ID"},
+ )
+ key: str = Field(
+ description="Key identifier (e.g. 'entity:companyName')",
+ json_schema_extra={"label": "Schluessel"},
+ )
+ value: str = Field(
+ description="Extracted value",
+ json_schema_extra={"label": "Wert"},
+ )
+ source: str = Field(
+ default="extraction",
+ description="Origin: extraction, tool, conversation, summary",
+ json_schema_extra={"label": "Quelle"},
+ )
+ embedding: Optional[List[float]] = Field(
+ default=None,
+ description="Optional embedding for semantic lookup",
+ json_schema_extra={"label": "Embedding", "db_type": "vector(1536)"},
)
-
-
-registerModelLabels(
- "WorkflowMemory",
- {"en": "Workflow Memory", "fr": "Mémoire de workflow"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "workflowId": {"en": "Workflow ID", "fr": "ID du workflow"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance"},
- "key": {"en": "Key", "fr": "Clé"},
- "value": {"en": "Value", "fr": "Valeur"},
- "source": {"en": "Source", "fr": "Source"},
- "embedding": {"en": "Embedding", "fr": "Vecteur d'embedding"},
- },
-)
diff --git a/modules/datamodels/datamodelMembership.py b/modules/datamodels/datamodelMembership.py
index ce753d15..0cf8468f 100644
--- a/modules/datamodels/datamodelMembership.py
+++ b/modules/datamodels/datamodelMembership.py
@@ -10,9 +10,10 @@ Rollen werden über Junction Tables verknüpft für saubere CASCADE DELETE.
import uuid
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
+@i18nModel("Benutzer-Mandant")
class UserMandate(PowerOnModel):
"""
User-Mitgliedschaft in einem Mandanten.
@@ -21,36 +22,24 @@ class UserMandate(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the user-mandate membership",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
userId: str = Field(
description="FK → User.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
+ json_schema_extra={"label": "Benutzer", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
)
mandateId: str = Field(
description="FK → Mandate.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
+ json_schema_extra={"label": "Mandant", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
)
enabled: bool = Field(
default=True,
description="Whether this membership is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
)
- # Rollen werden via Junction Table UserMandateRole verknüpft
-
-
-registerModelLabels(
- "UserMandate",
- {"en": "User Mandate", "de": "Benutzer-Mandant", "fr": "Mandat utilisateur"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
- "mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
- "enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
- },
-)
+@i18nModel("Feature-Zugang")
class FeatureAccess(PowerOnModel):
"""
User-Zugriff auf eine Feature-Instanz.
@@ -59,36 +48,24 @@ class FeatureAccess(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the feature access",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
userId: str = Field(
description="FK → User.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
+ json_schema_extra={"label": "Benutzer", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/users/", "frontend_fk_display_field": "username"}
)
featureInstanceId: str = Field(
description="FK → FeatureInstance.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
+ json_schema_extra={"label": "Feature-Instanz", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
)
enabled: bool = Field(
default=True,
description="Whether this feature access is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
)
- # Rollen werden via Junction Table FeatureAccessRole verknüpft
-
-
-registerModelLabels(
- "FeatureAccess",
- {"en": "Feature Access", "de": "Feature-Zugang", "fr": "Accès fonctionnalité"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- "enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
- },
-)
+@i18nModel("Benutzer-Mandant-Rolle")
class UserMandateRole(PowerOnModel):
"""
Junction Table: UserMandate zu Role.
@@ -97,29 +74,19 @@ class UserMandateRole(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the junction record",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
userMandateId: str = Field(
description="FK → UserMandate.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/user-mandates/", "frontend_fk_display_field": "userId"}
+ json_schema_extra={"label": "Benutzer-Mandant", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/user-mandates/", "frontend_fk_display_field": "userId"}
)
roleId: str = Field(
description="FK → Role.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
+ json_schema_extra={"label": "Rolle", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
)
-registerModelLabels(
- "UserMandateRole",
- {"en": "User Mandate Role", "de": "Benutzer-Mandant-Rolle", "fr": "Rôle mandat utilisateur"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userMandateId": {"en": "User Mandate", "de": "Benutzer-Mandant", "fr": "Mandat utilisateur"},
- "roleId": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
- },
-)
-
-
+@i18nModel("Feature-Zugang-Rolle")
class FeatureAccessRole(PowerOnModel):
"""
Junction Table: FeatureAccess zu Role.
@@ -128,24 +95,13 @@ class FeatureAccessRole(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the junction record",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
featureAccessId: str = Field(
description="FK → FeatureAccess.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-access/", "frontend_fk_display_field": "userId"}
+ json_schema_extra={"label": "Feature-Zugang", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/feature-access/", "frontend_fk_display_field": "userId"}
)
roleId: str = Field(
description="FK → Role.id (CASCADE DELETE)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
+ json_schema_extra={"label": "Rolle", "frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
)
-
-
-registerModelLabels(
- "FeatureAccessRole",
- {"en": "Feature Access Role", "de": "Feature-Zugang-Rolle", "fr": "Rôle accès fonctionnalité"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "featureAccessId": {"en": "Feature Access", "de": "Feature-Zugang", "fr": "Accès fonctionnalité"},
- "roleId": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
- },
-)
diff --git a/modules/datamodels/datamodelMessaging.py b/modules/datamodels/datamodelMessaging.py
index ebacc9d4..d7671da1 100644
--- a/modules/datamodels/datamodelMessaging.py
+++ b/modules/datamodels/datamodelMessaging.py
@@ -7,7 +7,7 @@ from typing import Optional
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
class MessagingChannel(str, Enum):
@@ -26,86 +26,137 @@ class DeliveryStatus(str, Enum):
FAILED = "failed"
+@i18nModel("Messaging-Abonnement")
class MessagingSubscription(PowerOnModel):
"""Data model for messaging subscriptions"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the subscription",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "ID",
+ },
)
subscriptionId: str = Field(
description="Unique subscription identifier (e.g., 'system_errors', 'audit_login')",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": False,
+ "frontend_required": True,
+ "label": "Abonnement-ID",
+ },
)
subscriptionLabel: str = Field(
description="Display name of the subscription",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": False,
+ "frontend_required": True,
+ "label": "Bezeichnung",
+ },
)
mandateId: str = Field(
description="ID of the mandate this subscription belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Mandanten-ID",
+ },
)
featureInstanceId: str = Field(
description="ID of the feature instance this subscription belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Feature-Instanz-ID",
+ },
)
description: Optional[str] = Field(
default=None,
description="Description of the subscription",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "textarea",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "label": "Beschreibung",
+ },
)
isSystemSubscription: bool = Field(
default=False,
description="Whether this is a system subscription (only admin can create)",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "checkbox",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "System-Abonnement",
+ },
)
enabled: bool = Field(
default=True,
description="Whether the subscription is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "checkbox",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "label": "Aktiviert",
+ },
)
model_config = ConfigDict(use_enum_values=True)
-registerModelLabels(
- "MessagingSubscription",
- {"en": "Messaging Subscription", "fr": "Abonnement de messagerie"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
- "subscriptionLabel": {"en": "Subscription Label", "fr": "Label d'abonnement"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
- "description": {"en": "Description", "fr": "Description"},
- "isSystemSubscription": {"en": "System Subscription", "fr": "Abonnement système"},
- "enabled": {"en": "Enabled", "fr": "Activé"},
- },
-)
-
-
+@i18nModel("Messaging-Registrierung")
class MessagingSubscriptionRegistration(BaseModel):
"""Data model for user registrations to messaging subscriptions"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the registration",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "ID",
+ },
)
mandateId: str = Field(
description="ID of the mandate this registration belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Mandanten-ID",
+ },
)
featureInstanceId: str = Field(
description="ID of the feature instance this registration belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Feature-Instanz-ID",
+ },
)
subscriptionId: str = Field(
description="ID of the subscription this registration belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": False,
+ "frontend_required": True,
+ "label": "Abonnement-ID",
+ },
)
userId: str = Field(
description="ID of the user registered to this subscription",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Benutzer-ID",
+ },
)
channel: MessagingChannel = Field(
description="Channel type for this registration",
@@ -117,62 +168,83 @@ class MessagingSubscriptionRegistration(BaseModel):
{"value": "email", "label": {"en": "Email", "fr": "Email"}},
{"value": "sms", "label": {"en": "SMS", "fr": "SMS"}},
{"value": "whatsapp", "label": {"en": "WhatsApp", "fr": "WhatsApp"}},
- {"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}}
- ]
- }
+ {"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}},
+ ],
+ "label": "Kanal",
+ },
)
channelConfig: str = Field(
default="",
description="Channel-specific configuration (e.g., email address, phone number, Teams user ID)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "label": "Kanal-Konfiguration",
+ },
)
enabled: bool = Field(
default=True,
description="Whether this registration is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "checkbox",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "label": "Aktiviert",
+ },
)
model_config = ConfigDict(use_enum_values=True)
-registerModelLabels(
- "MessagingSubscriptionRegistration",
- {"en": "Messaging Registration", "fr": "Inscription à la messagerie"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
- "subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "channel": {"en": "Channel", "fr": "Canal"},
- "channelConfig": {"en": "Channel Config", "fr": "Configuration du canal"},
- "enabled": {"en": "Enabled", "fr": "Activé"},
- },
-)
-
-
+@i18nModel("Messaging-Zustellung")
class MessagingDelivery(BaseModel):
"""Data model for individual message deliveries"""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the delivery",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "ID",
+ },
)
mandateId: str = Field(
description="ID of the mandate this delivery belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Mandanten-ID",
+ },
)
featureInstanceId: str = Field(
description="ID of the feature instance this delivery belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Feature-Instanz-ID",
+ },
)
subscriptionId: str = Field(
description="ID of the subscription this delivery belongs to",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Abonnement-ID",
+ },
)
userId: str = Field(
description="ID of the user receiving this delivery",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Benutzer-ID",
+ },
)
channel: MessagingChannel = Field(
description="Channel used for this delivery",
@@ -184,9 +256,10 @@ class MessagingDelivery(BaseModel):
{"value": "email", "label": {"en": "Email", "fr": "Email"}},
{"value": "sms", "label": {"en": "SMS", "fr": "SMS"}},
{"value": "whatsapp", "label": {"en": "WhatsApp", "fr": "WhatsApp"}},
- {"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}}
- ]
- }
+ {"value": "teams_chat", "label": {"en": "Teams Chat", "fr": "Chat Teams"}},
+ ],
+ "label": "Kanal",
+ },
)
status: DeliveryStatus = Field(
default=DeliveryStatus.PENDING,
@@ -198,112 +271,113 @@ class MessagingDelivery(BaseModel):
"frontend_options": [
{"value": "pending", "label": {"en": "Pending", "fr": "En attente"}},
{"value": "sent", "label": {"en": "Sent", "fr": "Envoyé"}},
- {"value": "failed", "label": {"en": "Failed", "fr": "Échoué"}}
- ]
- }
+ {"value": "failed", "label": {"en": "Failed", "fr": "Échoué"}},
+ ],
+ "label": "Status",
+ },
)
errorMessage: Optional[str] = Field(
default=None,
description="Error message if delivery failed",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "textarea",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Fehlermeldung",
+ },
)
sentAt: Optional[float] = Field(
default=None,
description="When the delivery was sent (UTC timestamp in seconds)",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "datetime",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Gesendet am",
+ },
)
model_config = ConfigDict(use_enum_values=True)
-registerModelLabels(
- "MessagingDelivery",
- {"en": "Messaging Delivery", "fr": "Livraison de messagerie"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
- "subscriptionId": {"en": "Subscription ID", "fr": "ID d'abonnement"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "channel": {"en": "Channel", "fr": "Canal"},
- "status": {"en": "Status", "fr": "Statut"},
- "errorMessage": {"en": "Error Message", "fr": "Message d'erreur"},
- "sentAt": {"en": "Sent At", "fr": "Envoyé le"},
- },
-)
-
-
+@i18nModel("Messaging-Ereignisparameter")
class MessagingEventParameters(BaseModel):
"""Data model for event parameters passed to subscription functions"""
triggerData: dict = Field(
default_factory=dict,
description="Event data from trigger as dictionary/JSON",
- json_schema_extra={"frontend_type": "json", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "json",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "label": "Trigger-Daten",
+ },
)
-registerModelLabels(
- "MessagingEventParameters",
- {"en": "Messaging Event Parameters", "fr": "Paramètres d'événement de messagerie"},
- {
- "triggerData": {"en": "Trigger Data", "fr": "Données de déclenchement"},
- },
-)
-
-
-registerModelLabels(
- "MessagingSendResult",
- {"en": "Messaging Send Result", "fr": "Résultat d'envoi de messagerie"},
- {
- "success": {"en": "Success", "fr": "Succès"},
- "deliveryId": {"en": "Delivery ID", "fr": "ID de livraison"},
- "errorMessage": {"en": "Error Message", "fr": "Message d'erreur"},
- },
-)
-
-
-registerModelLabels(
- "MessagingSubscriptionExecutionResult",
- {"en": "Messaging Subscription Execution Result", "fr": "Résultat d'exécution d'abonnement"},
- {
- "success": {"en": "Success", "fr": "Succès"},
- "messagesSent": {"en": "Messages Sent", "fr": "Messages envoyés"},
- "errorMessage": {"en": "Error Message", "fr": "Message d'erreur"},
- },
-)
-
-
+@i18nModel("Messaging-Sendeergebnis")
class MessagingSendResult(BaseModel):
"""Data model for sendMessage result"""
success: bool = Field(
description="Whether the message was sent successfully",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={
+ "frontend_type": "checkbox",
+ "frontend_readonly": True,
+ "frontend_required": True,
+ "label": "Erfolg",
+ },
)
deliveryId: Optional[str] = Field(
default=None,
description="ID of the created MessagingDelivery record",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "text",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Zustellungs-ID",
+ },
)
errorMessage: Optional[str] = Field(
default=None,
description="Error message if sending failed",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "textarea",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Fehlermeldung",
+ },
)
+@i18nModel("Messaging-Abonnement-Ausführung")
class MessagingSubscriptionExecutionResult(BaseModel):
"""Data model for subscription function execution result"""
success: bool = Field(
description="Whether the subscription execution was successful",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={
+ "frontend_type": "checkbox",
+ "frontend_readonly": True,
+ "frontend_required": True,
+ "label": "Erfolg",
+ },
)
messagesSent: int = Field(
default=0,
description="Number of messages sent",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "number",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Gesendete Nachrichten",
+ },
)
errorMessage: Optional[str] = Field(
default=None,
description="Error message if execution failed",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={
+ "frontend_type": "textarea",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "label": "Fehlermeldung",
+ },
)
diff --git a/modules/datamodels/datamodelNotification.py b/modules/datamodels/datamodelNotification.py
index f5af0f55..9dfa2b7e 100644
--- a/modules/datamodels/datamodelNotification.py
+++ b/modules/datamodels/datamodelNotification.py
@@ -10,7 +10,7 @@ from typing import Optional, List
from enum import Enum
from pydantic import BaseModel, Field, ConfigDict
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
class NotificationType(str, Enum):
@@ -29,20 +29,25 @@ class NotificationStatus(str, Enum):
DISMISSED = "dismissed" # Verworfen/Geschlossen
+@i18nModel("Benachrichtigungs-Aktion")
class NotificationAction(BaseModel):
"""Possible action for a notification"""
actionId: str = Field(
- description="Unique identifier for the action (e.g., 'accept', 'decline')"
+ description="Unique identifier for the action (e.g., 'accept', 'decline')",
+ json_schema_extra={"label": "Aktions-ID"},
)
label: str = Field(
- description="Display label for the action button"
+ description="Display label for the action button",
+ json_schema_extra={"label": "Bezeichnung"},
)
style: str = Field(
default="default",
- description="Button style: 'primary', 'danger', 'default'"
+ description="Button style: 'primary', 'danger', 'default'",
+ json_schema_extra={"label": "Stil"},
)
+@i18nModel("Benachrichtigung")
class UserNotification(PowerOnModel):
"""
In-app notification for a user.
@@ -51,18 +56,18 @@ class UserNotification(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the notification",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
userId: str = Field(
description="Target user ID for this notification",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Benutzer", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
-
- # Notification type and status
+
type: NotificationType = Field(
default=NotificationType.SYSTEM,
description="Type of notification",
json_schema_extra={
+ "label": "Typ",
"frontend_type": "select",
"frontend_readonly": True,
"frontend_required": True,
@@ -78,6 +83,7 @@ class UserNotification(PowerOnModel):
default=NotificationStatus.UNREAD,
description="Current status of the notification",
json_schema_extra={
+ "label": "Status",
"frontend_type": "select",
"frontend_readonly": True,
"frontend_required": False,
@@ -89,115 +95,63 @@ class UserNotification(PowerOnModel):
]
}
)
-
- # Content
+
title: str = Field(
description="Notification title",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Titel", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
)
message: str = Field(
description="Notification message/body",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"label": "Nachricht", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": True}
)
icon: Optional[str] = Field(
default=None,
description="Optional icon identifier (e.g., 'mail', 'warning', 'info')",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Symbol", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
- # Reference to triggering object (for actionable notifications)
+
referenceType: Optional[str] = Field(
default=None,
description="Type of referenced object (e.g., 'Invitation', 'Workflow')",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Referenz-Typ", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
referenceId: Optional[str] = Field(
default=None,
description="ID of referenced object",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Referenz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
-
- # Actions (for actionable notifications like invitations)
+
actions: Optional[List[NotificationAction]] = Field(
default=None,
description="List of possible actions for this notification",
- json_schema_extra={"frontend_type": "json", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Aktionen", "frontend_type": "json", "frontend_readonly": True, "frontend_required": False}
)
-
- # Action result (when user takes action)
+
actionTaken: Optional[str] = Field(
default=None,
description="Which action was taken (actionId)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Durchgefuehrte Aktion", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False}
)
actionResult: Optional[str] = Field(
default=None,
description="Result message from the action",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Aktions-Ergebnis", "frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False}
)
-
- # Timestamps
+
readAt: Optional[float] = Field(
default=None,
description="When the notification was read (UTC timestamp)",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Gelesen am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
)
actionedAt: Optional[float] = Field(
default=None,
description="When action was taken (UTC timestamp)",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Bearbeitet am", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
)
expiresAt: Optional[float] = Field(
default=None,
description="When the notification expires (optional, UTC timestamp)",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "Gueltig bis", "frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
)
-
+
model_config = ConfigDict(use_enum_values=True)
-
-
-registerModelLabels(
- "UserNotification",
- {"en": "Notification", "de": "Benachrichtigung", "fr": "Notification"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
- "type": {"en": "Type", "de": "Typ", "fr": "Type"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "title": {"en": "Title", "de": "Titel", "fr": "Titre"},
- "message": {"en": "Message", "de": "Nachricht", "fr": "Message"},
- "icon": {"en": "Icon", "de": "Symbol", "fr": "Icône"},
- "referenceType": {"en": "Reference Type", "de": "Referenz-Typ", "fr": "Type de référence"},
- "referenceId": {"en": "Reference ID", "de": "Referenz-ID", "fr": "ID de référence"},
- "actions": {"en": "Actions", "de": "Aktionen", "fr": "Actions"},
- "actionTaken": {"en": "Action Taken", "de": "Durchgeführte Aktion", "fr": "Action effectuée"},
- "actionResult": {"en": "Action Result", "de": "Aktions-Ergebnis", "fr": "Résultat de l'action"},
- "readAt": {"en": "Read At", "de": "Gelesen am", "fr": "Lu le"},
- "actionedAt": {"en": "Actioned At", "de": "Bearbeitet am", "fr": "Traité le"},
- "expiresAt": {"en": "Expires At", "de": "Gültig bis", "fr": "Expire le"},
- },
-)
-
-
-registerModelLabels(
- "NotificationType",
- {"en": "Notification Type", "de": "Benachrichtigungs-Typ", "fr": "Type de notification"},
- {
- "invitation": {"en": "Invitation", "de": "Einladung", "fr": "Invitation"},
- "system": {"en": "System", "de": "System", "fr": "Système"},
- "workflow": {"en": "Workflow", "de": "Workflow", "fr": "Workflow"},
- "mention": {"en": "Mention", "de": "Erwähnung", "fr": "Mention"},
- },
-)
-
-
-registerModelLabels(
- "NotificationStatus",
- {"en": "Notification Status", "de": "Benachrichtigungs-Status", "fr": "Statut de notification"},
- {
- "unread": {"en": "Unread", "de": "Ungelesen", "fr": "Non lu"},
- "read": {"en": "Read", "de": "Gelesen", "fr": "Lu"},
- "actioned": {"en": "Actioned", "de": "Bearbeitet", "fr": "Traité"},
- "dismissed": {"en": "Dismissed", "de": "Verworfen", "fr": "Rejeté"},
- },
-)
diff --git a/modules/datamodels/datamodelRbac.py b/modules/datamodels/datamodelRbac.py
index b9e0cb91..c9829458 100644
--- a/modules/datamodels/datamodelRbac.py
+++ b/modules/datamodels/datamodelRbac.py
@@ -14,7 +14,7 @@ from typing import Optional
from enum import Enum
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.datamodels.datamodelUtils import TextMultilingual
from modules.datamodels.datamodelUam import AccessLevel
@@ -26,6 +26,7 @@ class AccessRuleContext(str, Enum):
RESOURCE = "RESOURCE" # System resources (AI models, actions, etc.)
+@i18nModel("Rolle")
class Role(PowerOnModel):
"""
Data model for RBAC roles.
@@ -41,56 +42,42 @@ class Role(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the role",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
roleLabel: str = Field(
description="Unique role label identifier (e.g., 'admin', 'user', 'viewer')",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Rollen-Label", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
)
description: TextMultilingual = Field(
description="Role description in multiple languages",
- json_schema_extra={"frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Beschreibung", "frontend_type": "multilingual", "frontend_readonly": False, "frontend_required": True}
)
# KONTEXT - IMMUTABLE nach Create (nur Create/Delete, kein Update!)
mandateId: Optional[str] = Field(
default=None,
description="FK → Mandate.id (CASCADE DELETE). Null = Global/Template role.",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
+ json_schema_extra={"label": "Mandant", "frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/mandates/", "frontend_fk_display_field": "label"}
)
featureInstanceId: Optional[str] = Field(
default=None,
description="FK → FeatureInstance.id (CASCADE DELETE). Null = Mandate-level or Global role.",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
+ json_schema_extra={"label": "Feature-Instanz", "frontend_type": "select", "frontend_readonly": True, "frontend_visible": True, "frontend_required": False, "frontend_fk_source": "/api/feature-instances/", "frontend_fk_display_field": "name"}
)
featureCode: Optional[str] = Field(
default=None,
description="Feature code (z.B. 'trustee') - für Template-Rollen",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "Feature-Code", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
-
+
isSystemRole: bool = Field(
default=False,
description="Whether this is a system role that cannot be deleted",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"label": "System-Rolle", "frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
)
-registerModelLabels(
- "Role",
- {"en": "Role", "de": "Rolle", "fr": "Rôle"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "roleLabel": {"en": "Role Label", "de": "Rollen-Label", "fr": "Label du rôle"},
- "description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
- "mandateId": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- "featureCode": {"en": "Feature Code", "de": "Feature-Code", "fr": "Code fonctionnalité"},
- "isSystemRole": {"en": "System Role", "de": "System-Rolle", "fr": "Rôle système"},
- },
-)
-
-
+@i18nModel("Zugriffsregel")
class AccessRule(PowerOnModel):
"""
Data model for access control rules.
@@ -101,15 +88,15 @@ class AccessRule(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the access rule",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
)
roleId: str = Field(
description="FK → Role.id (CASCADE DELETE!)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
+ json_schema_extra={"label": "Rolle", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_fk_source": "/api/rbac/roles", "frontend_fk_display_field": "roleLabel"}
)
context: AccessRuleContext = Field(
description="Context type: DATA (database), UI (interface), RESOURCE (system resources). IMMUTABLE!",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_options": [
+ json_schema_extra={"label": "Kontext", "frontend_type": "select", "frontend_readonly": True, "frontend_required": True, "frontend_options": [
{"value": "DATA", "label": {"en": "Data", "de": "Daten", "fr": "Données"}},
{"value": "UI", "label": {"en": "UI", "de": "Oberfläche", "fr": "Interface"}},
{"value": "RESOURCE", "label": {"en": "Resource", "de": "Ressource", "fr": "Ressource"}}
@@ -118,17 +105,17 @@ class AccessRule(PowerOnModel):
item: Optional[str] = Field(
default=None,
description="Item identifier (null = all items in context). Format: DATA: '' or '.', UI: cascading string (e.g., 'playground.voice.settings'), RESOURCE: cascading string (e.g., 'ai.model.anthropic')",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"label": "Element", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
)
view: bool = Field(
default=False,
description="View permission: if true, item is visible/enabled. Only objects with view=true are shown.",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"label": "Anzeigen", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": True}
)
read: Optional[AccessLevel] = Field(
default=None,
description="Read permission level (only for DATA context)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Lesen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
@@ -138,7 +125,7 @@ class AccessRule(PowerOnModel):
create: Optional[AccessLevel] = Field(
default=None,
description="Create permission level (only for DATA context)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Erstellen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
@@ -148,7 +135,7 @@ class AccessRule(PowerOnModel):
update: Optional[AccessLevel] = Field(
default=None,
description="Update permission level (only for DATA context)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Aktualisieren", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
@@ -158,7 +145,7 @@ class AccessRule(PowerOnModel):
delete: Optional[AccessLevel] = Field(
default=None,
description="Delete permission level (only for DATA context)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ json_schema_extra={"label": "Loeschen", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
{"value": "a", "label": {"en": "All Records", "de": "Alle Datensätze", "fr": "Tous les enregistrements"}},
{"value": "m", "label": {"en": "My Records", "de": "Meine Datensätze", "fr": "Mes enregistrements"}},
{"value": "g", "label": {"en": "Group Records", "de": "Gruppen-Datensätze", "fr": "Enregistrements du groupe"}},
@@ -167,23 +154,6 @@ class AccessRule(PowerOnModel):
)
-registerModelLabels(
- "AccessRule",
- {"en": "Access Rule", "de": "Zugriffsregel", "fr": "Règle d'accès"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "roleId": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
- "context": {"en": "Context", "de": "Kontext", "fr": "Contexte"},
- "item": {"en": "Item", "de": "Element", "fr": "Élément"},
- "view": {"en": "View", "de": "Anzeigen", "fr": "Vue"},
- "read": {"en": "Read", "de": "Lesen", "fr": "Lecture"},
- "create": {"en": "Create", "de": "Erstellen", "fr": "Créer"},
- "update": {"en": "Update", "de": "Aktualisieren", "fr": "Mettre à jour"},
- "delete": {"en": "Delete", "de": "Löschen", "fr": "Supprimer"},
- },
-)
-
-
# IMMUTABLE Fields Definition - für Enforcement auf Application-Level
IMMUTABLE_FIELDS = {
"Role": ["mandateId", "featureInstanceId", "featureCode"],
diff --git a/modules/datamodels/datamodelSecurity.py b/modules/datamodels/datamodelSecurity.py
index dc8c26e6..52237226 100644
--- a/modules/datamodels/datamodelSecurity.py
+++ b/modules/datamodels/datamodelSecurity.py
@@ -12,7 +12,7 @@ Multi-Tenant Design:
from typing import Optional, Any
from pydantic import BaseModel, Field, ConfigDict, model_validator
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.shared.timeUtils import getUtcTimestamp
from .datamodelUam import AuthAuthority
from enum import Enum
@@ -31,46 +31,79 @@ class TokenPurpose(str, Enum):
DATA_CONNECTION = "dataConnection"
+@i18nModel("Token")
class Token(PowerOnModel):
"""
Authentication Token model.
-
+
Multi-Tenant Design:
- Token ist User-gebunden, NICHT Mandant-gebunden
- Ermöglicht parallele Arbeit in mehreren Mandanten
- Mandant-Kontext wird per Request-Header bestimmt
"""
- id: Optional[str] = None
- userId: str
- authority: AuthAuthority
+ id: Optional[str] = Field(
+ default=None,
+ json_schema_extra={"label": "ID"},
+ )
+ userId: str = Field(
+ ...,
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ authority: AuthAuthority = Field(
+ ...,
+ json_schema_extra={"label": "Autoritaet"},
+ )
connectionId: Optional[str] = Field(
- None, description="ID of the connection this token belongs to"
+ None,
+ description="ID of the connection this token belongs to",
+ json_schema_extra={"label": "Verbindungs-ID"},
)
tokenPurpose: Optional[TokenPurpose] = Field(
default=None,
description="authSession = gateway login JWT; dataConnection = provider OAuth for a connection",
+ json_schema_extra={"label": "Token-Verwendung"},
+ )
+ tokenAccess: str = Field(
+ ...,
+ json_schema_extra={"label": "Zugriffstoken"},
+ )
+ tokenType: str = Field(
+ default="bearer",
+ json_schema_extra={"label": "Token-Typ"},
)
- tokenAccess: str
- tokenType: str = "bearer"
expiresAt: float = Field(
- description="When the token expires (UTC timestamp in seconds)"
+ description="When the token expires (UTC timestamp in seconds)",
+ json_schema_extra={"label": "Laeuft ab am"},
+ )
+ tokenRefresh: Optional[str] = Field(
+ default=None,
+ json_schema_extra={"label": "Refresh-Token"},
)
- tokenRefresh: Optional[str] = None
status: TokenStatus = Field(
- default=TokenStatus.ACTIVE, description="Token status: active/revoked"
+ default=TokenStatus.ACTIVE,
+ description="Token status: active/revoked",
+ json_schema_extra={"label": "Status"},
)
revokedAt: Optional[float] = Field(
- None, description="When the token was revoked (UTC timestamp in seconds)"
+ None,
+ description="When the token was revoked (UTC timestamp in seconds)",
+ json_schema_extra={"label": "Widerrufen am"},
)
revokedBy: Optional[str] = Field(
- None, description="User ID who revoked the token (admin/self)"
+ None,
+ description="User ID who revoked the token (admin/self)",
+ json_schema_extra={"label": "Widerrufen von"},
+ )
+ reason: Optional[str] = Field(
+ None,
+ description="Optional revocation reason",
+ json_schema_extra={"label": "Grund"},
)
- reason: Optional[str] = Field(None, description="Optional revocation reason")
sessionId: Optional[str] = Field(
- None, description="Logical session grouping for logout revocation"
+ None,
+ description="Logical session grouping for logout revocation",
+ json_schema_extra={"label": "Sitzungs-ID"},
)
- # ENTFERNT: mandateId - Token ist nicht mehr Mandant-spezifisch
- # Mandant-Kontext wird per Request-Header (X-Mandate-Id) bestimmt
model_config = ConfigDict(use_enum_values=True)
@@ -91,51 +124,44 @@ class Token(PowerOnModel):
return data
-registerModelLabels(
- "Token",
- {"en": "Token", "de": "Token", "fr": "Jeton"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
- "authority": {"en": "Authority", "de": "Autorität", "fr": "Autorité"},
- "connectionId": {"en": "Connection ID", "de": "Verbindungs-ID", "fr": "ID de connexion"},
- "tokenPurpose": {"en": "Token purpose", "de": "Token-Verwendung", "fr": "Usage du jeton"},
- "tokenAccess": {"en": "Access Token", "de": "Zugriffstoken", "fr": "Jeton d'accès"},
- "tokenType": {"en": "Token Type", "de": "Token-Typ", "fr": "Type de jeton"},
- "expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
- "tokenRefresh": {"en": "Refresh Token", "de": "Refresh-Token", "fr": "Jeton de rafraîchissement"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "revokedAt": {"en": "Revoked At", "de": "Widerrufen am", "fr": "Révoqué le"},
- "revokedBy": {"en": "Revoked By", "de": "Widerrufen von", "fr": "Révoqué par"},
- "reason": {"en": "Reason", "de": "Grund", "fr": "Raison"},
- "sessionId": {"en": "Session ID", "de": "Sitzungs-ID", "fr": "ID de session"},
- },
-)
-
-
+@i18nModel("Authentifizierungsereignis")
class AuthEvent(PowerOnModel):
"""Authentication event for audit logging."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the auth event", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- userId: str = Field(description="ID of the user this event belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- eventType: str = Field(description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- timestamp: float = Field(default_factory=getUtcTimestamp, description="Unix timestamp when the event occurred", json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True})
- ipAddress: Optional[str] = Field(default=None, description="IP address from which the event originated", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- userAgent: Optional[str] = Field(default=None, description="User agent string from the request", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- success: bool = Field(default=True, description="Whether the authentication event was successful", json_schema_extra={"frontend_type": "boolean", "frontend_readonly": True, "frontend_required": True})
- details: Optional[str] = Field(default=None, description="Additional details about the event", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
-
-
-registerModelLabels(
- "AuthEvent",
- {"en": "Authentication Event", "de": "Authentifizierungsereignis", "fr": "Événement d'authentification"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
- "eventType": {"en": "Event Type", "de": "Ereignistyp", "fr": "Type d'événement"},
- "timestamp": {"en": "Timestamp", "de": "Zeitstempel", "fr": "Horodatage"},
- "ipAddress": {"en": "IP Address", "de": "IP-Adresse", "fr": "Adresse IP"},
- "userAgent": {"en": "User Agent", "de": "User-Agent", "fr": "Agent utilisateur"},
- "success": {"en": "Success", "de": "Erfolgreich", "fr": "Succès"},
- "details": {"en": "Details", "de": "Details", "fr": "Détails"},
- },
-)
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Unique ID of the auth event",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ userId: str = Field(
+ description="ID of the user this event belongs to",
+ json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ eventType: str = Field(
+ description="Type of authentication event (e.g., 'login', 'logout', 'token_refresh')",
+ json_schema_extra={"label": "Ereignistyp", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ timestamp: float = Field(
+ default_factory=getUtcTimestamp,
+ description="Unix timestamp when the event occurred",
+ json_schema_extra={"label": "Zeitstempel", "frontend_type": "datetime", "frontend_readonly": True, "frontend_required": True},
+ )
+ ipAddress: Optional[str] = Field(
+ default=None,
+ description="IP address from which the event originated",
+ json_schema_extra={"label": "IP-Adresse", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ userAgent: Optional[str] = Field(
+ default=None,
+ description="User agent string from the request",
+ json_schema_extra={"label": "User-Agent", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ success: bool = Field(
+ default=True,
+ description="Whether the authentication event was successful",
+ json_schema_extra={"label": "Erfolgreich", "frontend_type": "boolean", "frontend_readonly": True, "frontend_required": True},
+ )
+ details: Optional[str] = Field(
+ default=None,
+ description="Additional details about the event",
+ json_schema_extra={"label": "Details", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
diff --git a/modules/datamodels/datamodelSubscription.py b/modules/datamodels/datamodelSubscription.py
index 1791e7a9..16f6789d 100644
--- a/modules/datamodels/datamodelSubscription.py
+++ b/modules/datamodels/datamodelSubscription.py
@@ -11,7 +11,7 @@ from enum import Enum
from datetime import datetime, timezone
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
@@ -55,123 +55,224 @@ class BillingPeriodEnum(str, Enum):
# Catalog: SubscriptionPlan (static, in-memory)
# ============================================================================
+@i18nModel("Abonnement-Plan")
class SubscriptionPlan(BaseModel):
- """Plan definition (catalog entry). Not stored per mandate — static."""
- planKey: str = Field(..., description="Unique plan identifier")
- selectableByUser: bool = Field(default=True, description="Whether users can choose this plan in the UI")
+ """Plan-Definition (Katalog). Nicht pro Mandat gespeichert — statisch."""
+ planKey: str = Field(
+ ...,
+ description="Unique plan identifier",
+ json_schema_extra={"label": "Plan"},
+ )
+ selectableByUser: bool = Field(
+ default=True,
+ description="Whether users can choose this plan in the UI",
+ json_schema_extra={"label": "Waehlbar"},
+ )
- title: Dict[str, str] = Field(default_factory=dict, description="Multilingual title (en/de/fr)")
- description: Dict[str, str] = Field(default_factory=dict, description="Multilingual description")
+ title: Dict[str, str] = Field(
+ default_factory=dict,
+ description="Multilingual title (en/de/fr)",
+ json_schema_extra={"label": "Titel"},
+ )
+ description: Dict[str, str] = Field(
+ default_factory=dict,
+ description="Multilingual description",
+ json_schema_extra={"label": "Beschreibung"},
+ )
- currency: str = Field(default="CHF", description="Billing currency")
- billingPeriod: BillingPeriodEnum = Field(default=BillingPeriodEnum.MONTHLY, description="Recurring interval")
- pricePerUserCHF: float = Field(default=0.0, description="Price per active user per period")
- pricePerFeatureInstanceCHF: float = Field(default=0.0, description="Price per active feature instance per period")
- autoRenew: bool = Field(default=True, description="Stripe renews automatically at period end")
+ currency: str = Field(
+ default="CHF",
+ description="Billing currency",
+ json_schema_extra={"label": "Waehrung"},
+ )
+ billingPeriod: BillingPeriodEnum = Field(
+ default=BillingPeriodEnum.MONTHLY,
+ description="Recurring interval",
+ json_schema_extra={"label": "Abrechnungszeitraum"},
+ )
+ pricePerUserCHF: float = Field(
+ default=0.0,
+ description="Price per active user per period",
+ json_schema_extra={"label": "Preis pro User (CHF)"},
+ )
+ pricePerFeatureInstanceCHF: float = Field(
+ default=0.0,
+ description="Price per active feature instance per period",
+ json_schema_extra={"label": "Preis pro Instanz (CHF)"},
+ )
+ autoRenew: bool = Field(
+ default=True,
+ description="Stripe renews automatically at period end",
+ json_schema_extra={"label": "Auto-Verlaengerung"},
+ )
- maxUsers: Optional[int] = Field(None, description="Hard cap on active users (None = unlimited)")
- maxFeatureInstances: Optional[int] = Field(None, description="Hard cap on active feature instances (None = unlimited)")
- trialDays: Optional[int] = Field(None, description="Trial duration in days (only for trial plans)")
- maxDataVolumeMB: Optional[int] = Field(None, description="Soft-limit for data volume in MB per mandate (None = unlimited)")
- budgetAiCHF: float = Field(default=0.0, description="AI budget (CHF) included in subscription price per billing period")
- successorPlanKey: Optional[str] = Field(None, description="Plan to transition to when trial ends")
-
-
-registerModelLabels(
- "SubscriptionPlan",
- {"en": "Subscription Plan", "de": "Abonnement-Plan", "fr": "Plan d'abonnement"},
- {
- "planKey": {"en": "Plan", "de": "Plan", "fr": "Plan"},
- "selectableByUser": {"en": "Selectable", "de": "Wählbar", "fr": "Sélectionnable"},
- "billingPeriod": {"en": "Billing Period", "de": "Abrechnungszeitraum", "fr": "Période de facturation"},
- "pricePerUserCHF": {"en": "Price per User (CHF)", "de": "Preis pro User (CHF)"},
- "pricePerFeatureInstanceCHF": {"en": "Price per Instance (CHF)", "de": "Preis pro Instanz (CHF)"},
- "maxUsers": {"en": "Max Users", "de": "Max. Benutzer", "fr": "Max. utilisateurs"},
- "maxFeatureInstances": {"en": "Max Instances", "de": "Max. Instanzen", "fr": "Max. instances"},
- "maxDataVolumeMB": {"en": "Data Volume (MB)", "de": "Datenvolumen (MB)"},
- "budgetAiCHF": {"en": "AI Budget (CHF)", "de": "AI-Budget (CHF)"},
- },
-)
+ maxUsers: Optional[int] = Field(
+ None,
+ description="Hard cap on active users (None = unlimited)",
+ json_schema_extra={"label": "Max. Benutzer"},
+ )
+ maxFeatureInstances: Optional[int] = Field(
+ None,
+ description="Hard cap on active feature instances (None = unlimited)",
+ json_schema_extra={"label": "Max. Instanzen"},
+ )
+ trialDays: Optional[int] = Field(
+ None,
+ description="Trial duration in days (only for trial plans)",
+ json_schema_extra={"label": "Probentage"},
+ )
+ maxDataVolumeMB: Optional[int] = Field(
+ None,
+ description="Soft-limit for data volume in MB per mandate (None = unlimited)",
+ json_schema_extra={"label": "Datenvolumen (MB)"},
+ )
+ budgetAiCHF: float = Field(
+ default=0.0,
+ description="AI budget (CHF) included in subscription price per billing period",
+ json_schema_extra={"label": "AI-Budget (CHF)"},
+ )
+ successorPlanKey: Optional[str] = Field(
+ None,
+ description="Plan to transition to when trial ends",
+ json_schema_extra={"label": "Nachfolge-Plan"},
+ )
# ============================================================================
# Stripe Price mapping (persisted in DB, auto-created at bootstrap)
# ============================================================================
+@i18nModel("Stripe-Planpreise")
class StripePlanPrice(BaseModel):
- """Persisted mapping from planKey to Stripe Product/Price IDs.
- Auto-created at startup — no manual configuration needed.
- Uses separate Stripe Products for users and instances for clear invoice labels."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- planKey: str = Field(..., description="Reference to SubscriptionPlan.planKey")
- stripeProductId: str = Field("", description="Legacy single-product ID (unused)")
- stripeProductIdUsers: Optional[str] = Field(None, description="Stripe Product ID for user licenses")
- stripeProductIdInstances: Optional[str] = Field(None, description="Stripe Product ID for feature instances")
- stripePriceIdUsers: Optional[str] = Field(None, description="Stripe Price ID for user-seat line item")
- stripePriceIdInstances: Optional[str] = Field(None, description="Stripe Price ID for instance line item")
-
-
-registerModelLabels(
- "StripePlanPrice",
- {"en": "Stripe Plan Prices", "de": "Stripe-Planpreise"},
- {
- "planKey": {"en": "Plan", "de": "Plan"},
- "stripeProductIdUsers": {"en": "Product (Users)", "de": "Produkt (User)"},
- "stripeProductIdInstances": {"en": "Product (Instances)", "de": "Produkt (Instanzen)"},
- "stripePriceIdUsers": {"en": "Price ID (Users)", "de": "Preis-ID (User)"},
- "stripePriceIdInstances": {"en": "Price ID (Instances)", "de": "Preis-ID (Instanzen)"},
- },
-)
+ """Persistierte Zuordnung planKey zu Stripe Product/Price IDs."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ planKey: str = Field(
+ ...,
+ description="Reference to SubscriptionPlan.planKey",
+ json_schema_extra={"label": "Plan"},
+ )
+ stripeProductId: str = Field(
+ "",
+ description="Legacy single-product ID (unused)",
+ json_schema_extra={"label": "Stripe-Produkt-ID (Legacy)"},
+ )
+ stripeProductIdUsers: Optional[str] = Field(
+ None,
+ description="Stripe Product ID for user licenses",
+ json_schema_extra={"label": "Produkt (User)"},
+ )
+ stripeProductIdInstances: Optional[str] = Field(
+ None,
+ description="Stripe Product ID for feature instances",
+ json_schema_extra={"label": "Produkt (Instanzen)"},
+ )
+ stripePriceIdUsers: Optional[str] = Field(
+ None,
+ description="Stripe Price ID for user-seat line item",
+ json_schema_extra={"label": "Preis-ID (User)"},
+ )
+ stripePriceIdInstances: Optional[str] = Field(
+ None,
+ description="Stripe Price ID for instance line item",
+ json_schema_extra={"label": "Preis-ID (Instanzen)"},
+ )
# ============================================================================
# Instance: MandateSubscription
# ============================================================================
+@i18nModel("Mandanten-Abonnement")
class MandateSubscription(PowerOnModel):
- """A subscription instance bound to a specific mandate.
- See wiki/concepts/Subscription-State-Machine.md for state transitions."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- mandateId: str = Field(..., description="Foreign key to Mandate")
- planKey: str = Field(..., description="Reference to SubscriptionPlan.planKey")
+ """Abonnement-Instanz gebunden an einen Mandanten."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ mandateId: str = Field(
+ ...,
+ description="Foreign key to Mandate",
+ json_schema_extra={"label": "Mandanten-ID"},
+ )
+ planKey: str = Field(
+ ...,
+ description="Reference to SubscriptionPlan.planKey",
+ json_schema_extra={"label": "Plan"},
+ )
- status: SubscriptionStatusEnum = Field(default=SubscriptionStatusEnum.PENDING, description="Current lifecycle status")
- recurring: bool = Field(default=True, description="True: auto-renews at period end. False: expires at period end (gekuendigt).")
+ status: SubscriptionStatusEnum = Field(
+ default=SubscriptionStatusEnum.PENDING,
+ description="Current lifecycle status",
+ json_schema_extra={"label": "Status"},
+ )
+ recurring: bool = Field(
+ default=True,
+ description="True: auto-renews at period end. False: expires at period end (gekuendigt).",
+ json_schema_extra={"label": "Wiederkehrend"},
+ )
- startedAt: datetime = Field(default_factory=lambda: datetime.now(timezone.utc), description="Record creation timestamp")
- effectiveFrom: Optional[datetime] = Field(None, description="When this subscription becomes operative. None = immediate. Set for SCHEDULED subs.")
- endedAt: Optional[datetime] = Field(None, description="When subscription ended (terminal)")
- currentPeriodStart: Optional[datetime] = Field(None, description="Current billing period start (synced from Stripe)")
- currentPeriodEnd: Optional[datetime] = Field(None, description="Current billing period end (synced from Stripe)")
- trialEndsAt: Optional[datetime] = Field(None, description="Trial expiry timestamp")
+ startedAt: datetime = Field(
+ default_factory=lambda: datetime.now(timezone.utc),
+ description="Record creation timestamp",
+ json_schema_extra={"label": "Gestartet"},
+ )
+ effectiveFrom: Optional[datetime] = Field(
+ None,
+ description="When this subscription becomes operative. None = immediate. Set for SCHEDULED subs.",
+ json_schema_extra={"label": "Wirksam ab"},
+ )
+ endedAt: Optional[datetime] = Field(
+ None,
+ description="When subscription ended (terminal)",
+ json_schema_extra={"label": "Beendet"},
+ )
+ currentPeriodStart: Optional[datetime] = Field(
+ None,
+ description="Current billing period start (synced from Stripe)",
+ json_schema_extra={"label": "Periodenbeginn"},
+ )
+ currentPeriodEnd: Optional[datetime] = Field(
+ None,
+ description="Current billing period end (synced from Stripe)",
+ json_schema_extra={"label": "Periodenende"},
+ )
+ trialEndsAt: Optional[datetime] = Field(
+ None,
+ description="Trial expiry timestamp",
+ json_schema_extra={"label": "Trial endet"},
+ )
- snapshotPricePerUserCHF: float = Field(default=0.0, description="Price snapshot at activation (for invoice history)")
- snapshotPricePerInstanceCHF: float = Field(default=0.0, description="Price snapshot at activation")
+ snapshotPricePerUserCHF: float = Field(
+ default=0.0,
+ description="Price snapshot at activation (for invoice history)",
+ json_schema_extra={"label": "Preis/User (CHF)"},
+ )
+ snapshotPricePerInstanceCHF: float = Field(
+ default=0.0,
+ description="Price snapshot at activation",
+ json_schema_extra={"label": "Preis/Instanz (CHF)"},
+ )
- stripeSubscriptionId: Optional[str] = Field(None, description="Stripe Subscription ID (sub_xxx)")
- stripeItemIdUsers: Optional[str] = Field(None, description="Stripe Subscription Item ID for user seats")
- stripeItemIdInstances: Optional[str] = Field(None, description="Stripe Subscription Item ID for feature instances")
-
-
-registerModelLabels(
- "MandateSubscription",
- {"en": "Mandate Subscription", "de": "Mandanten-Abonnement", "fr": "Abonnement du mandat"},
- {
- "id": {"en": "ID", "de": "ID"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
- "planKey": {"en": "Plan", "de": "Plan"},
- "status": {"en": "Status", "de": "Status"},
- "recurring": {"en": "Recurring", "de": "Wiederkehrend"},
- "startedAt": {"en": "Started", "de": "Gestartet"},
- "effectiveFrom": {"en": "Effective From", "de": "Wirksam ab"},
- "endedAt": {"en": "Ended", "de": "Beendet"},
- "currentPeriodStart": {"en": "Period Start", "de": "Periodenbeginn"},
- "currentPeriodEnd": {"en": "Period End", "de": "Periodenende"},
- "trialEndsAt": {"en": "Trial Ends", "de": "Trial endet"},
- "snapshotPricePerUserCHF": {"en": "Price/User (CHF)", "de": "Preis/User (CHF)"},
- "snapshotPricePerInstanceCHF": {"en": "Price/Instance (CHF)", "de": "Preis/Instanz (CHF)"},
- },
-)
+ stripeSubscriptionId: Optional[str] = Field(
+ None,
+ description="Stripe Subscription ID (sub_xxx)",
+ json_schema_extra={"label": "Stripe-Abonnement-ID"},
+ )
+ stripeItemIdUsers: Optional[str] = Field(
+ None,
+ description="Stripe Subscription Item ID for user seats",
+ json_schema_extra={"label": "Stripe-Item (User)"},
+ )
+ stripeItemIdInstances: Optional[str] = Field(
+ None,
+ description="Stripe Subscription Item ID for feature instances",
+ json_schema_extra={"label": "Stripe-Item (Instanzen)"},
+ )
# ============================================================================
@@ -225,10 +326,10 @@ BUILTIN_PLANS: Dict[str, SubscriptionPlan] = {
"STANDARD_YEARLY": SubscriptionPlan(
planKey="STANDARD_YEARLY",
selectableByUser=True,
- title={"en": "Standard (Yearly)", "de": "Standard (Jährlich)", "fr": "Standard (Annuel)"},
+ title={"en": "Standard (Yearly)", "de": "Standard (Jaehrlich)", "fr": "Standard (Annuel)"},
description={
"en": "Usage-based billing per active user and feature instance, billed yearly. Includes 120 CHF AI budget.",
- "de": "Nutzungsbasierte Abrechnung pro aktivem User und Feature-Instanz, jährlich. Inkl. 120 CHF AI-Budget.",
+ "de": "Nutzungsbasierte Abrechnung pro aktivem User und Feature-Instanz, jaehrlich. Inkl. 120 CHF AI-Budget.",
},
billingPeriod=BillingPeriodEnum.YEARLY,
pricePerUserCHF=948.0,
diff --git a/modules/datamodels/datamodelUam.py b/modules/datamodels/datamodelUam.py
index 35e9ec7c..e33bf7d8 100644
--- a/modules/datamodels/datamodelUam.py
+++ b/modules/datamodels/datamodelUam.py
@@ -14,7 +14,7 @@ from typing import Optional, List, Dict, Any
from enum import Enum
from pydantic import BaseModel, Field, EmailStr, field_validator, computed_field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.shared.timeUtils import getUtcTimestamp
@@ -61,6 +61,7 @@ class UserPermissions(BaseModel):
)
+@i18nModel("Mandant")
class Mandate(PowerOnModel):
"""
Mandate (Mandant/Tenant) model.
@@ -69,31 +70,31 @@ class Mandate(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the mandate",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False, "label": "ID"},
)
name: str = Field(
description="Name of the mandate",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True, "label": "Name"},
)
label: Optional[str] = Field(
default=None,
description="Display label of the mandate",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Label"},
)
enabled: bool = Field(
default=True,
description="Indicates whether the mandate is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False, "label": "Aktiviert"},
)
isSystem: bool = Field(
default=False,
description="Whether this is a system mandate (e.g. root mandate). Cannot be deleted.",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": True, "frontend_required": False, "label": "System-Mandant"},
)
deletedAt: Optional[float] = Field(
default=None,
description="Timestamp when the mandate was soft-deleted. After 30 days, hard-delete is triggered.",
- json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False}
+ json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Gelöscht am"},
)
@field_validator('isSystem', mode='before')
@@ -104,38 +105,91 @@ class Mandate(PowerOnModel):
return False
return v
-registerModelLabels(
- "Mandate",
- {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "name": {"en": "Name", "de": "Name", "fr": "Nom"},
- "label": {"en": "Label", "de": "Label", "fr": "Libellé"},
- "enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
- "isSystem": {"en": "System Mandate", "de": "System-Mandant", "fr": "Mandat système"},
- "deletedAt": {"en": "Deleted at", "de": "Gelöscht am", "fr": "Supprimé le"},
- },
-)
-
-
+@i18nModel("Benutzerverbindung")
class UserConnection(PowerOnModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the connection", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- userId: str = Field(description="ID of the user this connection belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- authority: AuthAuthority = Field(description="Authentication authority", json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": "/api/connections/authorities/options"})
- externalId: str = Field(description="User ID in the external system", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- externalUsername: str = Field(description="Username in the external system", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
- externalEmail: Optional[EmailStr] = Field(None, description="Email in the external system", json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": False})
- status: ConnectionStatus = Field(default=ConnectionStatus.ACTIVE, description="Connection status", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": "/api/connections/statuses/options"})
- connectedAt: float = Field(default_factory=getUtcTimestamp, description="When the connection was established (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
- lastChecked: float = Field(default_factory=getUtcTimestamp, description="When the connection was last verified (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
- expiresAt: Optional[float] = Field(None, description="When the connection expires (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
- tokenStatus: Optional[str] = Field(None, description="Current token status: active, expired, none", json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": [
- {"value": "active", "label": {"en": "Active", "fr": "Actif"}},
- {"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
- {"value": "none", "label": {"en": "None", "fr": "Aucun"}},
- ]})
- tokenExpiresAt: Optional[float] = Field(None, description="When the current token expires (UTC timestamp in seconds)", json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False})
- grantedScopes: Optional[List[str]] = Field(None, description="OAuth scopes granted for this connection", json_schema_extra={"frontend_type": "list", "frontend_readonly": True, "frontend_required": False})
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Unique ID of the connection",
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
+ )
+ userId: str = Field(
+ description="ID of the user this connection belongs to",
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Benutzer-ID"},
+ )
+ authority: AuthAuthority = Field(
+ description="Authentication authority",
+ json_schema_extra={
+ "frontend_type": "select",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "frontend_options": "/api/connections/authorities/options",
+ "label": "Autorität",
+ },
+ )
+ externalId: str = Field(
+ description="User ID in the external system",
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Externe ID"},
+ )
+ externalUsername: str = Field(
+ description="Username in the external system",
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Externer Benutzername"},
+ )
+ externalEmail: Optional[EmailStr] = Field(
+ None,
+ description="Email in the external system",
+ json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": False, "label": "Externe E-Mail"},
+ )
+ status: ConnectionStatus = Field(
+ default=ConnectionStatus.ACTIVE,
+ description="Connection status",
+ json_schema_extra={
+ "frontend_type": "select",
+ "frontend_readonly": False,
+ "frontend_required": False,
+ "frontend_options": "/api/connections/statuses/options",
+ "label": "Status",
+ },
+ )
+ connectedAt: float = Field(
+ default_factory=getUtcTimestamp,
+ description="When the connection was established (UTC timestamp in seconds)",
+ json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Verbunden am"},
+ )
+ lastChecked: float = Field(
+ default_factory=getUtcTimestamp,
+ description="When the connection was last verified (UTC timestamp in seconds)",
+ json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Zuletzt geprüft"},
+ )
+ expiresAt: Optional[float] = Field(
+ None,
+ description="When the connection expires (UTC timestamp in seconds)",
+ json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Läuft ab am"},
+ )
+ tokenStatus: Optional[str] = Field(
+ None,
+ description="Current token status: active, expired, none",
+ json_schema_extra={
+ "frontend_type": "select",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "frontend_options": [
+ {"value": "active", "label": {"en": "Active", "fr": "Actif"}},
+ {"value": "expired", "label": {"en": "Expired", "fr": "Expiré"}},
+ {"value": "none", "label": {"en": "None", "fr": "Aucun"}},
+ ],
+ "label": "Verbindungsstatus",
+ },
+ )
+ tokenExpiresAt: Optional[float] = Field(
+ None,
+ description="When the current token expires (UTC timestamp in seconds)",
+ json_schema_extra={"frontend_type": "timestamp", "frontend_readonly": True, "frontend_required": False, "label": "Token läuft ab am"},
+ )
+ grantedScopes: Optional[List[str]] = Field(
+ None,
+ description="OAuth scopes granted for this connection",
+ json_schema_extra={"frontend_type": "list", "frontend_readonly": True, "frontend_required": False, "label": "Gewährte Berechtigungen"},
+ )
@computed_field
@computed_field
@@ -157,29 +211,7 @@ class UserConnection(PowerOnModel):
return f"{authorityLabels.get(self.authority.value, self.authority.value)}: {self.externalUsername}"
-registerModelLabels(
- "UserConnection",
- {"en": "User Connection", "de": "Benutzerverbindung", "fr": "Connexion utilisateur"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
- "authority": {"en": "Authority", "de": "Autorität", "fr": "Autorité"},
- "externalId": {"en": "External ID", "de": "Externe ID", "fr": "ID externe"},
- "externalUsername": {"en": "External Username", "de": "Externer Benutzername", "fr": "Nom d'utilisateur externe"},
- "externalEmail": {"en": "External Email", "de": "Externe E-Mail", "fr": "Email externe"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "connectedAt": {"en": "Connected At", "de": "Verbunden am", "fr": "Connecté le"},
- "lastChecked": {"en": "Last Checked", "de": "Zuletzt geprüft", "fr": "Dernière vérification"},
- "expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
- "tokenStatus": {"en": "Connection Status", "de": "Verbindungsstatus", "fr": "Statut de connexion"},
- "tokenExpiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
- "grantedScopes": {"en": "Granted Scopes", "de": "Gewährte Berechtigungen", "fr": "Autorisations accordées"},
- "connectionReference": {"en": "Connection Reference", "de": "Verbindungsreferenz", "fr": "Référence de connexion"},
- "displayLabel": {"en": "Display Label", "de": "Anzeigebezeichnung", "fr": "Libellé d'affichage"},
- },
-)
-
-
+@i18nModel("Benutzer")
class User(PowerOnModel):
"""
User model.
@@ -193,31 +225,37 @@ class User(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique ID of the user",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False, "label": "ID"},
)
username: str = Field(
description="Username for login (immutable after creation)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True}
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Benutzername"},
)
email: Optional[EmailStr] = Field(
default=None,
description="Email address of the user",
- json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": True}
+ json_schema_extra={"frontend_type": "email", "frontend_readonly": False, "frontend_required": True, "label": "E-Mail"},
)
fullName: Optional[str] = Field(
default=None,
description="Full name of the user",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Vollständiger Name"},
)
language: str = Field(
default="de",
description="Preferred language of the user (ISO 639-1 code: de, en, fr, it)",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": True, "frontend_options": [
- {"value": "de", "label": {"en": "Deutsch", "de": "Deutsch", "fr": "Allemand"}},
- {"value": "en", "label": {"en": "English", "de": "Englisch", "fr": "Anglais"}},
- {"value": "fr", "label": {"en": "Français", "de": "Französisch", "fr": "Français"}},
- {"value": "it", "label": {"en": "Italiano", "de": "Italienisch", "fr": "Italien"}},
- ]}
+ json_schema_extra={
+ "frontend_type": "select",
+ "frontend_readonly": False,
+ "frontend_required": True,
+ "frontend_options": [
+ {"value": "de", "label": {"en": "Deutsch", "de": "Deutsch", "fr": "Allemand"}},
+ {"value": "en", "label": {"en": "English", "de": "Englisch", "fr": "Anglais"}},
+ {"value": "fr", "label": {"en": "Français", "de": "Französisch", "fr": "Français"}},
+ {"value": "it", "label": {"en": "Italiano", "de": "Italienisch", "fr": "Italien"}},
+ ],
+ "label": "Sprache",
+ },
)
@field_validator('language', mode='before')
@@ -245,13 +283,13 @@ class User(PowerOnModel):
enabled: bool = Field(
default=True,
description="Indicates whether the user is enabled",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False, "label": "Aktiviert"},
)
isSysAdmin: bool = Field(
default=False,
description="Global SysAdmin flag. SysAdmin = System-Zugriff, KEIN Daten-Zugriff!",
- json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False}
+ json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False, "label": "System-Admin"},
)
@field_validator('isSysAdmin', mode='before')
@@ -265,48 +303,45 @@ class User(PowerOnModel):
authenticationAuthority: AuthAuthority = Field(
default=AuthAuthority.LOCAL,
description="Primary authentication authority",
- json_schema_extra={"frontend_type": "select", "frontend_readonly": True, "frontend_required": False, "frontend_options": "/api/connections/authorities/options"}
+ json_schema_extra={
+ "frontend_type": "select",
+ "frontend_readonly": True,
+ "frontend_required": False,
+ "frontend_options": "/api/connections/authorities/options",
+ "label": "Authentifizierung",
+ },
)
roleLabels: List[str] = Field(
default_factory=list,
description="Role labels (from DB or enriched when loading users)",
- json_schema_extra={"frontend_type": "multiselect", "frontend_readonly": True, "frontend_visible": False, "frontend_required": False},
+ json_schema_extra={
+ "frontend_type": "multiselect",
+ "frontend_readonly": True,
+ "frontend_visible": False,
+ "frontend_required": False,
+ "label": "Rollen-Labels",
+ },
)
-registerModelLabels(
- "User",
- {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "username": {"en": "Username", "de": "Benutzername", "fr": "Nom d'utilisateur"},
- "email": {"en": "Email", "de": "E-Mail", "fr": "Email"},
- "fullName": {"en": "Full Name", "de": "Vollständiger Name", "fr": "Nom complet"},
- "language": {"en": "Language", "de": "Sprache", "fr": "Langue"},
- "enabled": {"en": "Enabled", "de": "Aktiviert", "fr": "Activé"},
- "isSysAdmin": {"en": "System Admin", "de": "System-Admin", "fr": "Admin système"},
- "authenticationAuthority": {"en": "Auth Authority", "de": "Authentifizierung", "fr": "Autorité d'authentification"},
- "roleLabels": {"en": "Role Labels", "de": "Rollen-Labels", "fr": "Libellés de rôles"},
- },
-)
-
-
+@i18nModel("Benutzerzugang")
class UserInDB(User):
"""User model with password hash for database storage."""
- hashedPassword: Optional[str] = Field(None, description="Hash of the user password")
- resetToken: Optional[str] = Field(None, description="Password reset token (UUID)")
- resetTokenExpires: Optional[float] = Field(None, description="Reset token expiration (UTC timestamp in seconds)")
-
-
-registerModelLabels(
- "UserInDB",
- {"en": "User Access", "de": "Benutzerzugang", "fr": "Accès de l'utilisateur"},
- {
- "hashedPassword": {"en": "Password hash", "de": "Passwort-Hash", "fr": "Hachage de mot de passe"},
- "resetToken": {"en": "Reset Token", "de": "Reset-Token", "fr": "Jeton de réinitialisation"},
- "resetTokenExpires": {"en": "Reset Token Expires", "de": "Token läuft ab", "fr": "Expiration du jeton"},
- },
-)
+ hashedPassword: Optional[str] = Field(
+ None,
+ description="Hash of the user password",
+ json_schema_extra={"label": "Passwort-Hash"},
+ )
+ resetToken: Optional[str] = Field(
+ None,
+ description="Password reset token (UUID)",
+ json_schema_extra={"label": "Reset-Token"},
+ )
+ resetTokenExpires: Optional[float] = Field(
+ None,
+ description="Reset token expiration (UTC timestamp in seconds)",
+ json_schema_extra={"label": "Token läuft ab"},
+ )
def _normalizeTtsVoiceMap(value: Any) -> Optional[Dict[str, str]]:
@@ -336,17 +371,50 @@ def _normalizeTtsVoiceMap(value: Any) -> Optional[Dict[str, str]]:
return out if out else None
+@i18nModel("Spracheinstellungen")
class UserVoicePreferences(PowerOnModel):
"""User-level voice/language preferences, shared across all features."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key")
- userId: str = Field(description="User ID")
- mandateId: Optional[str] = Field(default=None, description="Mandate scope (None = global for user)")
- sttLanguage: str = Field(default="de-DE", description="Speech-to-text language code")
- ttsLanguage: str = Field(default="de-DE", description="Text-to-speech language code")
- ttsVoice: Optional[str] = Field(default=None, description="Preferred TTS voice identifier")
- ttsVoiceMap: Optional[Dict[str, str]] = Field(default=None, description="Language-to-voice mapping")
- translationSourceLanguage: Optional[str] = Field(default=None, description="Source language for translations")
- translationTargetLanguage: Optional[str] = Field(default=None, description="Target language for translations")
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID"},
+ )
+ userId: str = Field(description="User ID", json_schema_extra={"label": "Benutzer-ID"})
+ mandateId: Optional[str] = Field(
+ default=None,
+ description="Mandate scope (None = global for user)",
+ json_schema_extra={"label": "Mandanten-ID"},
+ )
+ sttLanguage: str = Field(
+ default="de-DE",
+ description="Speech-to-text language code",
+ json_schema_extra={"label": "STT-Sprache"},
+ )
+ ttsLanguage: str = Field(
+ default="de-DE",
+ description="Text-to-speech language code",
+ json_schema_extra={"label": "TTS-Sprache"},
+ )
+ ttsVoice: Optional[str] = Field(
+ default=None,
+ description="Preferred TTS voice identifier",
+ json_schema_extra={"label": "TTS-Stimme"},
+ )
+ ttsVoiceMap: Optional[Dict[str, str]] = Field(
+ default=None,
+ description="Language-to-voice mapping",
+ json_schema_extra={"label": "Stimmen-Zuordnung"},
+ )
+ translationSourceLanguage: Optional[str] = Field(
+ default=None,
+ description="Source language for translations",
+ json_schema_extra={"label": "Übersetzung Quelle"},
+ )
+ translationTargetLanguage: Optional[str] = Field(
+ default=None,
+ description="Target language for translations",
+ json_schema_extra={"label": "Übersetzung Ziel"},
+ )
@field_validator("ttsVoiceMap", mode="before")
@classmethod
@@ -354,18 +422,3 @@ class UserVoicePreferences(PowerOnModel):
return _normalizeTtsVoiceMap(value)
-registerModelLabels(
- "UserVoicePreferences",
- {"en": "Voice Preferences", "de": "Spracheinstellungen", "fr": "Préférences vocales"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "userId": {"en": "User ID", "de": "Benutzer-ID", "fr": "ID utilisateur"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
- "sttLanguage": {"en": "STT Language", "de": "STT-Sprache", "fr": "Langue STT"},
- "ttsLanguage": {"en": "TTS Language", "de": "TTS-Sprache", "fr": "Langue TTS"},
- "ttsVoice": {"en": "TTS Voice", "de": "TTS-Stimme", "fr": "Voix TTS"},
- "ttsVoiceMap": {"en": "Voice Map", "de": "Stimmen-Zuordnung", "fr": "Carte des voix"},
- "translationSourceLanguage": {"en": "Translation Source", "de": "Übersetzung Quelle", "fr": "Langue source"},
- "translationTargetLanguage": {"en": "Translation Target", "de": "Übersetzung Ziel", "fr": "Langue cible"},
- },
-)
diff --git a/modules/datamodels/datamodelUiLanguage.py b/modules/datamodels/datamodelUiLanguage.py
index 3fec1878..8154f735 100644
--- a/modules/datamodels/datamodelUiLanguage.py
+++ b/modules/datamodels/datamodelUiLanguage.py
@@ -7,7 +7,7 @@ from typing import List, Literal
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
UiLanguageStatus = Literal["complete", "incomplete", "generating"]
@@ -20,7 +20,7 @@ class I18nEntry(BaseModel):
"db.management.files.name" for backend data objects.
key: German plaintext (the canonical identifier across all sets).
value: For xx (base set): UI context description for AI translation.
- For language sets (de, en, …): the translated text.
+ For language sets (de, en, ...): the translated text.
"""
context: str = Field(
@@ -37,17 +37,15 @@ class I18nEntry(BaseModel):
)
+@i18nModel("UI-Sprachset")
class UiLanguageSet(PowerOnModel):
- """One row per language. id = ISO 639-1 code or 'xx' (base set).
-
- The xx set is the master: key = German plaintext, value = UI context for AI.
- All other sets (incl. de) are AI-generated translations.
- """
+ """Ein Sprachset pro Sprache. id = ISO 639-1 Code oder 'xx' (Basisset). Enthaelt alle Uebersetzungen."""
id: str = Field(
...,
description="ISO 639-1 language code or 'xx' for the base set",
json_schema_extra={
+ "label": "Code",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": True,
@@ -57,6 +55,7 @@ class UiLanguageSet(PowerOnModel):
...,
description="Human-readable language name",
json_schema_extra={
+ "label": "Bezeichnung",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": True,
@@ -66,6 +65,7 @@ class UiLanguageSet(PowerOnModel):
default_factory=list,
description="Translation entries: list of {context, key, value}",
json_schema_extra={
+ "label": "Eintraege",
"frontend_type": "textarea",
"frontend_readonly": False,
"frontend_required": False,
@@ -75,6 +75,7 @@ class UiLanguageSet(PowerOnModel):
default="complete",
description="complete | incomplete | generating",
json_schema_extra={
+ "label": "Status",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": True,
@@ -89,21 +90,9 @@ class UiLanguageSet(PowerOnModel):
default=False,
description="True only for the xx base set",
json_schema_extra={
+ "label": "Standard",
"frontend_type": "boolean",
"frontend_readonly": False,
"frontend_required": False,
},
)
-
-
-registerModelLabels(
- "UiLanguageSet",
- {"en": "UI Language Set", "de": "UI-Sprachset"},
- {
- "id": {"en": "Code", "de": "Code"},
- "label": {"en": "Label", "de": "Bezeichnung"},
- "entries": {"en": "Entries", "de": "Einträge"},
- "status": {"en": "Status", "de": "Status"},
- "isDefault": {"en": "Default", "de": "Standard"},
- },
-)
diff --git a/modules/datamodels/datamodelUtils.py b/modules/datamodels/datamodelUtils.py
index 1088cb31..d187687e 100644
--- a/modules/datamodels/datamodelUtils.py
+++ b/modules/datamodels/datamodelUtils.py
@@ -2,20 +2,40 @@
# All rights reserved.
"""Utility datamodels: Prompt, TextMultilingual."""
-from typing import Dict, Optional
+from typing import Any, Dict, Optional
from pydantic import BaseModel, Field, field_validator
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
+@i18nModel("Prompt")
class Prompt(PowerOnModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- mandateId: str = Field(default="", description="ID of the mandate this prompt belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- isSystem: bool = Field(default=False, description="System prompt visible to all users (read-only for non-SysAdmin)", json_schema_extra={"frontend_type": "boolean", "frontend_readonly": True, "frontend_required": False})
- content: str = Field(description="Content of the prompt", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": True})
- name: str = Field(description="Name of the prompt", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": True})
-
+ """Benutzer- oder System-Prompt fuer die KI."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ mandateId: str = Field(
+ default="",
+ description="ID of the mandate this prompt belongs to",
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ isSystem: bool = Field(
+ default=False,
+ description="System prompt visible to all users (read-only for non-SysAdmin)",
+ json_schema_extra={"label": "System", "frontend_type": "boolean", "frontend_readonly": True, "frontend_required": False},
+ )
+ content: str = Field(
+ description="Content of the prompt",
+ json_schema_extra={"label": "Inhalt", "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": True},
+ )
+ name: str = Field(
+ description="Name of the prompt",
+ json_schema_extra={"label": "Name", "frontend_type": "text", "frontend_readonly": False, "frontend_required": True},
+ )
+
@field_validator('isSystem', mode='before')
@classmethod
def _coerceIsSystem(cls, v):
@@ -23,62 +43,64 @@ class Prompt(PowerOnModel):
if v is None:
return False
return v
-registerModelLabels(
- "Prompt",
- {"en": "Prompt", "fr": "Invite"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat"},
- "isSystem": {"en": "System", "fr": "Système"},
- "content": {"en": "Content", "fr": "Contenu"},
- "name": {"en": "Name", "fr": "Nom"},
- },
-)
class TextMultilingual(BaseModel):
- """
- Multilingual text field supporting multiple languages.
- Default languages: en (English), ge (German), fr (French), it (Italian)
- English (en) is the default/required language.
- """
+ """Multilingual text field. Language codes follow ISO 639-1 (en, de, fr, it, …)."""
en: str = Field(description="English text (default language, required)")
- ge: Optional[str] = Field(None, description="German text")
+ de: Optional[str] = Field(None, description="German text")
fr: Optional[str] = Field(None, description="French text")
it: Optional[str] = Field(None, description="Italian text")
-
+
@field_validator('en')
@classmethod
- def validate_en_required(cls, v):
- """Ensure English text is not empty"""
+ def _validateEnRequired(cls, v):
if not v or not v.strip():
raise ValueError("English text (en) is required and cannot be empty")
return v
-
+
def model_dump(self, **kwargs) -> Dict[str, str]:
- """Return as dictionary, filtering out None values"""
result = {}
- for lang in ['en', 'ge', 'fr', 'it']:
- value = getattr(self, lang, None)
+ for key in self.model_fields:
+ value = getattr(self, key, None)
if value is not None:
- result[lang] = value
+ result[key] = value
return result
-
+
@classmethod
def from_dict(cls, data: Dict[str, str]) -> 'TextMultilingual':
- """Create TextMultilingual from dictionary"""
- return cls(
- en=data.get('en', ''),
- ge=data.get('ge'),
- fr=data.get('fr'),
- it=data.get('it')
- )
-
+ fields = {k: data[k] for k in cls.model_fields if k in data}
+ fields.setdefault('en', '')
+ return cls(**fields)
+
def get_text(self, lang: str = 'en') -> str:
- """Get text for a specific language, fallback to English if not available"""
+ """Get text for *lang*. Falls back to English."""
value = getattr(self, lang, None)
if value:
return value
- return self.en # Fallback to English
+ return self.en
+
+ @classmethod
+ def fromUniform(cls, text: str) -> "TextMultilingual":
+ """Same string in all languages (bootstrap / i18n key until per-language values exist in DB)."""
+ t = text.strip()
+ if not t:
+ raise ValueError("Text must be non-empty")
+ return cls(en=t, de=t, fr=t, it=t)
+def coerce_text_multilingual(val: Any) -> TextMultilingual:
+ """Normalize str, dict, or TextMultilingual for Role.description and similar fields."""
+ if isinstance(val, TextMultilingual):
+ return val
+ if isinstance(val, dict):
+ if not val:
+ return TextMultilingual.fromUniform("—")
+ d = {k: val[k] for k in TextMultilingual.model_fields if k in val and val[k] is not None}
+ if not d.get("en"):
+ d["en"] = (d.get("de") or d.get("fr") or "—").strip() or "—"
+ return TextMultilingual(**{k: d[k] for k in TextMultilingual.model_fields if k in d})
+ if isinstance(val, str) and val.strip():
+ return TextMultilingual.fromUniform(val)
+ return TextMultilingual.fromUniform("—")
+
diff --git a/modules/datamodels/datamodelWorkflow.py b/modules/datamodels/datamodelWorkflow.py
index 1a1e49e8..490d9fb0 100644
--- a/modules/datamodels/datamodelWorkflow.py
+++ b/modules/datamodels/datamodelWorkflow.py
@@ -6,45 +6,52 @@ Workflow execution models for action definitions, AI responses, and workflow-lev
from typing import Dict, Any, List, Optional, TYPE_CHECKING
from pydantic import BaseModel, Field
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.shared.jsonUtils import extractJsonString, tryParseJson, repairBrokenJson
# Import DocumentReferenceList at runtime (needed for ActionDefinition)
from modules.datamodels.datamodelDocref import DocumentReferenceList
+@i18nModel("Aktionsdefinition")
class ActionDefinition(BaseModel):
"""Action definition with selection and parameters from planning phase"""
# Core action selection (Stage 1)
- action: str = Field(description="Compound action name (method.action)")
- actionObjective: str = Field(description="Objective for this action")
+ action: str = Field(description="Compound action name (method.action)", json_schema_extra={"label": "Aktion"})
+ actionObjective: str = Field(description="Objective for this action", json_schema_extra={"label": "Aktionsziel"})
userMessage: Optional[str] = Field(
None,
- description="User-friendly message in user's language explaining what this action will do (generated by AI in prompts)"
+ description="User-friendly message in user's language explaining what this action will do (generated by AI in prompts)",
+ json_schema_extra={"label": "Benutzernachricht"},
)
parametersContext: Optional[str] = Field(
None,
- description="Context for parameter generation"
+ description="Context for parameter generation",
+ json_schema_extra={"label": "Parameter-Kontext"},
)
learnings: List[str] = Field(
default_factory=list,
- description="Learnings from previous actions"
+ description="Learnings from previous actions",
+ json_schema_extra={"label": "Erkenntnisse"},
)
# Resources (ALWAYS defined in Stage 1 if action needs them)
documentList: Optional[DocumentReferenceList] = Field(
None,
- description="Document references (ALWAYS defined in Stage 1 if action needs documents)"
+ description="Document references (ALWAYS defined in Stage 1 if action needs documents)",
+ json_schema_extra={"label": "Dokumentenliste"},
)
connectionReference: Optional[str] = Field(
None,
- description="Connection reference (ALWAYS defined in Stage 1 if action needs connection)"
+ description="Connection reference (ALWAYS defined in Stage 1 if action needs connection)",
+ json_schema_extra={"label": "Verbindungsreferenz"},
)
# Parameters (may be defined in Stage 1 OR Stage 2, depending on action and actionObjective)
parameters: Optional[Dict[str, Any]] = Field(
None,
- description="Action-specific parameters (generated in Stage 2 for complex actions, or inferred from actionObjective for simple actions)"
+ description="Action-specific parameters (generated in Stage 2 for complex actions, or inferred from actionObjective for simple actions)",
+ json_schema_extra={"label": "Parameter"},
)
def hasParameters(self) -> bool:
@@ -75,34 +82,47 @@ class ActionDefinition(BaseModel):
self.connectionReference = connectionRef
+@i18nModel("KI-Antwort-Metadaten")
class AiResponseMetadata(BaseModel):
"""Metadata for AI response (varies by operation type)."""
# Document Generation Metadata
- title: Optional[str] = Field(None, description="Document title")
- filename: Optional[str] = Field(None, description="Document filename")
+ title: Optional[str] = Field(None, description="Document title", json_schema_extra={"label": "Titel"})
+ filename: Optional[str] = Field(None, description="Document filename", json_schema_extra={"label": "Dateiname"})
# Operation-Specific Metadata
- operationType: Optional[str] = Field(None, description="Type of operation performed")
- schemaVersion: Optional[str] = Field(None, description="Schema version (e.g., 'parameters_v1')", alias="schema")
- extractionMethod: Optional[str] = Field(None, description="Method used for extraction")
- sourceDocuments: Optional[List[str]] = Field(None, description="Source document references")
+ operationType: Optional[str] = Field(None, description="Type of operation performed", json_schema_extra={"label": "Vorgangstyp"})
+ schemaVersion: Optional[str] = Field(
+ None,
+ description="Schema version (e.g., 'parameters_v1')",
+ alias="schema",
+ json_schema_extra={"label": "Schema-Version"},
+ )
+ extractionMethod: Optional[str] = Field(None, description="Method used for extraction", json_schema_extra={"label": "Extraktionsmethode"})
+ sourceDocuments: Optional[List[str]] = Field(None, description="Source document references", json_schema_extra={"label": "Quelldokumente"})
# Additional metadata (for extensibility)
- additionalData: Optional[Dict[str, Any]] = Field(None, description="Additional operation-specific metadata")
-
-
-class DocumentData(BaseModel):
- """Single document in response"""
- documentName: str = Field(description="Document name")
- documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)")
- mimeType: str = Field(description="MIME type of the document")
- sourceJson: Optional[Dict[str, Any]] = Field(
+ additionalData: Optional[Dict[str, Any]] = Field(
None,
- description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)"
+ description="Additional operation-specific metadata",
+ json_schema_extra={"label": "Zusätzliche Daten"},
)
+@i18nModel("Dokumentdaten")
+class DocumentData(BaseModel):
+ """Single document in response"""
+ documentName: str = Field(description="Document name", json_schema_extra={"label": "Dokumentname"})
+ documentData: Any = Field(description="Document data (can be str, bytes, dict, etc.)", json_schema_extra={"label": "Dokumentdaten"})
+ mimeType: str = Field(description="MIME type of the document", json_schema_extra={"label": "MIME-Typ"})
+ sourceJson: Optional[Dict[str, Any]] = Field(
+ None,
+ description="Source JSON structure (preserved when rendering to xlsx/docx/pdf)",
+ json_schema_extra={"label": "Quell-JSON"},
+ )
+
+
+@i18nModel("Extraktionsparameter")
class ExtractContentParameters(BaseModel):
"""Parameters for extraction action.
@@ -110,24 +130,34 @@ class ExtractContentParameters(BaseModel):
All action parameter models follow this pattern: defined in the same module as the action.
However, since this is a workflow-level model used across the system, it's defined here.
"""
- documentList: DocumentReferenceList = Field(description="Document references to extract content from")
+ documentList: DocumentReferenceList = Field(
+ description="Document references to extract content from",
+ json_schema_extra={"label": "Dokumentenliste"},
+ )
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
None,
- description="Extraction options (determined dynamically based on task and document characteristics)"
+ description="Extraction options (determined dynamically based on task and document characteristics)",
+ json_schema_extra={"label": "Extraktionsoptionen"},
)
+@i18nModel("KI-Antwort")
class AiResponse(BaseModel):
"""Unified response from all AI calls (planning, text, documents)"""
- content: str = Field(description="Response content (JSON string for planning, text for analysis, unified JSON for documents)")
+ content: str = Field(
+ description="Response content (JSON string for planning, text for analysis, unified JSON for documents)",
+ json_schema_extra={"label": "Inhalt"},
+ )
metadata: Optional[AiResponseMetadata] = Field(
None,
- description="Response metadata (varies by operation type)"
+ description="Response metadata (varies by operation type)",
+ json_schema_extra={"label": "Metadaten"},
)
documents: Optional[List[DocumentData]] = Field(
None,
- description="Generated documents (only for document generation operations)"
+ description="Generated documents (only for document generation operations)",
+ json_schema_extra={"label": "Dokumente"},
)
def toJson(self) -> Dict[str, Any]:
@@ -186,278 +216,88 @@ class AiResponse(BaseModel):
# Workflow-level models
+@i18nModel("Anfragekontext")
class RequestContext(BaseModel):
"""Normalized request context from user input"""
- originalPrompt: str = Field(description="Original user prompt")
+ originalPrompt: str = Field(description="Original user prompt", json_schema_extra={"label": "Ursprüngliche Eingabe"})
documents: List[Any] = Field( # ChatDocument - forward reference
default_factory=list,
- description="Documents provided by user"
+ description="Documents provided by user",
+ json_schema_extra={"label": "Dokumente"},
)
- userLanguage: str = Field(description="User's language")
+ userLanguage: str = Field(description="User's language", json_schema_extra={"label": "Benutzersprache"})
detectedComplexity: str = Field(
- description="Complexity level: simple, moderate, complex"
+ description="Complexity level: simple, moderate, complex",
+ json_schema_extra={"label": "Erkannte Komplexität"},
)
- requiresDocuments: bool = Field(default=False, description="Whether request requires documents")
- requiresWebResearch: bool = Field(default=False, description="Whether request requires web research")
- requiresAnalysis: bool = Field(default=False, description="Whether request requires analysis")
- expectedOutputFormat: Optional[str] = Field(None, description="Expected output format")
- expectedOutputType: Optional[str] = Field(None, description="Expected output type: answer, document, analysis")
+ requiresDocuments: bool = Field(default=False, description="Whether request requires documents", json_schema_extra={"label": "Benötigt Dokumente"})
+ requiresWebResearch: bool = Field(default=False, description="Whether request requires web research", json_schema_extra={"label": "Benötigt Web-Recherche"})
+ requiresAnalysis: bool = Field(default=False, description="Whether request requires analysis", json_schema_extra={"label": "Benötigt Analyse"})
+ expectedOutputFormat: Optional[str] = Field(None, description="Expected output format", json_schema_extra={"label": "Erwartetes Ausgabeformat"})
+ expectedOutputType: Optional[str] = Field(None, description="Expected output type: answer, document, analysis", json_schema_extra={"label": "Erwarteter Ausgabetyp"})
+@i18nModel("Verständnis-Ergebnis")
class UnderstandingResult(BaseModel):
"""Result from initial understanding phase (combined AI call)"""
parameters: Dict[str, Any] = Field(
default_factory=dict,
- description="Basic parameters (language, format, detail level)"
+ description="Basic parameters (language, format, detail level)",
+ json_schema_extra={"label": "Parameter"},
)
intention: Dict[str, Any] = Field(
default_factory=dict,
- description="User intention (primaryGoal, secondaryGoals, intentionType)"
+ description="User intention (primaryGoal, secondaryGoals, intentionType)",
+ json_schema_extra={"label": "Absicht"},
)
context: Dict[str, Any] = Field(
default_factory=dict,
- description="Extracted context (topics, requirements, constraints)"
+ description="Extracted context (topics, requirements, constraints)",
+ json_schema_extra={"label": "Kontext"},
)
documentReferences: List[Dict[str, Any]] = Field(
default_factory=list,
- description="Document references with purpose and relevance"
+ description="Document references with purpose and relevance",
+ json_schema_extra={"label": "Dokumentenreferenzen"},
)
tasks: List["TaskDefinition"] = Field( # Forward reference
default_factory=list,
- description="Task definitions with deliverables"
+ description="Task definitions with deliverables",
+ json_schema_extra={"label": "Aufgaben"},
)
+@i18nModel("Aufgabenbeschreibung")
class TaskDefinition(BaseModel):
"""Task definition from understanding phase"""
- id: str = Field(description="Task identifier")
- objective: str = Field(description="Task objective")
+ id: str = Field(description="Task identifier", json_schema_extra={"label": "Aufgaben-ID"})
+ objective: str = Field(description="Task objective", json_schema_extra={"label": "Ziel"})
deliverable: Dict[str, Any] = Field(
- description="Deliverable specification (type, format, style, detailLevel)"
+ description="Deliverable specification (type, format, style, detailLevel)",
+ json_schema_extra={"label": "Lieferobjekt"},
)
- requiresWebResearch: bool = Field(default=False, description="Whether task requires web research")
- requiresDocumentAnalysis: bool = Field(default=False, description="Whether task requires document analysis")
- requiresContentGeneration: bool = Field(default=True, description="Whether task requires content generation")
+ requiresWebResearch: bool = Field(default=False, description="Whether task requires web research", json_schema_extra={"label": "Benötigt Web-Recherche"})
+ requiresDocumentAnalysis: bool = Field(default=False, description="Whether task requires document analysis", json_schema_extra={"label": "Benötigt Dokumentenanalyse"})
+ requiresContentGeneration: bool = Field(default=True, description="Whether task requires content generation", json_schema_extra={"label": "Benötigt Inhaltserstellung"})
requiredDocuments: List[str] = Field(
default_factory=list,
- description="Document references needed for this task"
+ description="Document references needed for this task",
+ json_schema_extra={"label": "Benötigte Dokumente"},
)
extractionOptions: Optional[Any] = Field( # ExtractionOptions - forward reference
None,
- description="Extraction options for document processing (determined dynamically based on task and document characteristics)"
+ description="Extraction options for document processing (determined dynamically based on task and document characteristics)",
+ json_schema_extra={"label": "Extraktionsoptionen"},
)
-class TaskResult(BaseModel):
+@i18nModel("Workflow-Aufgabenergebnis")
+class WorkflowTaskResult(BaseModel):
"""Result from task execution"""
- taskId: str = Field(description="Task identifier")
- actionResult: Any = Field(description="ActionResult from task execution") # ActionResult - forward reference
-
-
-# Register model labels for UI
-registerModelLabels(
- "RequestContext",
- {"en": "Request Context", "fr": "Contexte de la demande"},
- {
- "originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
- "documents": {"en": "Documents", "fr": "Documents"},
- "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
- "detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
- "requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
- "requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
- "requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
- "expectedOutputFormat": {"en": "Expected Output Format", "fr": "Format de sortie attendu"},
- "expectedOutputType": {"en": "Expected Output Type", "fr": "Type de sortie attendu"},
- },
-)
-
-registerModelLabels(
- "UnderstandingResult",
- {"en": "Understanding Result", "fr": "Résultat de compréhension"},
- {
- "parameters": {"en": "Parameters", "fr": "Paramètres"},
- "intention": {"en": "Intention", "fr": "Intention"},
- "context": {"en": "Context", "fr": "Contexte"},
- "documentReferences": {"en": "Document References", "fr": "Références de documents"},
- "tasks": {"en": "Tasks", "fr": "Tâches"},
- },
-)
-
-registerModelLabels(
- "TaskDefinition",
- {"en": "Task Definition", "fr": "Définition de tâche"},
- {
- "id": {"en": "Task ID", "fr": "ID de la tâche"},
- "objective": {"en": "Objective", "fr": "Objectif"},
- "deliverable": {"en": "Deliverable", "fr": "Livrable"},
- "requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
- "requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de documents"},
- "requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
- "requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
- "extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
- },
-)
-
-registerModelLabels(
- "TaskResult",
- {"en": "Task Result", "fr": "Résultat de tâche"},
- {
- "taskId": {"en": "Task ID", "fr": "ID de la tâche"},
- "actionResult": {"en": "Action Result", "fr": "Résultat de l'action"},
- },
-)
-
-registerModelLabels(
- "RequestContext",
- {"en": "Request Context", "fr": "Contexte de la demande"},
- {
- "originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
- "documents": {"en": "Documents", "fr": "Documents"},
- "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
- "detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
- "requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
- "requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
- "requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
- "expectedOutputFormat": {"en": "Expected Output Format", "fr": "Format de sortie attendu"},
- "expectedOutputType": {"en": "Expected Output Type", "fr": "Type de sortie attendu"},
- },
-)
-
-registerModelLabels(
- "UnderstandingResult",
- {"en": "Understanding Result", "fr": "Résultat de compréhension"},
- {
- "parameters": {"en": "Parameters", "fr": "Paramètres"},
- "intention": {"en": "Intention", "fr": "Intention"},
- "context": {"en": "Context", "fr": "Contexte"},
- "documentReferences": {"en": "Document References", "fr": "Références de documents"},
- "tasks": {"en": "Tasks", "fr": "Tâches"},
- },
-)
-
-registerModelLabels(
- "TaskDefinition",
- {"en": "Task Definition", "fr": "Définition de tâche"},
- {
- "id": {"en": "Task ID", "fr": "ID de la tâche"},
- "objective": {"en": "Objective", "fr": "Objectif"},
- "deliverable": {"en": "Deliverable", "fr": "Livrable"},
- "requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
- "requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de documents"},
- "requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
- "requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
- "extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
- },
-)
-
-registerModelLabels(
- "TaskResult",
- {"en": "Task Result", "fr": "Résultat de tâche"},
- {
- "taskId": {"en": "Task ID", "fr": "ID de la tâche"},
- "actionResult": {"en": "Action Result", "fr": "Résultat de l'action"},
- },
-)
-
-# Register model labels for UI
-registerModelLabels(
- "ActionDefinition",
- {"en": "Action Definition", "fr": "Définition d'action"},
- {
- "action": {"en": "Action", "fr": "Action"},
- "actionObjective": {"en": "Action Objective", "fr": "Objectif de l'action"},
- "parametersContext": {"en": "Parameters Context", "fr": "Contexte des paramètres"},
- "learnings": {"en": "Learnings", "fr": "Apprentissages"},
- "documentList": {"en": "Document List", "fr": "Liste de documents"},
- "connectionReference": {"en": "Connection Reference", "fr": "Référence de connexion"},
- "parameters": {"en": "Parameters", "fr": "Paramètres"},
- },
-)
-
-registerModelLabels(
- "AiResponse",
- {"en": "AI Response", "fr": "Réponse IA"},
- {
- "content": {"en": "Content", "fr": "Contenu"},
- "metadata": {"en": "Metadata", "fr": "Métadonnées"},
- "documents": {"en": "Documents", "fr": "Documents"},
- },
-)
-
-registerModelLabels(
- "AiResponseMetadata",
- {"en": "AI Response Metadata", "fr": "Métadonnées de réponse IA"},
- {
- "title": {"en": "Title", "fr": "Titre"},
- "filename": {"en": "Filename", "fr": "Nom de fichier"},
- "operationType": {"en": "Operation Type", "fr": "Type d'opération"},
- "schemaVersion": {"en": "Schema Version", "fr": "Version du schéma"},
- "extractionMethod": {"en": "Extraction Method", "fr": "Méthode d'extraction"},
- "sourceDocuments": {"en": "Source Documents", "fr": "Documents sources"},
- },
-)
-
-registerModelLabels(
- "DocumentData",
- {"en": "Document Data", "fr": "Données de document"},
- {
- "documentName": {"en": "Document Name", "fr": "Nom du document"},
- "documentData": {"en": "Document Data", "fr": "Données du document"},
- "mimeType": {"en": "MIME Type", "fr": "Type MIME"},
- },
-)
-
-registerModelLabels(
- "RequestContext",
- {"en": "Request Context", "fr": "Contexte de requête"},
- {
- "originalPrompt": {"en": "Original Prompt", "fr": "Invite originale"},
- "documents": {"en": "Documents", "fr": "Documents"},
- "userLanguage": {"en": "User Language", "fr": "Langue de l'utilisateur"},
- "detectedComplexity": {"en": "Detected Complexity", "fr": "Complexité détectée"},
- "requiresDocuments": {"en": "Requires Documents", "fr": "Nécessite des documents"},
- "requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
- "requiresAnalysis": {"en": "Requires Analysis", "fr": "Nécessite une analyse"},
- },
-)
-
-registerModelLabels(
- "UnderstandingResult",
- {"en": "Understanding Result", "fr": "Résultat de compréhension"},
- {
- "parameters": {"en": "Parameters", "fr": "Paramètres"},
- "intention": {"en": "Intention", "fr": "Intention"},
- "context": {"en": "Context", "fr": "Contexte"},
- "documentReferences": {"en": "Document References", "fr": "Références de documents"},
- "tasks": {"en": "Tasks", "fr": "Tâches"},
- },
-)
-
-registerModelLabels(
- "TaskDefinition",
- {"en": "Task Definition", "fr": "Définition de tâche"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "objective": {"en": "Objective", "fr": "Objectif"},
- "deliverable": {"en": "Deliverable", "fr": "Livrable"},
- "requiresWebResearch": {"en": "Requires Web Research", "fr": "Nécessite une recherche web"},
- "requiresDocumentAnalysis": {"en": "Requires Document Analysis", "fr": "Nécessite une analyse de document"},
- "requiresContentGeneration": {"en": "Requires Content Generation", "fr": "Nécessite une génération de contenu"},
- "requiredDocuments": {"en": "Required Documents", "fr": "Documents requis"},
- "extractionOptions": {"en": "Extraction Options", "fr": "Options d'extraction"},
- },
-)
-
-registerModelLabels(
- "TaskResult",
- {"en": "Task Result", "fr": "Résultat de tâche"},
- {
- "taskId": {"en": "Task ID", "fr": "ID de tâche"},
- "actionResult": {"en": "Action Result", "fr": "Résultat d'action"},
- },
-)
+ taskId: str = Field(description="Task identifier", json_schema_extra={"label": "Aufgaben-ID"})
+ actionResult: Any = Field(description="ActionResult from task execution", json_schema_extra={"label": "Aktionsergebnis"}) # ActionResult - forward reference
diff --git a/modules/datamodels/datamodelWorkflowActions.py b/modules/datamodels/datamodelWorkflowActions.py
index 8bac1fd5..09c07c14 100644
--- a/modules/datamodels/datamodelWorkflowActions.py
+++ b/modules/datamodels/datamodelWorkflowActions.py
@@ -6,85 +6,97 @@ from typing import Optional, Any, Union, List, Dict, Callable, Awaitable
from pydantic import BaseModel, Field
from modules.datamodels.datamodelChat import ActionResult
from modules.shared.frontendTypes import FrontendType
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
+@i18nModel("Workflow-Aktionsparameter")
class WorkflowActionParameter(BaseModel):
"""
Parameter schema definition for a workflow action.
-
+
This defines the structure and UI rendering for a single action parameter,
NOT the actual parameter values (those are in ActionDefinition.parameters).
"""
- name: str = Field(description="Parameter name")
- type: str = Field(description="Python type as string: 'str', 'int', 'bool', 'List[str]', etc.")
- frontendType: FrontendType = Field(description="UI rendering type (from global FrontendType enum)")
+ name: str = Field(
+ description="Parameter name",
+ json_schema_extra={"label": "Name"},
+ )
+ type: str = Field(
+ description="Python type as string: 'str', 'int', 'bool', 'List[str]', etc.",
+ json_schema_extra={"label": "Typ"},
+ )
+ frontendType: FrontendType = Field(
+ description="UI rendering type (from global FrontendType enum)",
+ json_schema_extra={"label": "Frontend-Typ"},
+ )
frontendOptions: Optional[Union[str, List[str]]] = Field(
None,
- description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or list of strings (e.g., ['txt', 'json']). For custom types, this is automatically set to the API endpoint."
+ description="Options for select/multiselect/custom types. String reference (e.g., 'user.connection') or list of strings (e.g., ['txt', 'json']). For custom types, this is automatically set to the API endpoint.",
+ json_schema_extra={"label": "Frontend-Optionen"},
+ )
+ required: bool = Field(
+ False,
+ description="Whether parameter is required",
+ json_schema_extra={"label": "Pflichtfeld"},
+ )
+ default: Optional[Any] = Field(
+ None,
+ description="Default value",
+ json_schema_extra={"label": "Standard"},
+ )
+ description: str = Field(
+ "",
+ description="Parameter description",
+ json_schema_extra={"label": "Beschreibung"},
)
- required: bool = Field(False, description="Whether parameter is required")
- default: Optional[Any] = Field(None, description="Default value")
- description: str = Field("", description="Parameter description")
validation: Optional[Dict[str, Any]] = Field(
None,
- description="Validation rules (e.g., {'min': 1, 'max': 100})"
+ description="Validation rules (e.g., {'min': 1, 'max': 100})",
+ json_schema_extra={"label": "Validierung"},
)
+@i18nModel("Workflow-Aktionsdefinition")
class WorkflowActionDefinition(BaseModel):
"""
Complete schema definition of a workflow action.
-
+
This defines the metadata, parameters, and execution function for an action.
This is different from datamodelWorkflow.ActionDefinition which contains
actual execution values (action, actionObjective, parameters with values).
-
+
This class defines the ACTION SCHEMA, not the execution plan.
"""
actionId: str = Field(
- description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')"
+ description="Unique action identifier for RBAC (format: 'module.actionName', e.g., 'outlook.readEmails')",
+ json_schema_extra={"label": "Aktions-ID"},
+ )
+ description: str = Field(
+ description="Action description",
+ json_schema_extra={"label": "Beschreibung"},
)
- description: str = Field(description="Action description")
parameters: Dict[str, WorkflowActionParameter] = Field(
default_factory=dict,
- description="Parameter schema definitions"
+ description="Parameter schema definitions",
+ json_schema_extra={"label": "Parameter"},
)
execute: Optional[Callable] = Field(
None,
- description="Execution function - async function that takes parameters dict and returns ActionResult. Set dynamically."
+ description="Execution function - async function that takes parameters dict and returns ActionResult. Set dynamically.",
+ json_schema_extra={"label": "Ausfuehrung"},
+ )
+ category: Optional[str] = Field(
+ None,
+ description="Action category for grouping",
+ json_schema_extra={"label": "Kategorie"},
+ )
+ tags: List[str] = Field(
+ default_factory=list,
+ description="Tags for search/filtering",
+ json_schema_extra={"label": "Tags"},
+ )
+ dynamicMode: bool = Field(
+ False,
+ description="Whether this action is available in dynamic workflow mode (only tagged actions are visible in action planning and refinement prompts)",
+ json_schema_extra={"label": "Dynamischer Modus"},
)
- category: Optional[str] = Field(None, description="Action category for grouping")
- tags: List[str] = Field(default_factory=list, description="Tags for search/filtering")
- dynamicMode: bool = Field(False, description="Whether this action is available in dynamic workflow mode (only tagged actions are visible in action planning and refinement prompts)")
-
-
-# Register model labels for UI
-registerModelLabels(
- "WorkflowActionDefinition",
- {"en": "Workflow Action Definition", "fr": "Définition d'action de workflow"},
- {
- "actionId": {"en": "Action ID", "fr": "ID d'action"},
- "description": {"en": "Description", "fr": "Description"},
- "parameters": {"en": "Parameters", "fr": "Paramètres"},
- "category": {"en": "Category", "fr": "Catégorie"},
- "tags": {"en": "Tags", "fr": "Étiquettes"},
- "dynamicMode": {"en": "Dynamic Mode", "fr": "Mode dynamique"},
- },
-)
-
-registerModelLabels(
- "WorkflowActionParameter",
- {"en": "Workflow Action Parameter", "fr": "Paramètre d'action de workflow"},
- {
- "name": {"en": "Name", "fr": "Nom"},
- "type": {"en": "Type", "fr": "Type"},
- "frontendType": {"en": "Frontend Type", "fr": "Type frontend"},
- "frontendOptions": {"en": "Frontend Options", "fr": "Options frontend"},
- "required": {"en": "Required", "fr": "Requis"},
- "default": {"en": "Default", "fr": "Par défaut"},
- "description": {"en": "Description", "fr": "Description"},
- "validation": {"en": "Validation", "fr": "Validation"},
- },
-)
-
diff --git a/modules/features/chatbot/mainChatbot.py b/modules/features/chatbot/mainChatbot.py
index 33f8ae2f..79f970c6 100644
--- a/modules/features/chatbot/mainChatbot.py
+++ b/modules/features/chatbot/mainChatbot.py
@@ -12,14 +12,14 @@ logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "chatbot"
-FEATURE_LABEL = {"en": "Chatbot", "de": "Chatbot", "fr": "Chatbot"}
+FEATURE_LABEL = "Chatbot"
FEATURE_ICON = "mdi-robot"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.chatbot.conversations",
- "label": {"en": "Conversations", "de": "Konversationen", "fr": "Conversations"},
+ "label": "Konversationen",
"meta": {"area": "conversations"}
}
]
@@ -28,22 +28,22 @@ UI_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.chatbot.startStream",
- "label": {"en": "Start Chat (Stream)", "de": "Chat starten (Stream)", "fr": "Démarrer chat (Stream)"},
+ "label": "Chat starten (Stream)",
"meta": {"endpoint": "/api/chatbot/{instanceId}/start/stream", "method": "POST"}
},
{
"objectKey": "resource.feature.chatbot.stop",
- "label": {"en": "Stop Chat", "de": "Chat stoppen", "fr": "Arrêter chat"},
+ "label": "Chat stoppen",
"meta": {"endpoint": "/api/chatbot/{instanceId}/stop/{workflowId}", "method": "POST"}
},
{
"objectKey": "resource.feature.chatbot.threads",
- "label": {"en": "Get Threads", "de": "Threads abrufen", "fr": "Récupérer threads"},
+ "label": "Threads abrufen",
"meta": {"endpoint": "/api/chatbot/{instanceId}/threads", "method": "GET"}
},
{
"objectKey": "resource.feature.chatbot.delete",
- "label": {"en": "Delete Chat", "de": "Chat löschen", "fr": "Supprimer chat"},
+ "label": "Chat löschen",
"meta": {"endpoint": "/api/chatbot/{instanceId}/{workflowId}", "method": "DELETE"}
},
]
@@ -74,11 +74,7 @@ REQUIRED_SERVICES = [
TEMPLATE_ROLES = [
{
"roleLabel": "chatbot-viewer",
- "description": {
- "en": "Chatbot Viewer - View chat threads (read-only)",
- "de": "Chatbot Betrachter - Chat-Threads ansehen (nur lesen)",
- "fr": "Visualiseur Chatbot - Consulter les threads (lecture seule)"
- },
+ "description": "Chatbot Betrachter - Chat-Threads ansehen (nur lesen)",
"accessRules": [
# UI: only threads view, NO active chat
{"context": "UI", "item": "ui.feature.chatbot.threads", "view": True},
@@ -90,11 +86,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "chatbot-user",
- "description": {
- "en": "Chatbot User - Use the chatbot and manage own threads",
- "de": "Chatbot Benutzer - Chatbot nutzen und eigene Threads verwalten",
- "fr": "Utilisateur Chatbot - Utiliser le chatbot et gérer ses threads"
- },
+ "description": "Chatbot Benutzer - Chatbot nutzen und eigene Threads verwalten",
"accessRules": [
# UI: full access to all views
{"context": "UI", "item": "ui.feature.chatbot.conversations", "view": True},
@@ -110,11 +102,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "chatbot-admin",
- "description": {
- "en": "Chatbot Admin - Full access to all chatbot features",
- "de": "Chatbot Admin - Vollzugriff auf alle Chatbot-Funktionen",
- "fr": "Administrateur Chatbot - Accès complet à toutes les fonctions chatbot"
- },
+ "description": "Chatbot Admin - Vollzugriff auf alle Chatbot-Funktionen",
"accessRules": [
# Full UI access
{"context": "UI", "item": None, "view": True},
@@ -391,7 +379,8 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
-
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
+
rootInterface = getRootInterface()
# Get existing template roles for this feature (Pydantic models)
@@ -412,7 +401,7 @@ def _syncTemplateRolesToDb() -> int:
# Create new template role
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None, # Global template
featureInstanceId=None,
diff --git a/modules/features/chatbot/routeFeatureChatbot.py b/modules/features/chatbot/routeFeatureChatbot.py
index 821e7ae9..fa7ab93c 100644
--- a/modules/features/chatbot/routeFeatureChatbot.py
+++ b/modules/features/chatbot/routeFeatureChatbot.py
@@ -32,6 +32,8 @@ from modules.features.chatbot.interfaceFeatureChatbot import ChatbotConversation
# Import chatbot feature
from modules.features.chatbot import chatProcess
from modules.features.chatbot.mainChatbot import getEventManager
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureChatbot")
# Pre-warm AI connectors when this router loads (before first request).
# Ensures connectors are ready; avoids 4–8 s delay on first chatbot message.
@@ -265,7 +267,7 @@ async def stream_chatbot_start(
if not workflow:
raise HTTPException(
status_code=500,
- detail="Failed to create or load workflow"
+ detail=routeApiMsg("Failed to create or load workflow")
)
# Get event queue for the workflow
@@ -562,7 +564,7 @@ def delete_chatbot(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to delete workflow"
+ detail=routeApiMsg("Failed to delete workflow")
)
return {
diff --git a/modules/features/commcoach/mainCommcoach.py b/modules/features/commcoach/mainCommcoach.py
index d21da056..3b4f66c1 100644
--- a/modules/features/commcoach/mainCommcoach.py
+++ b/modules/features/commcoach/mainCommcoach.py
@@ -11,23 +11,23 @@ from typing import Dict, List, Any
logger = logging.getLogger(__name__)
FEATURE_CODE = "commcoach"
-FEATURE_LABEL = {"en": "Communication Coach", "de": "Kommunikations-Coach", "fr": "Coach Communication"}
+FEATURE_LABEL = "Kommunikations-Coach"
FEATURE_ICON = "mdi-account-voice"
UI_OBJECTS = [
{
"objectKey": "ui.feature.commcoach.dashboard",
- "label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
+ "label": "Dashboard",
"meta": {"area": "dashboard"}
},
{
"objectKey": "ui.feature.commcoach.coaching",
- "label": {"en": "Coaching & Dossier", "de": "Coaching & Dossier", "fr": "Coaching & Dossier"},
+ "label": "Coaching & Dossier",
"meta": {"area": "coaching"}
},
{
"objectKey": "ui.feature.commcoach.settings",
- "label": {"en": "Settings", "de": "Einstellungen", "fr": "Parametres"},
+ "label": "Einstellungen",
"meta": {"area": "settings"}
},
]
@@ -35,7 +35,7 @@ UI_OBJECTS = [
DATA_OBJECTS = [
{
"objectKey": "data.feature.commcoach.CoachingContext",
- "label": {"en": "Coaching Context", "de": "Coaching-Kontext", "fr": "Contexte coaching"},
+ "label": "Coaching-Kontext",
"meta": {
"table": "CoachingContext",
"fields": ["id", "title", "category", "status"],
@@ -45,7 +45,7 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.commcoach.CoachingSession",
- "label": {"en": "Coaching Session", "de": "Coaching-Session", "fr": "Session coaching"},
+ "label": "Coaching-Session",
"meta": {
"table": "CoachingSession",
"fields": ["id", "contextId", "status", "summary"],
@@ -55,12 +55,12 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.commcoach.CoachingMessage",
- "label": {"en": "Coaching Message", "de": "Coaching-Nachricht", "fr": "Message coaching"},
+ "label": "Coaching-Nachricht",
"meta": {"table": "CoachingMessage", "fields": ["id", "sessionId", "role", "content"]}
},
{
"objectKey": "data.feature.commcoach.CoachingTask",
- "label": {"en": "Coaching Task", "de": "Coaching-Aufgabe", "fr": "Tache coaching"},
+ "label": "Coaching-Aufgabe",
"meta": {
"table": "CoachingTask",
"fields": ["id", "contextId", "title", "status"],
@@ -70,27 +70,27 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.commcoach.CoachingScore",
- "label": {"en": "Coaching Score", "de": "Coaching-Score", "fr": "Score coaching"},
+ "label": "Coaching-Score",
"meta": {"table": "CoachingScore", "fields": ["id", "dimension", "score", "trend"]}
},
{
"objectKey": "data.feature.commcoach.CoachingUserProfile",
- "label": {"en": "User Profile", "de": "Benutzerprofil", "fr": "Profil utilisateur"},
+ "label": "Benutzerprofil",
"meta": {"table": "CoachingUserProfile", "fields": ["id", "userId", "dailyReminderEnabled"]}
},
{
"objectKey": "data.feature.commcoach.CoachingPersona",
- "label": {"en": "Coaching Persona", "de": "Coaching-Persona", "fr": "Persona coaching"},
+ "label": "Coaching-Persona",
"meta": {"table": "CoachingPersona", "fields": ["id", "key", "label", "gender"]}
},
{
"objectKey": "data.feature.commcoach.CoachingBadge",
- "label": {"en": "Coaching Badge", "de": "Coaching-Auszeichnung", "fr": "Badge coaching"},
+ "label": "Coaching-Auszeichnung",
"meta": {"table": "CoachingBadge", "fields": ["id", "badgeKey", "awardedAt"]}
},
{
"objectKey": "data.feature.commcoach.*",
- "label": {"en": "All CommCoach Data", "de": "Alle CommCoach-Daten", "fr": "Toutes les donnees CommCoach"},
+ "label": "Alle CommCoach-Daten",
"meta": {"wildcard": True}
},
]
@@ -98,27 +98,27 @@ DATA_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.commcoach.context.create",
- "label": {"en": "Create Context", "de": "Kontext erstellen", "fr": "Creer contexte"},
+ "label": "Kontext erstellen",
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts", "method": "POST"}
},
{
"objectKey": "resource.feature.commcoach.context.archive",
- "label": {"en": "Archive Context", "de": "Kontext archivieren", "fr": "Archiver contexte"},
+ "label": "Kontext archivieren",
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/archive", "method": "POST"}
},
{
"objectKey": "resource.feature.commcoach.session.start",
- "label": {"en": "Start Session", "de": "Session starten", "fr": "Demarrer session"},
+ "label": "Session starten",
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/sessions/start", "method": "POST"}
},
{
"objectKey": "resource.feature.commcoach.session.complete",
- "label": {"en": "Complete Session", "de": "Session abschliessen", "fr": "Terminer session"},
+ "label": "Session abschliessen",
"meta": {"endpoint": "/api/commcoach/{instanceId}/sessions/{sessionId}/complete", "method": "POST"}
},
{
"objectKey": "resource.feature.commcoach.task.manage",
- "label": {"en": "Manage Tasks", "de": "Aufgaben verwalten", "fr": "Gerer taches"},
+ "label": "Aufgaben verwalten",
"meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/tasks", "method": "POST"}
},
]
@@ -126,30 +126,22 @@ RESOURCE_OBJECTS = [
TEMPLATE_ROLES = [
{
"roleLabel": "commcoach-viewer",
- "description": {
- "en": "Communication Coach Viewer - View coaching data (read-only)",
- "de": "Kommunikations-Coach Betrachter - Coaching-Daten ansehen (nur lesen)",
- "fr": "Visualiseur Coach Communication - Consulter les donnees coaching (lecture seule)",
- },
+ "description": "Kommunikations-Coach Betrachter - Coaching-Daten ansehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.commcoach.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.commcoach.coaching", "view": True},
- {"context": "UI", "item": "ui.feature.commcoach.dossier", "view": True},
{"context": "UI", "item": "ui.feature.commcoach.settings", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
+ # Viewer: keine RESOURCE-Endpunkte (Mutationen); Regel explizit fuer konsistente Kontext-Matrix
+ {"context": "RESOURCE", "item": None, "view": False},
],
},
{
"roleLabel": "commcoach-user",
- "description": {
- "en": "Communication Coach User - Can manage own coaching contexts and sessions",
- "de": "Kommunikations-Coach Benutzer - Kann eigene Coaching-Kontexte und Sessions verwalten",
- "fr": "Utilisateur Coach Communication - Peut gerer ses propres contextes et sessions",
- },
+ "description": "Kommunikations-Coach Benutzer - Kann eigene Coaching-Kontexte und Sessions verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.commcoach.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.commcoach.coaching", "view": True},
- {"context": "UI", "item": "ui.feature.commcoach.dossier", "view": True},
{"context": "UI", "item": "ui.feature.commcoach.settings", "view": True},
{"context": "DATA", "item": "data.feature.commcoach.CoachingContext", "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"},
{"context": "DATA", "item": "data.feature.commcoach.CoachingSession", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
@@ -166,11 +158,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "commcoach-admin",
- "description": {
- "en": "Communication Coach Admin - All UI and API actions; data scoped to own records",
- "de": "Kommunikations-Coach Admin - Alle UI- und API-Aktionen; Daten nur eigene Datensaetze",
- "fr": "Administrateur Coach Communication - Toute l'UI et les API; donnees propres",
- },
+ "description": "Kommunikations-Coach Admin - Alle UI- und API-Aktionen; Daten nur eigene Datensaetze",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "RESOURCE", "item": None, "view": True},
@@ -271,6 +259,7 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
rootInterface = getRootInterface()
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
@@ -287,7 +276,7 @@ def _syncTemplateRolesToDb() -> int:
else:
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
diff --git a/modules/features/commcoach/routeFeatureCommcoach.py b/modules/features/commcoach/routeFeatureCommcoach.py
index 8ffd3eca..99ae798e 100644
--- a/modules/features/commcoach/routeFeatureCommcoach.py
+++ b/modules/features/commcoach/routeFeatureCommcoach.py
@@ -33,6 +33,8 @@ from .datamodelCommcoach import (
StartSessionRequest, CreatePersonaRequest, UpdatePersonaRequest,
)
from .serviceCommcoach import CommcoachService, emitSessionEvent, getSessionEventQueue, cleanupSessionEvents
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureCommcoach")
logger = logging.getLogger(__name__)
_activeProcessTasks: dict = {}
@@ -78,14 +80,14 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
raise HTTPException(status_code=404, detail=f"Feature instance '{instanceId}' not found")
mandateId = instance.get("mandateId") if isinstance(instance, dict) else getattr(instance, "mandateId", None)
if not mandateId:
- raise HTTPException(status_code=500, detail="Feature instance has no mandateId")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Feature instance has no mandateId"))
return str(mandateId)
def _validateOwnership(record: dict, context: RequestContext, fieldName: str = "userId") -> None:
"""Strict ownership check. SysAdmin does NOT bypass for content access."""
if record.get(fieldName) != str(context.user.id):
- raise HTTPException(status_code=404, detail="Not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Not found"))
# =========================================================================
@@ -158,7 +160,7 @@ async def getContext(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
tasks = interface.getTasks(contextId, userId)
@@ -187,7 +189,7 @@ async def updateContext(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
updates = body.model_dump(exclude_none=True)
@@ -208,7 +210,7 @@ async def deleteContext(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
interface.deleteContext(contextId)
@@ -228,7 +230,7 @@ async def archiveContext(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
updated = interface.updateContext(contextId, {"status": CoachingContextStatus.ARCHIVED.value})
@@ -249,7 +251,7 @@ async def activateContext(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
updated = interface.updateContext(contextId, {"status": CoachingContextStatus.ACTIVE.value})
@@ -274,7 +276,7 @@ async def listSessions(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
sessions = interface.getSessions(contextId, userId)
@@ -297,7 +299,7 @@ async def startSession(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
activeSession = interface.getActiveSession(contextId, userId)
@@ -420,7 +422,7 @@ async def getSession(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
messages = interface.getMessages(sessionId)
@@ -441,7 +443,7 @@ async def completeSession(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
@@ -466,7 +468,7 @@ async def cancelSession(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
from modules.shared.timeUtils import getIsoTimestamp
@@ -496,11 +498,11 @@ async def sendMessageStream(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
- raise HTTPException(status_code=400, detail="Session is not active")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Session is not active"))
contextId = session.get("contextId")
service = CommcoachService(context.user, mandateId, instanceId)
@@ -572,15 +574,15 @@ async def sendAudioStream(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
if session.get("status") != CoachingSessionStatus.ACTIVE.value:
- raise HTTPException(status_code=400, detail="Session is not active")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Session is not active"))
audioBody = await request.body()
if not audioBody:
- raise HTTPException(status_code=400, detail="No audio data received")
+ raise HTTPException(status_code=400, detail=routeApiMsg("No audio data received"))
from .serviceCommcoach import _getUserVoicePrefs
language, _ = _getUserVoicePrefs(str(context.user.id), mandateId)
@@ -640,7 +642,7 @@ async def streamSession(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
async def _eventGenerator():
@@ -708,7 +710,7 @@ async def createTask(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
taskData = CoachingTask(
@@ -739,7 +741,7 @@ async def updateTask(
task = interface.getTask(taskId)
if not task:
- raise HTTPException(status_code=404, detail="Task not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
_validateOwnership(task, context)
updates = body.model_dump(exclude_none=True)
@@ -761,7 +763,7 @@ async def updateTaskStatus(
task = interface.getTask(taskId)
if not task:
- raise HTTPException(status_code=404, detail="Task not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
_validateOwnership(task, context)
updates = {"status": body.status.value}
@@ -786,7 +788,7 @@ async def deleteTask(
task = interface.getTask(taskId)
if not task:
- raise HTTPException(status_code=404, detail="Task not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
_validateOwnership(task, context)
interface.deleteTask(taskId)
@@ -867,7 +869,7 @@ async def exportDossier(
ctx = interface.getContext(contextId)
if not ctx:
- raise HTTPException(status_code=404, detail="Context not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Context not found"))
_validateOwnership(ctx, context)
tasks = interface.getTasks(contextId, userId)
@@ -902,7 +904,7 @@ async def exportSession(
session = interface.getSession(sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Session not found"))
_validateOwnership(session, context)
contextId = session.get("contextId")
@@ -983,9 +985,9 @@ async def updatePersonaRoute(
persona = interface.getPersona(personaId)
if not persona:
- raise HTTPException(status_code=404, detail="Persona not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Persona not found"))
if persona.get("category") == "builtin":
- raise HTTPException(status_code=403, detail="Builtin personas cannot be edited")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Builtin personas cannot be edited"))
_validateOwnership(persona, context)
updates = body.model_dump(exclude_none=True)
@@ -1006,9 +1008,9 @@ async def deletePersonaRoute(
persona = interface.getPersona(personaId)
if not persona:
- raise HTTPException(status_code=404, detail="Persona not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Persona not found"))
if persona.get("category") == "builtin":
- raise HTTPException(status_code=403, detail="Builtin personas cannot be deleted")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Builtin personas cannot be deleted"))
_validateOwnership(persona, context)
interface.deletePersona(personaId)
diff --git a/modules/features/commcoach/tests/test_mainCommcoach.py b/modules/features/commcoach/tests/test_mainCommcoach.py
index 6be563b6..bed151c8 100644
--- a/modules/features/commcoach/tests/test_mainCommcoach.py
+++ b/modules/features/commcoach/tests/test_mainCommcoach.py
@@ -17,9 +17,8 @@ class TestFeatureMetadata:
assert FEATURE_CODE == "commcoach"
def test_featureLabel(self):
- assert "de" in FEATURE_LABEL
- assert "en" in FEATURE_LABEL
- assert "Coach" in FEATURE_LABEL["de"]
+ assert isinstance(FEATURE_LABEL, str)
+ assert "Coach" in FEATURE_LABEL
def test_featureIcon(self):
assert FEATURE_ICON.startswith("mdi-")
@@ -37,17 +36,17 @@ class TestFeatureDefinition:
class TestRbacObjects:
def test_uiObjectsExist(self):
objs = getUiObjects()
- assert len(objs) >= 4
+ assert len(objs) >= 3
keys = [o["objectKey"] for o in objs]
assert "ui.feature.commcoach.dashboard" in keys
assert "ui.feature.commcoach.coaching" in keys
- assert "ui.feature.commcoach.dossier" in keys
assert "ui.feature.commcoach.settings" in keys
def test_uiObjectsHaveLabels(self):
for obj in getUiObjects():
assert "label" in obj
- assert "de" in obj["label"]
+ assert isinstance(obj["label"], str)
+ assert len(obj["label"]) > 0
def test_dataObjectsExist(self):
objs = getDataObjects()
@@ -94,7 +93,7 @@ class TestTemplateRoles:
def test_roleHasDescription(self):
for role in getTemplateRoles():
assert "description" in role
- assert "de" in role["description"]
+ assert isinstance(role["description"], str) and len(role["description"].strip()) > 0
def test_roleHasAccessRules(self):
for role in getTemplateRoles():
diff --git a/modules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py b/modules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py
index d8aeef05..e6a5b103 100644
--- a/modules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py
+++ b/modules/features/graphicalEditor/datamodelFeatureGraphicalEditor.py
@@ -6,7 +6,7 @@ from enum import Enum
from typing import Dict, Any, List, Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
@@ -54,437 +54,341 @@ class AutoTemplateScope(str, Enum):
# AutoWorkflow
# ---------------------------------------------------------------------------
+@i18nModel("Workflow")
class AutoWorkflow(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
)
mandateId: str = Field(
description="Mandate ID",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Mandanten-ID"},
)
featureInstanceId: str = Field(
description="Feature instance ID",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Feature-Instanz-ID"},
)
label: str = Field(
description="User-friendly workflow name",
- json_schema_extra={"frontend_type": "text", "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_required": True, "label": "Bezeichnung"},
)
description: Optional[str] = Field(
default=None,
description="Workflow description",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Beschreibung"},
)
tags: List[str] = Field(
default_factory=list,
description="Tags for categorization",
- json_schema_extra={"frontend_type": "tags", "frontend_required": False},
+ json_schema_extra={"frontend_type": "tags", "frontend_required": False, "label": "Tags"},
)
isTemplate: bool = Field(
default=False,
description="Whether this workflow is a template",
- json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
+ json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Ist Vorlage"},
)
templateSourceId: Optional[str] = Field(
default=None,
description="ID of the template this workflow was created from",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Vorlagen-Quelle"},
)
templateScope: Optional[str] = Field(
default=None,
description="Template scope: user, instance, mandate, system (AutoTemplateScope)",
- json_schema_extra={"frontend_type": "select", "frontend_required": False},
+ json_schema_extra={"frontend_type": "select", "frontend_required": False, "label": "Vorlagen-Bereich"},
)
sharedReadOnly: bool = Field(
default=False,
description="If true, shared template is read-only for non-owners",
- json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
+ json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Freigabe nur-lesen"},
)
currentVersionId: Optional[str] = Field(
default=None,
description="ID of the currently published AutoVersion",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Aktuelle Version"},
)
active: bool = Field(
default=True,
description="Whether workflow is active",
- json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
+ json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Aktiv"},
)
eventId: Optional[str] = Field(
default=None,
description="Scheduler event ID for incremental sync",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Event-ID"},
)
notifyOnFailure: bool = Field(
default=True,
description="Send notification (in-app + email) when a run fails",
- json_schema_extra={"frontend_type": "checkbox", "frontend_required": False},
+ json_schema_extra={"frontend_type": "checkbox", "frontend_required": False, "label": "Bei Fehler benachrichtigen"},
)
# Legacy fields kept for backward compatibility during transition
graph: Dict[str, Any] = Field(
default_factory=dict,
description="Graph with nodes and connections (legacy; prefer AutoVersion.graph)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Graph"},
)
invocations: List[Dict[str, Any]] = Field(
default_factory=list,
description="Entry points / starts (manual, form, schedule, webhook, ...)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Starts / Einstiegspunkte"},
)
-registerModelLabels(
- "AutoWorkflow",
- {"en": "Workflow", "de": "Workflow", "fr": "Workflow"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID", "fr": "ID instance"},
- "label": {"en": "Label", "de": "Bezeichnung", "fr": "Libellé"},
- "description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
- "tags": {"en": "Tags", "de": "Tags", "fr": "Tags"},
- "isTemplate": {"en": "Is Template", "de": "Ist Vorlage", "fr": "Est modèle"},
- "templateSourceId": {"en": "Template Source", "de": "Vorlagen-Quelle", "fr": "Source du modèle"},
- "templateScope": {"en": "Template Scope", "de": "Vorlagen-Bereich", "fr": "Portée du modèle"},
- "sharedReadOnly": {"en": "Shared Read-Only", "de": "Freigabe nur-lesen", "fr": "Partage lecture seule"},
- "currentVersionId": {"en": "Current Version", "de": "Aktuelle Version", "fr": "Version actuelle"},
- "active": {"en": "Active", "de": "Aktiv", "fr": "Actif"},
- "eventId": {"en": "Event ID", "de": "Event-ID", "fr": "ID événement"},
- "graph": {"en": "Graph", "de": "Graph", "fr": "Graphe"},
- "invocations": {"en": "Starts / Entry points", "de": "Starts / Einstiegspunkte", "fr": "Points d'entrée"},
- },
-)
-
-
# ---------------------------------------------------------------------------
# AutoVersion
# ---------------------------------------------------------------------------
+@i18nModel("Workflow-Version")
class AutoVersion(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
)
workflowId: str = Field(
description="FK -> AutoWorkflow",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Workflow-ID"},
)
versionNumber: int = Field(
default=1,
description="Incrementing version number",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Version"},
)
status: str = Field(
default=AutoWorkflowStatus.DRAFT.value,
description="Version status: draft, published, archived",
- json_schema_extra={"frontend_type": "select", "frontend_required": False},
+ json_schema_extra={"frontend_type": "select", "frontend_required": False, "label": "Status"},
)
graph: Dict[str, Any] = Field(
default_factory=dict,
description="Graph with nodes and connections (incl. node parameters)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": True},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": True, "label": "Graph"},
)
invocations: List[Dict[str, Any]] = Field(
default_factory=list,
description="Entry points / starts for this version",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Einstiegspunkte"},
)
publishedAt: Optional[float] = Field(
default=None,
description="Timestamp when version was published",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Veröffentlicht am"},
)
publishedBy: Optional[str] = Field(
default=None,
description="User ID who published this version",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Veröffentlicht von"},
)
-registerModelLabels(
- "AutoVersion",
- {"en": "Workflow Version", "de": "Workflow-Version", "fr": "Version workflow"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "workflowId": {"en": "Workflow ID", "de": "Workflow-ID", "fr": "ID workflow"},
- "versionNumber": {"en": "Version", "de": "Version", "fr": "Version"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "graph": {"en": "Graph", "de": "Graph", "fr": "Graphe"},
- "invocations": {"en": "Entry Points", "de": "Einstiegspunkte", "fr": "Points d'entrée"},
- "publishedAt": {"en": "Published At", "de": "Veröffentlicht am", "fr": "Publié le"},
- "publishedBy": {"en": "Published By", "de": "Veröffentlicht von", "fr": "Publié par"},
- },
-)
-
-
# ---------------------------------------------------------------------------
# AutoRun
# ---------------------------------------------------------------------------
+@i18nModel("Workflow-Ausführung")
class AutoRun(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
)
workflowId: str = Field(
description="Workflow ID",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Workflow-ID"},
)
mandateId: Optional[str] = Field(
default=None,
description="Mandate ID for cross-feature querying",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Mandanten-ID"},
)
ownerId: Optional[str] = Field(
default=None,
description="User ID who triggered this run",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Auslöser"},
)
versionId: Optional[str] = Field(
default=None,
description="AutoVersion ID used for this run",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Versions-ID"},
)
status: str = Field(
default=AutoRunStatus.RUNNING.value,
description="Status: running, paused, completed, failed, cancelled",
- json_schema_extra={"frontend_type": "text", "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_required": False, "label": "Status"},
)
trigger: Dict[str, Any] = Field(
default_factory=dict,
description="Trigger info (type, entryPointId, payload, etc.)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Auslöser"},
)
startedAt: Optional[float] = Field(
default=None,
description="Run start timestamp",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Gestartet am"},
)
completedAt: Optional[float] = Field(
default=None,
description="Run completion timestamp",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Abgeschlossen am"},
)
nodeOutputs: Dict[str, Any] = Field(
default_factory=dict,
description="Outputs from executed nodes",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Node-Ausgaben"},
)
currentNodeId: Optional[str] = Field(
default=None,
description="Node ID when paused (human task / email wait)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "Aktueller Knoten"},
)
resumeContext: Dict[str, Any] = Field(
default_factory=dict,
description="Context for resume (connectionMap, inputSources, etc.)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Wiederaufnahme-Kontext"},
)
error: Optional[str] = Field(
default=None,
description="Error message if failed",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False, "label": "Fehler"},
)
costTokens: int = Field(
default=0,
description="Total tokens consumed by AI nodes",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Verbrauchte Tokens"},
)
costCredits: float = Field(
default=0.0,
description="Total credits consumed",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Verbrauchte Credits"},
)
-registerModelLabels(
- "AutoRun",
- {"en": "Workflow Run", "de": "Workflow-Ausführung", "fr": "Exécution workflow"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "workflowId": {"en": "Workflow ID", "de": "Workflow-ID", "fr": "ID workflow"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID", "fr": "ID du mandat"},
- "ownerId": {"en": "Owner", "de": "Auslöser", "fr": "Propriétaire"},
- "versionId": {"en": "Version ID", "de": "Versions-ID", "fr": "ID version"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "trigger": {"en": "Trigger", "de": "Auslöser", "fr": "Déclencheur"},
- "startedAt": {"en": "Started At", "de": "Gestartet am", "fr": "Démarré le"},
- "completedAt": {"en": "Completed At", "de": "Abgeschlossen am", "fr": "Terminé le"},
- "nodeOutputs": {"en": "Node Outputs", "de": "Node-Ausgaben", "fr": "Sorties nœuds"},
- "currentNodeId": {"en": "Current Node", "de": "Aktueller Knoten", "fr": "Nœud actuel"},
- "resumeContext": {"en": "Resume Context", "de": "Wiederaufnahme-Kontext", "fr": "Contexte reprise"},
- "error": {"en": "Error", "de": "Fehler", "fr": "Erreur"},
- "costTokens": {"en": "Tokens Used", "de": "Verbrauchte Tokens", "fr": "Tokens utilisés"},
- "costCredits": {"en": "Credits Used", "de": "Verbrauchte Credits", "fr": "Crédits utilisés"},
- },
-)
-
-
# ---------------------------------------------------------------------------
# AutoStepLog
# ---------------------------------------------------------------------------
+@i18nModel("Schritt-Protokoll")
class AutoStepLog(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
)
runId: str = Field(
description="FK -> AutoRun",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Lauf-ID"},
)
nodeId: str = Field(
description="Node ID in the graph",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knoten-ID"},
)
nodeType: str = Field(
description="Node type (e.g. ai.chat, email.send)",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knotentyp"},
)
status: str = Field(
default=AutoStepStatus.PENDING.value,
description="Step status: pending, running, completed, failed, skipped",
- json_schema_extra={"frontend_type": "text", "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_required": False, "label": "Status"},
)
inputSnapshot: Dict[str, Any] = Field(
default_factory=dict,
description="Snapshot of inputs at execution time",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Eingabe-Snapshot"},
)
output: Dict[str, Any] = Field(
default_factory=dict,
description="Node output",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Ausgabe"},
)
error: Optional[str] = Field(
default=None,
description="Error message if step failed",
- json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_readonly": True, "frontend_required": False, "label": "Fehler"},
)
startedAt: Optional[float] = Field(
default=None,
description="Step start timestamp",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Gestartet am"},
)
completedAt: Optional[float] = Field(
default=None,
description="Step completion timestamp",
- json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "datetime", "frontend_readonly": True, "frontend_required": False, "label": "Abgeschlossen am"},
)
durationMs: Optional[int] = Field(
default=None,
description="Execution duration in milliseconds",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Dauer (ms)"},
)
tokensUsed: int = Field(
default=0,
description="Tokens consumed by this step",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Verbrauchte Tokens"},
)
retryCount: int = Field(
default=0,
description="Number of retries executed",
- json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "number", "frontend_readonly": True, "frontend_required": False, "label": "Wiederholungen"},
)
-registerModelLabels(
- "AutoStepLog",
- {"en": "Step Log", "de": "Schritt-Protokoll", "fr": "Journal d'étape"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "runId": {"en": "Run ID", "de": "Lauf-ID", "fr": "ID exécution"},
- "nodeId": {"en": "Node ID", "de": "Knoten-ID", "fr": "ID nœud"},
- "nodeType": {"en": "Node Type", "de": "Knotentyp", "fr": "Type nœud"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "inputSnapshot": {"en": "Input Snapshot", "de": "Eingabe-Snapshot", "fr": "Snapshot entrée"},
- "output": {"en": "Output", "de": "Ausgabe", "fr": "Sortie"},
- "error": {"en": "Error", "de": "Fehler", "fr": "Erreur"},
- "startedAt": {"en": "Started At", "de": "Gestartet am", "fr": "Démarré le"},
- "completedAt": {"en": "Completed At", "de": "Abgeschlossen am", "fr": "Terminé le"},
- "durationMs": {"en": "Duration (ms)", "de": "Dauer (ms)", "fr": "Durée (ms)"},
- "tokensUsed": {"en": "Tokens Used", "de": "Verbrauchte Tokens", "fr": "Tokens utilisés"},
- "retryCount": {"en": "Retry Count", "de": "Wiederholungen", "fr": "Nombre de tentatives"},
- },
-)
-
-
# ---------------------------------------------------------------------------
# AutoTask
# ---------------------------------------------------------------------------
+@i18nModel("Aufgabe")
class AutoTask(PowerOnModel):
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Primary key",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False, "label": "ID"},
)
runId: str = Field(
description="FK -> AutoRun",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Lauf-ID"},
)
workflowId: str = Field(
description="Workflow ID",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Workflow-ID"},
)
nodeId: str = Field(
description="Node ID in the graph",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knoten-ID"},
)
nodeType: str = Field(
description="Node type: form, approval, upload, comment, review, selection, confirmation",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True, "label": "Knotentyp"},
)
config: Dict[str, Any] = Field(
default_factory=dict,
description="Node config (form schema, approval text, etc.)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Konfiguration"},
)
assigneeId: Optional[str] = Field(
default=None,
description="User ID assigned to complete the task",
- json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False, "label": "Zugewiesen an"},
)
status: str = Field(
default=AutoTaskStatus.PENDING.value,
description="Status: pending, completed, cancelled, expired",
- json_schema_extra={"frontend_type": "text", "frontend_required": False},
+ json_schema_extra={"frontend_type": "text", "frontend_required": False, "label": "Status"},
)
result: Optional[Dict[str, Any]] = Field(
default=None,
description="Task result (form data, approval decision, etc.)",
- json_schema_extra={"frontend_type": "textarea", "frontend_required": False},
+ json_schema_extra={"frontend_type": "textarea", "frontend_required": False, "label": "Ergebnis"},
)
expiresAt: Optional[float] = Field(
default=None,
description="Expiration timestamp for the task",
- json_schema_extra={"frontend_type": "datetime", "frontend_required": False},
+ json_schema_extra={"frontend_type": "datetime", "frontend_required": False, "label": "Läuft ab am"},
)
-registerModelLabels(
- "AutoTask",
- {"en": "Task", "de": "Aufgabe", "fr": "Tâche"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "runId": {"en": "Run ID", "de": "Lauf-ID", "fr": "ID exécution"},
- "workflowId": {"en": "Workflow ID", "de": "Workflow-ID", "fr": "ID workflow"},
- "nodeId": {"en": "Node ID", "de": "Knoten-ID", "fr": "ID nœud"},
- "nodeType": {"en": "Node Type", "de": "Knotentyp", "fr": "Type nœud"},
- "config": {"en": "Config", "de": "Konfiguration", "fr": "Configuration"},
- "assigneeId": {"en": "Assignee", "de": "Zugewiesen an", "fr": "Assigné à"},
- "status": {"en": "Status", "de": "Status", "fr": "Statut"},
- "result": {"en": "Result", "de": "Ergebnis", "fr": "Résultat"},
- "expiresAt": {"en": "Expires At", "de": "Läuft ab am", "fr": "Expire le"},
- },
-)
-
-
# ---------------------------------------------------------------------------
# Backward-compatible aliases for transition period
# ---------------------------------------------------------------------------
diff --git a/modules/features/graphicalEditor/entryPoints.py b/modules/features/graphicalEditor/entryPoints.py
index 2bcc74ce..07129545 100644
--- a/modules/features/graphicalEditor/entryPoints.py
+++ b/modules/features/graphicalEditor/entryPoints.py
@@ -30,22 +30,18 @@ def default_manual_entry_point() -> Dict[str, Any]:
"kind": "manual",
"category": "on_demand",
"enabled": True,
- "title": {
- "de": "Jetzt ausführen",
- "en": "Run now",
- "fr": "Exécuter",
- },
+ "title": "Jetzt ausführen",
"description": {},
"config": {},
}
-def _normalize_title(title: Any) -> Dict[str, str]:
+def _normalize_title(title: Any) -> str:
if isinstance(title, dict):
- return {k: str(v) for k, v in title.items() if v is not None}
+ return str(title.get("de") or title.get("en") or title.get("fr") or "").strip()
if isinstance(title, str) and title.strip():
- return {"de": title, "en": title, "fr": title}
- return {"de": "Start", "en": "Start", "fr": "Départ"}
+ return title.strip()
+ return "Start"
def normalize_invocation_entry(raw: Dict[str, Any]) -> Dict[str, Any]:
diff --git a/modules/features/graphicalEditor/mainGraphicalEditor.py b/modules/features/graphicalEditor/mainGraphicalEditor.py
index 5a8917e2..a2bff9bc 100644
--- a/modules/features/graphicalEditor/mainGraphicalEditor.py
+++ b/modules/features/graphicalEditor/mainGraphicalEditor.py
@@ -21,28 +21,28 @@ REQUIRED_SERVICES = [
{"serviceKey": "clickup", "meta": {"usage": "ClickUp actions"}},
{"serviceKey": "generation", "meta": {"usage": "file.create document rendering"}},
]
-FEATURE_LABEL = {"en": "Graphical Editor", "de": "Grafischer Editor", "fr": "Éditeur graphique"}
+FEATURE_LABEL = "Grafischer Editor"
FEATURE_ICON = "mdi-sitemap"
UI_OBJECTS = [
{
"objectKey": "ui.feature.graphicalEditor.editor",
- "label": {"en": "Editor", "de": "Editor", "fr": "Éditeur"},
+ "label": "Editor",
"meta": {"area": "editor"}
},
{
"objectKey": "ui.feature.graphicalEditor.workflows",
- "label": {"en": "Workflows", "de": "Workflows", "fr": "Workflows"},
+ "label": "Workflows",
"meta": {"area": "workflows"}
},
{
"objectKey": "ui.feature.graphicalEditor.templates",
- "label": {"en": "Templates", "de": "Vorlagen", "fr": "Modèles"},
+ "label": "Vorlagen",
"meta": {"area": "templates"}
},
{
"objectKey": "ui.feature.graphicalEditor.workflows-tasks",
- "label": {"en": "Tasks", "de": "Tasks", "fr": "Tâches"},
+ "label": "Tasks",
"meta": {"area": "tasks"}
},
]
@@ -50,17 +50,17 @@ UI_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.graphicalEditor.dashboard",
- "label": {"en": "Access Dashboard", "de": "Dashboard aufrufen", "fr": "Acceder au tableau de bord"},
+ "label": "Dashboard aufrufen",
"meta": {"endpoint": "/api/workflows/{instanceId}/info", "method": "GET"}
},
{
"objectKey": "resource.feature.graphicalEditor.node-types",
- "label": {"en": "Get Node Types", "de": "Node-Typen abrufen", "fr": "Obtenir types de nœuds"},
+ "label": "Node-Typen abrufen",
"meta": {"endpoint": "/api/workflows/{instanceId}/node-types", "method": "GET"}
},
{
"objectKey": "resource.feature.graphicalEditor.execute",
- "label": {"en": "Execute Workflow", "de": "Workflow ausführen", "fr": "Exécuter le workflow"},
+ "label": "Workflow ausführen",
"meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"}
},
]
@@ -68,11 +68,7 @@ RESOURCE_OBJECTS = [
TEMPLATE_ROLES = [
{
"roleLabel": "graphicalEditor-viewer",
- "description": {
- "en": "GraphicalEditor Viewer - View workflows (read-only)",
- "de": "Grafischer Editor Betrachter - Workflows ansehen (nur lesen)",
- "fr": "Visualiseur Éditeur graphique - Consulter les workflows (lecture seule)",
- },
+ "description": "Grafischer Editor Betrachter - Workflows ansehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows", "view": True},
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows-tasks", "view": True},
@@ -82,11 +78,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "graphicalEditor-user",
- "description": {
- "en": "GraphicalEditor User - Use flow builder",
- "de": "Grafischer Editor Benutzer - Flow-Builder nutzen",
- "fr": "Utilisateur Éditeur graphique - Utiliser le flow builder",
- },
+ "description": "Grafischer Editor Benutzer - Flow-Builder nutzen",
"accessRules": [
{"context": "UI", "item": "ui.feature.graphicalEditor.editor", "view": True},
{"context": "UI", "item": "ui.feature.graphicalEditor.workflows", "view": True},
@@ -100,11 +92,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "graphicalEditor-admin",
- "description": {
- "en": "GraphicalEditor Admin - Full UI and API for the instance; data remains user-scoped (MY)",
- "de": "Grafischer Editor Admin - Volle UI und API für die Instanz; Daten weiterhin benutzerspezifisch (MY)",
- "fr": "Administrateur Éditeur graphique - UI et API complets pour l'instance; donnees limitees a l'utilisateur (MY)",
- },
+ "description": "Grafischer Editor Admin - Volle UI und API für die Instanz; Daten weiterhin benutzerspezifisch (MY)",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "RESOURCE", "item": None, "view": True},
@@ -272,6 +260,7 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
rootInterface = getRootInterface()
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
@@ -285,7 +274,7 @@ def _syncTemplateRolesToDb() -> int:
else:
newRole = Role(
roleLabel=roleLabel,
- description=template.get("description", {}),
+ description=coerce_text_multilingual(template.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/ai.py b/modules/features/graphicalEditor/nodeDefinitions/ai.py
index 8586f9c4..b8a1cc02 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/ai.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/ai.py
@@ -5,14 +5,14 @@ AI_NODES = [
{
"id": "ai.prompt",
"category": "ai",
- "label": {"en": "Prompt", "de": "Prompt", "fr": "Invite"},
- "description": {"en": "Enter a prompt and AI does something", "de": "Prompt eingeben und KI führt aus", "fr": "Entrer une invite et l'IA exécute"},
+ "label": "Prompt",
+ "description": "Prompt eingeben und KI führt aus",
"parameters": [
{"name": "aiPrompt", "type": "string", "required": True, "frontendType": "textarea",
- "description": {"en": "AI prompt", "de": "KI-Prompt", "fr": "Invite IA"}},
+ "description": "KI-Prompt"},
{"name": "outputFormat", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["text", "json", "emailDraft"]},
- "description": {"en": "Output format", "de": "Ausgabeformat", "fr": "Format de sortie"}, "default": "text"},
+ "description": "Ausgabeformat", "default": "text"},
],
"inputs": 1,
"outputs": 1,
@@ -25,11 +25,11 @@ AI_NODES = [
{
"id": "ai.webResearch",
"category": "ai",
- "label": {"en": "Web Research", "de": "Web-Recherche", "fr": "Recherche web"},
- "description": {"en": "Research on the web", "de": "Recherche im Web", "fr": "Recherche sur le web"},
+ "label": "Web-Recherche",
+ "description": "Recherche im Web",
"parameters": [
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
- "description": {"en": "Research query", "de": "Recherche-Anfrage", "fr": "Requête de recherche"}},
+ "description": "Recherche-Anfrage"},
],
"inputs": 1,
"outputs": 1,
@@ -42,12 +42,12 @@ AI_NODES = [
{
"id": "ai.summarizeDocument",
"category": "ai",
- "label": {"en": "Summarize Document", "de": "Dokument zusammenfassen", "fr": "Résumer document"},
- "description": {"en": "Summarize document content", "de": "Dokumentinhalt zusammenfassen", "fr": "Résumer le contenu du document"},
+ "label": "Dokument zusammenfassen",
+ "description": "Dokumentinhalt zusammenfassen",
"parameters": [
{"name": "summaryLength", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["short", "medium", "long"]},
- "description": {"en": "Short, medium, or long", "de": "Kurz, mittel oder lang", "fr": "Court, moyen ou long"}, "default": "medium"},
+ "description": "Kurz, mittel oder lang", "default": "medium"},
],
"inputs": 1,
"outputs": 1,
@@ -60,12 +60,12 @@ AI_NODES = [
{
"id": "ai.translateDocument",
"category": "ai",
- "label": {"en": "Translate Document", "de": "Dokument übersetzen", "fr": "Traduire document"},
- "description": {"en": "Translate document to target language", "de": "Dokument in Zielsprache übersetzen", "fr": "Traduire le document"},
+ "label": "Dokument übersetzen",
+ "description": "Dokument in Zielsprache übersetzen",
"parameters": [
{"name": "targetLanguage", "type": "string", "required": True, "frontendType": "select",
"frontendOptions": {"options": ["en", "de", "fr", "it", "es", "pt", "nl"]},
- "description": {"en": "Target language", "de": "Zielsprache", "fr": "Langue cible"}},
+ "description": "Zielsprache"},
],
"inputs": 1,
"outputs": 1,
@@ -78,12 +78,12 @@ AI_NODES = [
{
"id": "ai.convertDocument",
"category": "ai",
- "label": {"en": "Convert Document", "de": "Dokument konvertieren", "fr": "Convertir document"},
- "description": {"en": "Convert document to another format", "de": "Dokument in anderes Format konvertieren", "fr": "Convertir le document"},
+ "label": "Dokument konvertieren",
+ "description": "Dokument in anderes Format konvertieren",
"parameters": [
{"name": "targetFormat", "type": "string", "required": True, "frontendType": "select",
"frontendOptions": {"options": ["pdf", "docx", "txt", "html", "md"]},
- "description": {"en": "Target format", "de": "Zielformat", "fr": "Format cible"}},
+ "description": "Zielformat"},
],
"inputs": 1,
"outputs": 1,
@@ -96,11 +96,11 @@ AI_NODES = [
{
"id": "ai.generateDocument",
"category": "ai",
- "label": {"en": "Generate Document", "de": "Dokument generieren", "fr": "Générer document"},
- "description": {"en": "Generate document from prompt", "de": "Dokument aus Prompt generieren", "fr": "Générer un document"},
+ "label": "Dokument generieren",
+ "description": "Dokument aus Prompt generieren",
"parameters": [
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
- "description": {"en": "Generation prompt", "de": "Generierungs-Prompt", "fr": "Invite de génération"}},
+ "description": "Generierungs-Prompt"},
],
"inputs": 1,
"outputs": 1,
@@ -113,14 +113,14 @@ AI_NODES = [
{
"id": "ai.generateCode",
"category": "ai",
- "label": {"en": "Generate Code", "de": "Code generieren", "fr": "Générer code"},
- "description": {"en": "Generate code from description", "de": "Code aus Beschreibung generieren", "fr": "Générer du code"},
+ "label": "Code generieren",
+ "description": "Code aus Beschreibung generieren",
"parameters": [
{"name": "prompt", "type": "string", "required": True, "frontendType": "textarea",
- "description": {"en": "Code generation prompt", "de": "Code-Generierungs-Prompt", "fr": "Invite de génération de code"}},
+ "description": "Code-Generierungs-Prompt"},
{"name": "language", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["python", "javascript", "typescript", "java", "csharp", "go"]},
- "description": {"en": "Programming language", "de": "Programmiersprache", "fr": "Langage de programmation"}, "default": "python"},
+ "description": "Programmiersprache", "default": "python"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/clickup.py b/modules/features/graphicalEditor/nodeDefinitions/clickup.py
index 0d3c75af..03504f73 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/clickup.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/clickup.py
@@ -6,26 +6,26 @@ CLICKUP_NODES = [
{
"id": "clickup.searchTasks",
"category": "clickup",
- "label": {"en": "Search tasks", "de": "Aufgaben suchen", "fr": "Rechercher tâches"},
- "description": {"en": "Search tasks in a workspace", "de": "Aufgaben in einem Workspace suchen", "fr": "Rechercher des tâches"},
+ "label": "Aufgaben suchen",
+ "description": "Aufgaben in einem Workspace suchen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
+ "description": "ClickUp-Verbindung"},
{"name": "teamId", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Workspace (team) ID", "de": "Team-/Workspace-ID", "fr": "ID équipe"}},
+ "description": "Team-/Workspace-ID"},
{"name": "query", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Search query", "de": "Suchbegriff", "fr": "Requête"}},
+ "description": "Suchbegriff"},
{"name": "page", "type": "number", "required": False, "frontendType": "number",
- "description": {"en": "Page", "de": "Seite", "fr": "Page"}, "default": 0},
+ "description": "Seite", "default": 0},
{"name": "listId", "type": "string", "required": False, "frontendType": "clickupList",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Search in this list", "de": "In dieser Liste suchen", "fr": "Rechercher dans cette liste"}},
+ "description": "In dieser Liste suchen"},
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Include closed tasks", "de": "Erledigte einbeziehen", "fr": "Inclure terminées"}, "default": False},
+ "description": "Erledigte einbeziehen", "default": False},
{"name": "fullTaskData", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Return full task data", "de": "Vollständige Daten", "fr": "Données complètes"}, "default": False},
+ "description": "Vollständige Daten", "default": False},
{"name": "matchNameOnly", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Match title only", "de": "Nur Titel", "fr": "Titre uniquement"}, "default": True},
+ "description": "Nur Titel", "default": True},
],
"inputs": 1,
"outputs": 1,
@@ -38,18 +38,18 @@ CLICKUP_NODES = [
{
"id": "clickup.listTasks",
"category": "clickup",
- "label": {"en": "List tasks", "de": "Aufgaben auflisten", "fr": "Lister les tâches"},
- "description": {"en": "List tasks in a list", "de": "Aufgaben einer Liste auflisten", "fr": "Lister les tâches"},
+ "label": "Aufgaben auflisten",
+ "description": "Aufgaben einer Liste auflisten",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
+ "description": "ClickUp-Verbindung"},
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "clickupList",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Path to list", "de": "Pfad zur Liste", "fr": "Chemin vers la liste"}},
+ "description": "Pfad zur Liste"},
{"name": "page", "type": "number", "required": False, "frontendType": "number",
- "description": {"en": "Page", "de": "Seite", "fr": "Page"}, "default": 0},
+ "description": "Seite", "default": 0},
{"name": "includeClosed", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Include closed", "de": "Erledigte einbeziehen", "fr": "Inclure terminées"}, "default": False},
+ "description": "Erledigte einbeziehen", "default": False},
],
"inputs": 1,
"outputs": 1,
@@ -62,15 +62,15 @@ CLICKUP_NODES = [
{
"id": "clickup.getTask",
"category": "clickup",
- "label": {"en": "Get task", "de": "Aufgabe abrufen", "fr": "Obtenir la tâche"},
- "description": {"en": "Get one task by ID or path", "de": "Eine Aufgabe abrufen", "fr": "Obtenir une tâche"},
+ "label": "Aufgabe abrufen",
+ "description": "Eine Aufgabe abrufen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
+ "description": "ClickUp-Verbindung"},
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Task ID", "de": "Task-ID", "fr": "ID tâche"}},
+ "description": "Task-ID"},
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Or path .../task/{id}", "de": "Oder Pfad", "fr": "Ou chemin"}},
+ "description": "Oder Pfad"},
],
"inputs": 1,
"outputs": 1,
@@ -83,39 +83,39 @@ CLICKUP_NODES = [
{
"id": "clickup.createTask",
"category": "clickup",
- "label": {"en": "Create task", "de": "Aufgabe erstellen", "fr": "Créer une tâche"},
- "description": {"en": "Create a task in a list", "de": "Aufgabe erstellen", "fr": "Créer une tâche"},
+ "label": "Aufgabe erstellen",
+ "description": "Aufgabe erstellen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
+ "description": "ClickUp-Verbindung"},
{"name": "teamId", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Workspace (team)", "de": "Workspace", "fr": "Équipe"}},
+ "description": "Workspace"},
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "clickupList",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Path to list", "de": "Pfad zur Liste", "fr": "Chemin"}},
+ "description": "Pfad zur Liste"},
{"name": "listId", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "List ID", "de": "Listen-ID", "fr": "ID liste"}},
+ "description": "Listen-ID"},
{"name": "name", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Task name", "de": "Name", "fr": "Nom"}},
+ "description": "Name"},
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
- "description": {"en": "Description", "de": "Beschreibung", "fr": "Description"}},
+ "description": "Beschreibung"},
{"name": "taskStatus", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Status", "de": "Status", "fr": "Statut"}},
+ "description": "Status"},
{"name": "taskPriority", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["1", "2", "3", "4"]},
- "description": {"en": "Priority 1-4", "de": "Priorität 1-4", "fr": "Priorité 1-4"}},
+ "description": "Priorität 1-4"},
{"name": "taskDueDateMs", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Due date (Unix ms)", "de": "Fälligkeit (ms)", "fr": "Échéance (ms)"}},
+ "description": "Fälligkeit (ms)"},
{"name": "taskAssigneeIds", "type": "object", "required": False, "frontendType": "json",
- "description": {"en": "Assignee user ids", "de": "Zugewiesene", "fr": "Assignés"}},
+ "description": "Zugewiesene"},
{"name": "taskTimeEstimateMs", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Time estimate (ms)", "de": "Zeitschätzung (ms)", "fr": "Estimation (ms)"}},
+ "description": "Zeitschätzung (ms)"},
{"name": "taskTimeEstimateHours", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Time estimate (hours)", "de": "Zeitschätzung (h)", "fr": "Heures"}},
+ "description": "Zeitschätzung (h)"},
{"name": "customFieldValues", "type": "object", "required": False, "frontendType": "json",
- "description": {"en": "Custom fields", "de": "Benutzerdefinierte Felder", "fr": "Champs personnalisés"}},
+ "description": "Benutzerdefinierte Felder"},
{"name": "taskFields", "type": "string", "required": False, "frontendType": "json",
- "description": {"en": "Extra JSON (advanced)", "de": "Zusätzliches JSON", "fr": "JSON avancé"}},
+ "description": "Zusätzliches JSON"},
],
"inputs": 1,
"outputs": 1,
@@ -128,19 +128,19 @@ CLICKUP_NODES = [
{
"id": "clickup.updateTask",
"category": "clickup",
- "label": {"en": "Update task", "de": "Aufgabe aktualisieren", "fr": "Mettre à jour la tâche"},
- "description": {"en": "Update task fields", "de": "Felder der Aufgabe ändern", "fr": "Mettre à jour les champs"},
+ "label": "Aufgabe aktualisieren",
+ "description": "Felder der Aufgabe ändern",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
+ "description": "ClickUp-Verbindung"},
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Task ID", "de": "Task-ID", "fr": "ID tâche"}},
+ "description": "Task-ID"},
{"name": "path", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Or path to task", "de": "Oder Pfad", "fr": "Ou chemin"}},
+ "description": "Oder Pfad"},
{"name": "taskUpdateEntries", "type": "object", "required": False, "frontendType": "keyValueRows",
- "description": {"en": "Fields to update", "de": "Zu ändernde Felder", "fr": "Champs à mettre à jour"}},
+ "description": "Zu ändernde Felder"},
{"name": "taskUpdate", "type": "string", "required": False, "frontendType": "json",
- "description": {"en": "JSON body (advanced)", "de": "JSON für API", "fr": "Corps JSON"}},
+ "description": "JSON für API"},
],
"inputs": 1,
"outputs": 1,
@@ -153,17 +153,17 @@ CLICKUP_NODES = [
{
"id": "clickup.uploadAttachment",
"category": "clickup",
- "label": {"en": "Upload attachment", "de": "Anhang hochladen", "fr": "Téléverser pièce jointe"},
- "description": {"en": "Upload file to a task", "de": "Datei an Task anhängen", "fr": "Joindre un fichier"},
+ "label": "Anhang hochladen",
+ "description": "Datei an Task anhängen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "ClickUp connection", "de": "ClickUp-Verbindung", "fr": "Connexion ClickUp"}},
+ "description": "ClickUp-Verbindung"},
{"name": "taskId", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Task ID", "de": "Task-ID", "fr": "ID tâche"}},
+ "description": "Task-ID"},
{"name": "path", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Or path to task", "de": "Oder Pfad", "fr": "Ou chemin"}},
+ "description": "Oder Pfad"},
{"name": "fileName", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "File name", "de": "Dateiname", "fr": "Nom du fichier"}},
+ "description": "Dateiname"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/data.py b/modules/features/graphicalEditor/nodeDefinitions/data.py
index a96c7ee5..e68f3d3d 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/data.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/data.py
@@ -5,12 +5,12 @@ DATA_NODES = [
{
"id": "data.aggregate",
"category": "data",
- "label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"},
- "description": {"en": "Collect results from loop iterations", "de": "Ergebnisse aus Schleifen-Iterationen sammeln", "fr": "Collecter les résultats des itérations"},
+ "label": "Sammeln",
+ "description": "Ergebnisse aus Schleifen-Iterationen sammeln",
"parameters": [
{"name": "mode", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["collect", "concat", "sum", "count"]},
- "description": {"en": "Aggregation mode", "de": "Aggregationsmodus", "fr": "Mode d'agrégation"}, "default": "collect"},
+ "description": "Aggregationsmodus", "default": "collect"},
],
"inputs": 1,
"outputs": 1,
@@ -22,11 +22,11 @@ DATA_NODES = [
{
"id": "data.transform",
"category": "data",
- "label": {"en": "Transform", "de": "Umwandeln", "fr": "Transformer"},
- "description": {"en": "Map and restructure data", "de": "Daten umstrukturieren", "fr": "Restructurer les données"},
+ "label": "Umwandeln",
+ "description": "Daten umstrukturieren",
"parameters": [
{"name": "mappings", "type": "json", "required": True, "frontendType": "mappingTable",
- "description": {"en": "Field mappings", "de": "Feld-Zuordnungen", "fr": "Correspondances"}, "default": []},
+ "description": "Feld-Zuordnungen", "default": []},
],
"inputs": 1,
"outputs": 1,
@@ -38,11 +38,11 @@ DATA_NODES = [
{
"id": "data.filter",
"category": "data",
- "label": {"en": "Filter", "de": "Filtern", "fr": "Filtrer"},
- "description": {"en": "Filter items by condition", "de": "Elemente nach Bedingung filtern", "fr": "Filtrer par condition"},
+ "label": "Filtern",
+ "description": "Elemente nach Bedingung filtern",
"parameters": [
{"name": "condition", "type": "string", "required": True, "frontendType": "filterExpression",
- "description": {"en": "Filter condition", "de": "Filterbedingung", "fr": "Condition de filtre"}},
+ "description": "Filterbedingung"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/email.py b/modules/features/graphicalEditor/nodeDefinitions/email.py
index 87ea5244..8dd6e9c5 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/email.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/email.py
@@ -5,23 +5,23 @@ EMAIL_NODES = [
{
"id": "email.checkEmail",
"category": "email",
- "label": {"en": "Check Email", "de": "E-Mail prüfen", "fr": "Vérifier email"},
- "description": {"en": "Check for new emails", "de": "Neue E-Mails prüfen", "fr": "Vérifier les nouveaux emails"},
+ "label": "E-Mail prüfen",
+ "description": "Neue E-Mails prüfen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "Email account connection", "de": "E-Mail-Konto Verbindung", "fr": "Connexion compte email"}},
+ "description": "E-Mail-Konto Verbindung"},
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Folder (e.g. Inbox)", "de": "Ordner", "fr": "Dossier"}, "default": "Inbox"},
+ "description": "Ordner", "default": "Inbox"},
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
- "description": {"en": "Max emails to fetch", "de": "Max E-Mails", "fr": "Max emails"}, "default": 100},
+ "description": "Max E-Mails", "default": 100},
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Only emails from this address", "de": "Nur von dieser Adresse", "fr": "Seulement de cette adresse"}, "default": ""},
+ "description": "Nur von dieser Adresse", "default": ""},
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Subject must contain", "de": "Betreff muss enthalten", "fr": "Le sujet doit contenir"}, "default": ""},
+ "description": "Betreff muss enthalten", "default": ""},
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Only with attachments", "de": "Nur mit Anhängen", "fr": "Avec pièces jointes"}, "default": False},
+ "description": "Nur mit Anhängen", "default": False},
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Advanced: raw filter", "de": "Erweitert: Filter-Text", "fr": "Avancé: filtre brut"}, "default": ""},
+ "description": "Erweitert: Filter-Text", "default": ""},
],
"inputs": 1,
"outputs": 1,
@@ -34,29 +34,29 @@ EMAIL_NODES = [
{
"id": "email.searchEmail",
"category": "email",
- "label": {"en": "Search Email", "de": "E-Mail suchen", "fr": "Rechercher email"},
- "description": {"en": "Search or find emails", "de": "E-Mails suchen", "fr": "Rechercher des emails"},
+ "label": "E-Mail suchen",
+ "description": "E-Mails suchen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "Email account connection", "de": "E-Mail-Konto Verbindung", "fr": "Connexion compte email"}},
+ "description": "E-Mail-Konto Verbindung"},
{"name": "query", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Search term", "de": "Suchbegriff", "fr": "Terme de recherche"}, "default": ""},
+ "description": "Suchbegriff", "default": ""},
{"name": "folder", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Folder to search", "de": "Ordner", "fr": "Dossier"}, "default": "Inbox"},
+ "description": "Ordner", "default": "Inbox"},
{"name": "limit", "type": "number", "required": False, "frontendType": "number",
- "description": {"en": "Max emails", "de": "Max E-Mails", "fr": "Max emails"}, "default": 100},
+ "description": "Max E-Mails", "default": 100},
{"name": "fromAddress", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "From address", "de": "Von Adresse", "fr": "De l'adresse"}, "default": ""},
+ "description": "Von Adresse", "default": ""},
{"name": "toAddress", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "To address", "de": "An Adresse", "fr": "À l'adresse"}, "default": ""},
+ "description": "An Adresse", "default": ""},
{"name": "subjectContains", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Subject contains", "de": "Betreff enthält", "fr": "Sujet contient"}, "default": ""},
+ "description": "Betreff enthält", "default": ""},
{"name": "bodyContains", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Body contains", "de": "Inhalt enthält", "fr": "Corps contient"}, "default": ""},
+ "description": "Inhalt enthält", "default": ""},
{"name": "hasAttachment", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "With attachments", "de": "Mit Anhängen", "fr": "Avec pièces jointes"}, "default": False},
+ "description": "Mit Anhängen", "default": False},
{"name": "filter", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Advanced: raw KQL", "de": "Erweitert: KQL-Filter", "fr": "Avancé: filtre KQL"}, "default": ""},
+ "description": "Erweitert: KQL-Filter", "default": ""},
],
"inputs": 1,
"outputs": 1,
@@ -69,17 +69,17 @@ EMAIL_NODES = [
{
"id": "email.draftEmail",
"category": "email",
- "label": {"en": "Draft Email", "de": "E-Mail entwerfen", "fr": "Brouillon email"},
- "description": {"en": "Create a draft email", "de": "E-Mail-Entwurf erstellen", "fr": "Créer un brouillon"},
+ "label": "E-Mail entwerfen",
+ "description": "E-Mail-Entwurf erstellen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "Email account", "de": "E-Mail-Konto", "fr": "Compte email"}},
+ "description": "E-Mail-Konto"},
{"name": "subject", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Subject", "de": "Betreff", "fr": "Sujet"}},
+ "description": "Betreff"},
{"name": "body", "type": "string", "required": True, "frontendType": "textarea",
- "description": {"en": "Body", "de": "Inhalt", "fr": "Corps"}},
+ "description": "Inhalt"},
{"name": "to", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Recipient(s)", "de": "Empfänger", "fr": "Destinataire(s)"}, "default": ""},
+ "description": "Empfänger", "default": ""},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/file.py b/modules/features/graphicalEditor/nodeDefinitions/file.py
index 9f5bea7a..bed2cbc7 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/file.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/file.py
@@ -5,26 +5,22 @@ FILE_NODES = [
{
"id": "file.create",
"category": "file",
- "label": {"en": "Create File", "de": "Datei erstellen", "fr": "Créer fichier"},
- "description": {
- "en": "Create a file from context (text/markdown from AI).",
- "de": "Erstellt eine Datei aus Kontext (Text/Markdown von KI).",
- "fr": "Crée un fichier à partir du contexte.",
- },
+ "label": "Datei erstellen",
+ "description": "Erstellt eine Datei aus Kontext (Text/Markdown von KI).",
"parameters": [
{"name": "contentSources", "type": "json", "required": False, "frontendType": "json",
- "description": {"en": "Context source refs", "de": "Kontext-Quellen", "fr": "Sources de contexte"}, "default": []},
+ "description": "Kontext-Quellen", "default": []},
{"name": "outputFormat", "type": "string", "required": True, "frontendType": "select",
"frontendOptions": {"options": ["docx", "pdf", "txt", "html", "md"]},
- "description": {"en": "Output format", "de": "Ausgabeformat", "fr": "Format de sortie"}, "default": "docx"},
+ "description": "Ausgabeformat", "default": "docx"},
{"name": "title", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Document title", "de": "Dokumenttitel", "fr": "Titre du document"}},
+ "description": "Dokumenttitel"},
{"name": "templateName", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["default", "corporate", "minimal"]},
- "description": {"en": "Style preset", "de": "Stil-Vorlage", "fr": "Prését style"}},
+ "description": "Stil-Vorlage"},
{"name": "language", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["de", "en", "fr"]},
- "description": {"en": "Language", "de": "Sprache", "fr": "Langue"}, "default": "de"},
+ "description": "Sprache", "default": "de"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/flow.py b/modules/features/graphicalEditor/nodeDefinitions/flow.py
index c3d0a84d..087f7391 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/flow.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/flow.py
@@ -5,20 +5,20 @@ FLOW_NODES = [
{
"id": "flow.ifElse",
"category": "flow",
- "label": {"en": "If / Else", "de": "Wenn / Sonst", "fr": "Si / Sinon"},
- "description": {"en": "Branch based on condition", "de": "Verzweigung nach Bedingung", "fr": "Branche selon condition"},
+ "label": "Wenn / Sonst",
+ "description": "Verzweigung nach Bedingung",
"parameters": [
{
"name": "condition",
"type": "string",
"required": True,
"frontendType": "condition",
- "description": {"en": "Condition to evaluate", "de": "Bedingung", "fr": "Condition"},
+ "description": "Bedingung",
},
],
"inputs": 1,
"outputs": 2,
- "outputLabels": {"en": ["Yes", "No"], "de": ["Ja", "Nein"], "fr": ["Oui", "Non"]},
+ "outputLabels": ["Ja", "Nein"],
"inputPorts": {0: {"accepts": ["Transit"]}},
"outputPorts": {0: {"schema": "Transit"}, 1: {"schema": "Transit"}},
"executor": "flow",
@@ -27,22 +27,22 @@ FLOW_NODES = [
{
"id": "flow.switch",
"category": "flow",
- "label": {"en": "Switch", "de": "Switch", "fr": "Switch"},
- "description": {"en": "Multiple branches based on value", "de": "Mehrere Zweige nach Wert", "fr": "Branches multiples selon valeur"},
+ "label": "Switch",
+ "description": "Mehrere Zweige nach Wert",
"parameters": [
{
"name": "value",
"type": "string",
"required": True,
"frontendType": "text",
- "description": {"en": "Value to match", "de": "Zu vergleichender Wert", "fr": "Valeur à comparer"},
+ "description": "Zu vergleichender Wert",
},
{
"name": "cases",
"type": "array",
"required": False,
"frontendType": "caseList",
- "description": {"en": "List of cases", "de": "Fälle", "fr": "Cas"},
+ "description": "Fälle",
},
],
"inputs": 1,
@@ -55,15 +55,15 @@ FLOW_NODES = [
{
"id": "flow.loop",
"category": "flow",
- "label": {"en": "Loop / For Each", "de": "Schleife / Für Jedes", "fr": "Boucle / Pour Chaque"},
- "description": {"en": "Iterate over array items", "de": "Über Array-Elemente iterieren", "fr": "Itérer sur les éléments"},
+ "label": "Schleife / Für Jedes",
+ "description": "Über Array-Elemente iterieren",
"parameters": [
{
"name": "items",
"type": "string",
"required": True,
"frontendType": "text",
- "description": {"en": "Path to array (e.g. {{input.items}})", "de": "Pfad zum Array", "fr": "Chemin vers le tableau"},
+ "description": "Pfad zum Array",
},
],
"inputs": 1,
@@ -76,8 +76,8 @@ FLOW_NODES = [
{
"id": "flow.merge",
"category": "flow",
- "label": {"en": "Merge", "de": "Zusammenführen", "fr": "Fusionner"},
- "description": {"en": "Merge multiple branches", "de": "Mehrere Zweige zusammenführen", "fr": "Fusionner plusieurs branches"},
+ "label": "Zusammenführen",
+ "description": "Mehrere Zweige zusammenführen",
"parameters": [
{
"name": "mode",
@@ -85,7 +85,7 @@ FLOW_NODES = [
"required": False,
"frontendType": "select",
"frontendOptions": {"options": ["first", "all", "append"]},
- "description": {"en": "Merge mode", "de": "Zusammenführungsmodus", "fr": "Mode de fusion"},
+ "description": "Zusammenführungsmodus",
"default": "first",
},
],
diff --git a/modules/features/graphicalEditor/nodeDefinitions/input.py b/modules/features/graphicalEditor/nodeDefinitions/input.py
index 4d15de46..20547635 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/input.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/input.py
@@ -5,19 +5,15 @@ INPUT_NODES = [
{
"id": "input.form",
"category": "input",
- "label": {"en": "Form", "de": "Formular", "fr": "Formulaire"},
- "description": {"en": "User fills out a form", "de": "Benutzer füllt ein Formular aus", "fr": "L'utilisateur remplit un formulaire"},
+ "label": "Formular",
+ "description": "Benutzer füllt ein Formular aus",
"parameters": [
{
"name": "fields",
"type": "json",
"required": True,
"frontendType": "fieldBuilder",
- "description": {
- "en": "Form fields: [{name, type, label, required, options?}]",
- "de": "Formularfelder",
- "fr": "Champs du formulaire",
- },
+ "description": "Formularfelder",
"default": [],
},
],
@@ -31,16 +27,16 @@ INPUT_NODES = [
{
"id": "input.approval",
"category": "input",
- "label": {"en": "Approval", "de": "Genehmigung", "fr": "Approbation"},
- "description": {"en": "User approves or rejects", "de": "Benutzer genehmigt oder lehnt ab", "fr": "L'utilisateur approuve ou rejette"},
+ "label": "Genehmigung",
+ "description": "Benutzer genehmigt oder lehnt ab",
"parameters": [
{"name": "title", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Approval title", "de": "Genehmigungstitel", "fr": "Titre"}},
+ "description": "Genehmigungstitel"},
{"name": "description", "type": "string", "required": False, "frontendType": "textarea",
- "description": {"en": "What to approve", "de": "Was genehmigt werden soll", "fr": "Ce qu'il faut approuver"}},
+ "description": "Was genehmigt werden soll"},
{"name": "approvalType", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["generic", "document"]},
- "description": {"en": "Type: document or generic", "de": "Typ: document oder generic", "fr": "Type"}, "default": "generic"},
+ "description": "Typ: document oder generic", "default": "generic"},
],
"inputs": 1,
"outputs": 1,
@@ -52,18 +48,18 @@ INPUT_NODES = [
{
"id": "input.upload",
"category": "input",
- "label": {"en": "Upload", "de": "Upload", "fr": "Téléversement"},
- "description": {"en": "User uploads file(s)", "de": "Benutzer lädt Datei(en) hoch", "fr": "L'utilisateur téléverse des fichiers"},
+ "label": "Upload",
+ "description": "Benutzer lädt Datei(en) hoch",
"parameters": [
{"name": "accept", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Accept string for file input (e.g. .pdf,image/*)", "de": "Accept-String", "fr": "Chaîne accept"}, "default": ""},
+ "description": "Accept-String", "default": ""},
{"name": "allowedTypes", "type": "json", "required": False, "frontendType": "multiselect",
"frontendOptions": {"options": ["pdf", "docx", "xlsx", "pptx", "txt", "csv", "jpg", "png", "gif"]},
- "description": {"en": "Selected file types", "de": "Ausgewählte Dateitypen", "fr": "Types sélectionnés"}, "default": []},
+ "description": "Ausgewählte Dateitypen", "default": []},
{"name": "maxSize", "type": "number", "required": False, "frontendType": "number",
- "description": {"en": "Max file size in MB", "de": "Max. Dateigröße in MB", "fr": "Taille max en Mo"}, "default": 10},
+ "description": "Max. Dateigröße in MB", "default": 10},
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Allow multiple files", "de": "Mehrere Dateien erlauben", "fr": "Autoriser plusieurs fichiers"}, "default": False},
+ "description": "Mehrere Dateien erlauben", "default": False},
],
"inputs": 1,
"outputs": 1,
@@ -75,13 +71,13 @@ INPUT_NODES = [
{
"id": "input.comment",
"category": "input",
- "label": {"en": "Comment", "de": "Kommentar", "fr": "Commentaire"},
- "description": {"en": "User adds a comment", "de": "Benutzer fügt einen Kommentar hinzu", "fr": "L'utilisateur ajoute un commentaire"},
+ "label": "Kommentar",
+ "description": "Benutzer fügt einen Kommentar hinzu",
"parameters": [
{"name": "placeholder", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Placeholder text", "de": "Platzhalter", "fr": "Texte indicatif"}, "default": ""},
+ "description": "Platzhalter", "default": ""},
{"name": "required", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Comment required", "de": "Kommentar erforderlich", "fr": "Commentaire requis"}, "default": True},
+ "description": "Kommentar erforderlich", "default": True},
],
"inputs": 1,
"outputs": 1,
@@ -93,14 +89,14 @@ INPUT_NODES = [
{
"id": "input.review",
"category": "input",
- "label": {"en": "Review", "de": "Prüfung", "fr": "Revue"},
- "description": {"en": "User reviews content", "de": "Benutzer prüft Inhalt", "fr": "L'utilisateur révise le contenu"},
+ "label": "Prüfung",
+ "description": "Benutzer prüft Inhalt",
"parameters": [
{"name": "contentRef", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Reference to content", "de": "Referenz auf Inhalt", "fr": "Référence au contenu"}},
+ "description": "Referenz auf Inhalt"},
{"name": "reviewType", "type": "string", "required": False, "frontendType": "select",
"frontendOptions": {"options": ["generic", "document"]},
- "description": {"en": "Type of review", "de": "Art der Prüfung", "fr": "Type de revue"}, "default": "generic"},
+ "description": "Art der Prüfung", "default": "generic"},
],
"inputs": 1,
"outputs": 1,
@@ -112,13 +108,13 @@ INPUT_NODES = [
{
"id": "input.selection",
"category": "input",
- "label": {"en": "Selection", "de": "Auswahl", "fr": "Sélection"},
- "description": {"en": "User selects from options", "de": "Benutzer wählt aus Optionen", "fr": "L'utilisateur choisit parmi les options"},
+ "label": "Auswahl",
+ "description": "Benutzer wählt aus Optionen",
"parameters": [
{"name": "options", "type": "json", "required": True, "frontendType": "keyValueRows",
- "description": {"en": "Options: [{value, label}]", "de": "Optionen", "fr": "Options"}, "default": []},
+ "description": "Optionen", "default": []},
{"name": "multiple", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Allow multiple selection", "de": "Mehrfachauswahl erlauben", "fr": "Sélection multiple"}, "default": False},
+ "description": "Mehrfachauswahl erlauben", "default": False},
],
"inputs": 1,
"outputs": 1,
@@ -130,15 +126,15 @@ INPUT_NODES = [
{
"id": "input.confirmation",
"category": "input",
- "label": {"en": "Confirmation", "de": "Bestätigung", "fr": "Confirmation"},
- "description": {"en": "User confirms yes/no", "de": "Benutzer bestätigt Ja/Nein", "fr": "L'utilisateur confirme oui/non"},
+ "label": "Bestätigung",
+ "description": "Benutzer bestätigt Ja/Nein",
"parameters": [
{"name": "question", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Question to confirm", "de": "Zu bestätigende Frage", "fr": "Question à confirmer"}},
+ "description": "Zu bestätigende Frage"},
{"name": "confirmLabel", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Label for confirm button", "de": "Label für Bestätigen-Button", "fr": "Libellé confirmer"}, "default": "Confirm"},
+ "description": "Label für Bestätigen-Button", "default": "Confirm"},
{"name": "rejectLabel", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Label for reject button", "de": "Label für Ablehnen-Button", "fr": "Libellé refuser"}, "default": "Reject"},
+ "description": "Label für Ablehnen-Button", "default": "Reject"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/sharepoint.py b/modules/features/graphicalEditor/nodeDefinitions/sharepoint.py
index 5490499f..199285c8 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/sharepoint.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/sharepoint.py
@@ -5,17 +5,17 @@ SHAREPOINT_NODES = [
{
"id": "sharepoint.findFile",
"category": "sharepoint",
- "label": {"en": "Find File", "de": "Datei finden", "fr": "Trouver fichier"},
- "description": {"en": "Find file by path or search", "de": "Datei nach Pfad oder Suche finden", "fr": "Trouver fichier par chemin ou recherche"},
+ "label": "Datei finden",
+ "description": "Datei nach Pfad oder Suche finden",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
+ "description": "SharePoint-Verbindung"},
{"name": "searchQuery", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Search query or path", "de": "Suchanfrage oder Pfad", "fr": "Requête ou chemin"}},
+ "description": "Suchanfrage oder Pfad"},
{"name": "site", "type": "string", "required": False, "frontendType": "text",
- "description": {"en": "Optional site hint", "de": "Optionaler Site-Hinweis", "fr": "Indication de site"}, "default": ""},
+ "description": "Optionaler Site-Hinweis", "default": ""},
{"name": "maxResults", "type": "number", "required": False, "frontendType": "number",
- "description": {"en": "Max results", "de": "Max Ergebnisse", "fr": "Max résultats"}, "default": 1000},
+ "description": "Max Ergebnisse", "default": 1000},
],
"inputs": 1,
"outputs": 1,
@@ -28,14 +28,14 @@ SHAREPOINT_NODES = [
{
"id": "sharepoint.readFile",
"category": "sharepoint",
- "label": {"en": "Read File", "de": "Datei lesen", "fr": "Lire fichier"},
- "description": {"en": "Extract content from file", "de": "Inhalt aus Datei extrahieren", "fr": "Extraire le contenu du fichier"},
+ "label": "Datei lesen",
+ "description": "Inhalt aus Datei extrahieren",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
+ "description": "SharePoint-Verbindung"},
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "File path", "de": "Dateipfad", "fr": "Chemin"}},
+ "description": "Dateipfad"},
],
"inputs": 1,
"outputs": 1,
@@ -48,14 +48,14 @@ SHAREPOINT_NODES = [
{
"id": "sharepoint.uploadFile",
"category": "sharepoint",
- "label": {"en": "Upload File", "de": "Datei hochladen", "fr": "Téléverser fichier"},
- "description": {"en": "Upload file to SharePoint", "de": "Datei zu SharePoint hochladen", "fr": "Téléverser fichier vers SharePoint"},
+ "label": "Datei hochladen",
+ "description": "Datei zu SharePoint hochladen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
+ "description": "SharePoint-Verbindung"},
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFolder",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Target folder path", "de": "Zielordner-Pfad", "fr": "Chemin du dossier cible"}},
+ "description": "Zielordner-Pfad"},
],
"inputs": 1,
"outputs": 1,
@@ -68,14 +68,14 @@ SHAREPOINT_NODES = [
{
"id": "sharepoint.listFiles",
"category": "sharepoint",
- "label": {"en": "List Files", "de": "Dateien auflisten", "fr": "Lister fichiers"},
- "description": {"en": "List files in folder", "de": "Dateien in Ordner auflisten", "fr": "Lister les fichiers"},
+ "label": "Dateien auflisten",
+ "description": "Dateien in Ordner auflisten",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
+ "description": "SharePoint-Verbindung"},
{"name": "pathQuery", "type": "string", "required": False, "frontendType": "sharepointFolder",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Folder path", "de": "Ordnerpfad", "fr": "Chemin du dossier"}, "default": "/"},
+ "description": "Ordnerpfad", "default": "/"},
],
"inputs": 1,
"outputs": 1,
@@ -88,14 +88,14 @@ SHAREPOINT_NODES = [
{
"id": "sharepoint.downloadFile",
"category": "sharepoint",
- "label": {"en": "Download File", "de": "Datei herunterladen", "fr": "Télécharger fichier"},
- "description": {"en": "Download file from path", "de": "Datei vom Pfad herunterladen", "fr": "Télécharger le fichier"},
+ "label": "Datei herunterladen",
+ "description": "Datei vom Pfad herunterladen",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
+ "description": "SharePoint-Verbindung"},
{"name": "pathQuery", "type": "string", "required": True, "frontendType": "sharepointFile",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Full file path", "de": "Vollständiger Dateipfad", "fr": "Chemin complet du fichier"}},
+ "description": "Vollständiger Dateipfad"},
],
"inputs": 1,
"outputs": 1,
@@ -108,17 +108,17 @@ SHAREPOINT_NODES = [
{
"id": "sharepoint.copyFile",
"category": "sharepoint",
- "label": {"en": "Copy File", "de": "Datei kopieren", "fr": "Copier fichier"},
- "description": {"en": "Copy file to destination", "de": "Datei an Ziel kopieren", "fr": "Copier le fichier"},
+ "label": "Datei kopieren",
+ "description": "Datei an Ziel kopieren",
"parameters": [
{"name": "connectionReference", "type": "string", "required": True, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}},
+ "description": "SharePoint-Verbindung"},
{"name": "sourcePath", "type": "string", "required": True, "frontendType": "sharepointFile",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Source file path", "de": "Quelldatei-Pfad", "fr": "Chemin fichier source"}},
+ "description": "Quelldatei-Pfad"},
{"name": "destPath", "type": "string", "required": True, "frontendType": "sharepointFolder",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "Destination folder", "de": "Zielordner", "fr": "Dossier cible"}},
+ "description": "Zielordner"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/triggers.py b/modules/features/graphicalEditor/nodeDefinitions/triggers.py
index ab9d75ed..c25fffbe 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/triggers.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/triggers.py
@@ -5,12 +5,8 @@ TRIGGER_NODES = [
{
"id": "trigger.manual",
"category": "trigger",
- "label": {"en": "Start", "de": "Start", "fr": "Départ"},
- "description": {
- "en": "Manual, API, or background triggers (webhook, email, …).",
- "de": "Manuell, API oder Hintergrund-Starts (Webhook, E-Mail, …).",
- "fr": "Manuel, API ou déclencheurs en arrière-plan.",
- },
+ "label": "Start",
+ "description": "Manuell, API oder Hintergrund-Starts (Webhook, E-Mail, …).",
"parameters": [],
"inputs": 0,
"outputs": 1,
@@ -22,19 +18,15 @@ TRIGGER_NODES = [
{
"id": "trigger.form",
"category": "trigger",
- "label": {"en": "Start (form)", "de": "Start (Formular)", "fr": "Départ (formulaire)"},
- "description": {
- "en": "Form fields are filled at run time; configure fields on this node.",
- "de": "Felder werden beim Start befüllt; konfigurieren Sie die Felder auf dieser Node.",
- "fr": "Les champs sont remplis au démarrage.",
- },
+ "label": "Start (Formular)",
+ "description": "Felder werden beim Start befüllt; konfigurieren Sie die Felder auf dieser Node.",
"parameters": [
{
"name": "formFields",
"type": "json",
"required": False,
"frontendType": "fieldBuilder",
- "description": {"en": "Field definitions", "de": "Felddefinitionen", "fr": "Définitions"},
+ "description": "Felddefinitionen",
},
],
"inputs": 0,
@@ -47,19 +39,15 @@ TRIGGER_NODES = [
{
"id": "trigger.schedule",
"category": "trigger",
- "label": {"en": "Start (schedule)", "de": "Start (Zeitplan)", "fr": "Départ (planification)"},
- "description": {
- "en": "Cron expression for scheduled runs (configure on this node).",
- "de": "Cron-Ausdruck für geplante Läufe.",
- "fr": "Expression cron pour les exécutions planifiées.",
- },
+ "label": "Start (Zeitplan)",
+ "description": "Cron-Ausdruck für geplante Läufe.",
"parameters": [
{
"name": "cron",
"type": "string",
"required": False,
"frontendType": "cron",
- "description": {"en": "Cron expression", "de": "Cron-Ausdruck", "fr": "Expression cron"},
+ "description": "Cron-Ausdruck",
},
],
"inputs": 0,
diff --git a/modules/features/graphicalEditor/nodeDefinitions/trustee.py b/modules/features/graphicalEditor/nodeDefinitions/trustee.py
index 7d57c91c..a242f2ae 100644
--- a/modules/features/graphicalEditor/nodeDefinitions/trustee.py
+++ b/modules/features/graphicalEditor/nodeDefinitions/trustee.py
@@ -5,21 +5,17 @@ TRUSTEE_NODES = [
{
"id": "trustee.refreshAccountingData",
"category": "trustee",
- "label": {"en": "Refresh Accounting Data", "de": "Buchhaltungsdaten aktualisieren", "fr": "Actualiser données comptables"},
- "description": {
- "en": "Import/refresh accounting data from external system (e.g. Abacus).",
- "de": "Buchhaltungsdaten aus externem System importieren/aktualisieren.",
- "fr": "Importer/actualiser les données comptables.",
- },
+ "label": "Buchhaltungsdaten aktualisieren",
+ "description": "Buchhaltungsdaten aus externem System importieren/aktualisieren.",
"parameters": [
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
- "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
+ "description": "Trustee Feature-Instanz-ID"},
{"name": "forceRefresh", "type": "boolean", "required": False, "frontendType": "checkbox",
- "description": {"en": "Force re-import", "de": "Import erzwingen", "fr": "Forcer la réimportation"}, "default": False},
+ "description": "Import erzwingen", "default": False},
{"name": "dateFrom", "type": "string", "required": False, "frontendType": "date",
- "description": {"en": "Start date (YYYY-MM-DD)", "de": "Startdatum", "fr": "Date début"}, "default": ""},
+ "description": "Startdatum", "default": ""},
{"name": "dateTo", "type": "string", "required": False, "frontendType": "date",
- "description": {"en": "End date (YYYY-MM-DD)", "de": "Enddatum", "fr": "Date fin"}, "default": ""},
+ "description": "Enddatum", "default": ""},
],
"inputs": 1,
"outputs": 1,
@@ -32,22 +28,18 @@ TRUSTEE_NODES = [
{
"id": "trustee.extractFromFiles",
"category": "trustee",
- "label": {"en": "Extract Documents", "de": "Dokumente extrahieren", "fr": "Extraire documents"},
- "description": {
- "en": "Extract document type and data from PDF/JPG via AI.",
- "de": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren.",
- "fr": "Extraire type et données de PDF/JPG par IA.",
- },
+ "label": "Dokumente extrahieren",
+ "description": "Dokumenttyp und Daten aus PDF/JPG per AI extrahieren.",
"parameters": [
{"name": "connectionReference", "type": "string", "required": False, "frontendType": "userConnection",
- "description": {"en": "SharePoint connection", "de": "SharePoint-Verbindung", "fr": "Connexion SharePoint"}, "default": ""},
+ "description": "SharePoint-Verbindung", "default": ""},
{"name": "sharepointFolder", "type": "string", "required": False, "frontendType": "sharepointFolder",
"frontendOptions": {"dependsOn": "connectionReference"},
- "description": {"en": "SharePoint folder path", "de": "SharePoint-Ordnerpfad", "fr": "Chemin dossier SharePoint"}, "default": ""},
+ "description": "SharePoint-Ordnerpfad", "default": ""},
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
- "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
+ "description": "Trustee Feature-Instanz-ID"},
{"name": "prompt", "type": "string", "required": False, "frontendType": "textarea",
- "description": {"en": "AI prompt for extraction", "de": "AI-Prompt für Extraktion", "fr": "Prompt IA"}, "default": ""},
+ "description": "AI-Prompt für Extraktion", "default": ""},
],
"inputs": 1,
"outputs": 1,
@@ -60,17 +52,13 @@ TRUSTEE_NODES = [
{
"id": "trustee.processDocuments",
"category": "trustee",
- "label": {"en": "Process Documents", "de": "Dokumente verarbeiten", "fr": "Traiter documents"},
- "description": {
- "en": "Create TrusteeDocument + TrusteePosition from extraction result.",
- "de": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen.",
- "fr": "Créer TrusteeDocument + TrusteePosition.",
- },
+ "label": "Dokumente verarbeiten",
+ "description": "TrusteeDocument + TrusteePosition aus Extraktionsergebnis erstellen.",
"parameters": [
{"name": "documentList", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Reference to extraction result", "de": "Referenz auf Ergebnis", "fr": "Référence au résultat"}},
+ "description": "Referenz auf Ergebnis"},
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
- "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
+ "description": "Trustee Feature-Instanz-ID"},
],
"inputs": 1,
"outputs": 1,
@@ -83,17 +71,13 @@ TRUSTEE_NODES = [
{
"id": "trustee.syncToAccounting",
"category": "trustee",
- "label": {"en": "Sync to Accounting", "de": "In Buchhaltung synchronisieren", "fr": "Synchroniser comptabilité"},
- "description": {
- "en": "Push trustee positions to accounting system.",
- "de": "Trustee-Positionen in Buchhaltungssystem übertragen.",
- "fr": "Transférer les positions vers la comptabilité.",
- },
+ "label": "In Buchhaltung synchronisieren",
+ "description": "Trustee-Positionen in Buchhaltungssystem übertragen.",
"parameters": [
{"name": "documentList", "type": "string", "required": True, "frontendType": "text",
- "description": {"en": "Reference to processed documents", "de": "Referenz auf Ergebnis", "fr": "Référence au résultat"}},
+ "description": "Referenz auf Ergebnis"},
{"name": "featureInstanceId", "type": "string", "required": True, "frontendType": "hidden",
- "description": {"en": "Trustee feature instance ID", "de": "Trustee Feature-Instanz-ID", "fr": "ID instance Trustee"}},
+ "description": "Trustee Feature-Instanz-ID"},
],
"inputs": 1,
"outputs": 1,
diff --git a/modules/features/graphicalEditor/nodeRegistry.py b/modules/features/graphicalEditor/nodeRegistry.py
index 3c42608b..81cce9c7 100644
--- a/modules/features/graphicalEditor/nodeRegistry.py
+++ b/modules/features/graphicalEditor/nodeRegistry.py
@@ -61,16 +61,16 @@ def getNodeTypesForApi(
nodes = getNodeTypes(services, language)
localized = [_localizeNode(n, language) for n in nodes]
categories = [
- {"id": "trigger", "label": {"en": "Trigger", "de": "Trigger", "fr": "Déclencheur"}},
- {"id": "input", "label": {"en": "Input/Human", "de": "Eingabe/Mensch", "fr": "Entrée/Humain"}},
- {"id": "flow", "label": {"en": "Flow", "de": "Ablauf", "fr": "Flux"}},
- {"id": "data", "label": {"en": "Data", "de": "Daten", "fr": "Données"}},
- {"id": "ai", "label": {"en": "AI", "de": "KI", "fr": "IA"}},
- {"id": "file", "label": {"en": "File", "de": "Datei", "fr": "Fichier"}},
- {"id": "email", "label": {"en": "Email", "de": "E-Mail", "fr": "Email"}},
- {"id": "sharepoint", "label": {"en": "SharePoint", "de": "SharePoint", "fr": "SharePoint"}},
- {"id": "clickup", "label": {"en": "ClickUp", "de": "ClickUp", "fr": "ClickUp"}},
- {"id": "trustee", "label": {"en": "Trustee", "de": "Treuhand", "fr": "Fiduciaire"}},
+ {"id": "trigger", "label": "Trigger"},
+ {"id": "input", "label": "Eingabe/Mensch"},
+ {"id": "flow", "label": "Ablauf"},
+ {"id": "data", "label": "Daten"},
+ {"id": "ai", "label": "KI"},
+ {"id": "file", "label": "Datei"},
+ {"id": "email", "label": "E-Mail"},
+ {"id": "sharepoint", "label": "SharePoint"},
+ {"id": "clickup", "label": "ClickUp"},
+ {"id": "trustee", "label": "Treuhand"},
]
catalogSerialized = {}
diff --git a/modules/features/graphicalEditor/portTypes.py b/modules/features/graphicalEditor/portTypes.py
index 523109b0..7de0e6fd 100644
--- a/modules/features/graphicalEditor/portTypes.py
+++ b/modules/features/graphicalEditor/portTypes.py
@@ -24,7 +24,7 @@ logger = logging.getLogger(__name__)
class PortField(BaseModel):
name: str
type: str # str, int, bool, List[str], List[Document], Dict[str,Any]
- description: Dict[str, str] = {} # {en, de, fr}
+ description: str = ""
required: bool = True
@@ -57,97 +57,97 @@ class OutputPortDef(BaseModel):
PORT_TYPE_CATALOG: Dict[str, PortSchema] = {
"DocumentList": PortSchema(name="DocumentList", fields=[
PortField(name="documents", type="List[Document]",
- description={"en": "List of documents", "de": "Dokumentenliste", "fr": "Liste de documents"}),
+ description="Dokumentenliste"),
]),
"FileList": PortSchema(name="FileList", fields=[
PortField(name="files", type="List[File]",
- description={"en": "List of files", "de": "Dateiliste", "fr": "Liste de fichiers"}),
+ description="Dateiliste"),
]),
"EmailDraft": PortSchema(name="EmailDraft", fields=[
PortField(name="subject", type="str",
- description={"en": "Subject", "de": "Betreff", "fr": "Sujet"}),
+ description="Betreff"),
PortField(name="body", type="str",
- description={"en": "Body", "de": "Inhalt", "fr": "Corps"}),
+ description="Inhalt"),
PortField(name="to", type="List[str]",
- description={"en": "Recipients", "de": "Empfänger", "fr": "Destinataires"}),
+ description="Empfänger"),
PortField(name="cc", type="List[str]", required=False,
- description={"en": "CC", "de": "CC", "fr": "CC"}),
+ description="CC"),
PortField(name="attachments", type="List[Document]", required=False,
- description={"en": "Attachments", "de": "Anhänge", "fr": "Pièces jointes"}),
+ description="Anhänge"),
]),
"EmailList": PortSchema(name="EmailList", fields=[
PortField(name="emails", type="List[Email]",
- description={"en": "Emails", "de": "E-Mails", "fr": "Emails"}),
+ description="E-Mails"),
]),
"TaskList": PortSchema(name="TaskList", fields=[
PortField(name="tasks", type="List[Task]",
- description={"en": "Tasks", "de": "Aufgaben", "fr": "Tâches"}),
+ description="Aufgaben"),
]),
"TaskResult": PortSchema(name="TaskResult", fields=[
PortField(name="success", type="bool",
- description={"en": "Success", "de": "Erfolg", "fr": "Succès"}),
+ description="Erfolg"),
PortField(name="taskId", type="str",
- description={"en": "Task ID", "de": "Aufgaben-ID", "fr": "ID tâche"}),
+ description="Aufgaben-ID"),
PortField(name="task", type="Dict",
- description={"en": "Task data", "de": "Aufgabendaten", "fr": "Données tâche"}),
+ description="Aufgabendaten"),
]),
"FormPayload": PortSchema(name="FormPayload", fields=[
PortField(name="payload", type="Dict[str,Any]",
- description={"en": "Form data", "de": "Formulardaten", "fr": "Données formulaire"}),
+ description="Formulardaten"),
]),
"AiResult": PortSchema(name="AiResult", fields=[
PortField(name="prompt", type="str",
- description={"en": "Prompt", "de": "Prompt", "fr": "Invite"}),
+ description="Prompt"),
PortField(name="response", type="str",
- description={"en": "Response text", "de": "Antworttext", "fr": "Texte réponse"}),
+ description="Antworttext"),
PortField(name="responseData", type="Dict", required=False,
- description={"en": "Structured response", "de": "Strukturierte Antwort", "fr": "Réponse structurée"}),
+ description="Strukturierte Antwort"),
PortField(name="context", type="str",
- description={"en": "Context", "de": "Kontext", "fr": "Contexte"}),
+ description="Kontext"),
PortField(name="documents", type="List[Document]",
- description={"en": "Documents", "de": "Dokumente", "fr": "Documents"}),
+ description="Dokumente"),
]),
"BoolResult": PortSchema(name="BoolResult", fields=[
PortField(name="result", type="bool",
- description={"en": "Result", "de": "Ergebnis", "fr": "Résultat"}),
+ description="Ergebnis"),
PortField(name="reason", type="str", required=False,
- description={"en": "Reason", "de": "Begründung", "fr": "Raison"}),
+ description="Begründung"),
]),
"TextResult": PortSchema(name="TextResult", fields=[
PortField(name="text", type="str",
- description={"en": "Text", "de": "Text", "fr": "Texte"}),
+ description="Text"),
]),
"LoopItem": PortSchema(name="LoopItem", fields=[
PortField(name="currentItem", type="Any",
- description={"en": "Current item", "de": "Aktuelles Element", "fr": "Élément courant"}),
+ description="Aktuelles Element"),
PortField(name="currentIndex", type="int",
- description={"en": "Current index", "de": "Aktueller Index", "fr": "Index courant"}),
+ description="Aktueller Index"),
PortField(name="items", type="List[Any]",
- description={"en": "All items", "de": "Alle Elemente", "fr": "Tous les éléments"}),
+ description="Alle Elemente"),
PortField(name="count", type="int",
- description={"en": "Total count", "de": "Gesamtanzahl", "fr": "Nombre total"}),
+ description="Gesamtanzahl"),
]),
"AggregateResult": PortSchema(name="AggregateResult", fields=[
PortField(name="items", type="List[Any]",
- description={"en": "Collected items", "de": "Gesammelte Elemente", "fr": "Éléments collectés"}),
+ description="Gesammelte Elemente"),
PortField(name="count", type="int",
- description={"en": "Count", "de": "Anzahl", "fr": "Nombre"}),
+ description="Anzahl"),
]),
"MergeResult": PortSchema(name="MergeResult", fields=[
PortField(name="inputs", type="Dict[int,Any]",
- description={"en": "Inputs by port", "de": "Eingaben nach Port", "fr": "Entrées par port"}),
+ description="Eingaben nach Port"),
PortField(name="first", type="Any",
- description={"en": "First available", "de": "Erstes verfügbares", "fr": "Premier disponible"}),
+ description="Erstes verfügbares"),
PortField(name="merged", type="Dict",
- description={"en": "Merged data", "de": "Zusammengeführte Daten", "fr": "Données fusionnées"}),
+ description="Zusammengeführte Daten"),
]),
"ActionResult": PortSchema(name="ActionResult", fields=[
PortField(name="success", type="bool",
- description={"en": "Success", "de": "Erfolg", "fr": "Succès"}),
+ description="Erfolg"),
PortField(name="error", type="str", required=False,
- description={"en": "Error", "de": "Fehler", "fr": "Erreur"}),
+ description="Fehler"),
PortField(name="data", type="Dict", required=False,
- description={"en": "Result data", "de": "Ergebnisdaten", "fr": "Données résultat"}),
+ description="Ergebnisdaten"),
]),
"Transit": PortSchema(name="Transit", fields=[]),
}
@@ -479,10 +479,16 @@ def _deriveFormPayloadSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
portFields = []
for f in fields_param:
if isinstance(f, dict) and f.get("name"):
+ _lab = f.get("label")
+ _desc = (
+ str(_lab.get("de") or _lab.get("en") or f["name"])
+ if isinstance(_lab, dict)
+ else str(_lab if _lab is not None else f["name"])
+ )
portFields.append(PortField(
name=f["name"],
type=f.get("type", "str"),
- description=f.get("label", {}) if isinstance(f.get("label"), dict) else {"en": str(f.get("label", f["name"]))},
+ description=_desc,
required=f.get("required", False),
))
return PortSchema(name="FormPayload_dynamic", fields=portFields) if portFields else None
@@ -499,6 +505,6 @@ def _deriveTransformSchema(node: Dict[str, Any]) -> Optional[PortSchema]:
portFields.append(PortField(
name=m["outputField"],
type=m.get("type", "str"),
- description={"en": m.get("label", m["outputField"])},
+ description=str(m.get("label", m["outputField"])),
))
return PortSchema(name="Transform_dynamic", fields=portFields) if portFields else None
diff --git a/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py b/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py
index 3c1f4649..c347f622 100644
--- a/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py
+++ b/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py
@@ -26,6 +26,8 @@ from modules.workflows.automation2.runEnvelope import (
normalize_run_envelope,
)
from modules.features.graphicalEditor.entryPoints import find_invocation
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureGraphicalEditor")
logger = logging.getLogger(__name__)
@@ -48,13 +50,13 @@ def _build_execute_run_envelope(
if not workflow:
raise HTTPException(
status_code=400,
- detail="entryPointId requires a saved workflow (workflowId must refer to a stored workflow)",
+ detail=routeApiMsg("entryPointId requires a saved workflow (workflowId must refer to a stored workflow)"),
)
inv = find_invocation(workflow, entry_point_id)
if not inv:
- raise HTTPException(status_code=400, detail="entryPointId not found on workflow")
+ raise HTTPException(status_code=400, detail=routeApiMsg("entryPointId not found on workflow"))
if not inv.get("enabled", True):
- raise HTTPException(status_code=400, detail="entry point is disabled")
+ raise HTTPException(status_code=400, detail=routeApiMsg("entry point is disabled"))
kind = inv.get("kind", "manual")
trig_map = {
"manual": "manual",
@@ -107,7 +109,7 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
if not featureAccess or not featureAccess.enabled:
- raise HTTPException(status_code=403, detail="Access denied to this feature instance")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied to this feature instance"))
return str(instance.mandateId) if instance.mandateId else ""
@@ -327,7 +329,7 @@ def create_draft_version(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
version = iface.createDraftVersion(workflowId)
if not version:
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
return version
@@ -345,7 +347,7 @@ def publish_version(
userId = str(context.user.id) if context.user else None
version = iface.publishVersion(versionId, userId=userId)
if not version:
- raise HTTPException(status_code=400, detail="Version not found or not in draft status")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Version not found or not in draft status"))
return version
@@ -362,7 +364,7 @@ def unpublish_version(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
version = iface.unpublishVersion(versionId)
if not version:
- raise HTTPException(status_code=400, detail="Version not found or not published")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Version not found or not published"))
return version
@@ -379,7 +381,7 @@ def archive_version(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
version = iface.archiveVersion(versionId)
if not version:
- raise HTTPException(status_code=404, detail="Version not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Version not found"))
return version
@@ -442,11 +444,11 @@ def create_template_from_workflow(
workflowId = body.get("workflowId")
scope = body.get("scope", "user")
if not workflowId:
- raise HTTPException(status_code=400, detail="workflowId required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("workflowId required"))
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
template = iface.createTemplateFromWorkflow(workflowId, scope=scope)
if not template:
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
return template
@@ -463,7 +465,7 @@ def copy_template(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
workflow = iface.copyTemplateToUser(templateId)
if not workflow:
- raise HTTPException(status_code=404, detail="Template not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Template not found"))
return workflow
@@ -480,11 +482,11 @@ def share_template(
mandateId = _validateInstanceAccess(instanceId, context)
scope = body.get("scope")
if not scope or scope not in ("user", "instance", "mandate", "system"):
- raise HTTPException(status_code=400, detail="scope must be user, instance, mandate, or system")
+ raise HTTPException(status_code=400, detail=routeApiMsg("scope must be user, instance, mandate, or system"))
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
template = iface.shareTemplate(templateId, scope=scope)
if not template:
- raise HTTPException(status_code=404, detail="Template not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Template not found"))
return template
@@ -506,12 +508,12 @@ async def post_editor_chat(
mandateId = _validateInstanceAccess(instanceId, context)
message = body.get("message", "")
if not message:
- raise HTTPException(status_code=400, detail="message required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("message required"))
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
wf = iface.getWorkflow(workflowId)
if not wf:
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
userLanguage = body.get("userLanguage", "de")
conversationHistory = body.get("conversationHistory") or []
@@ -946,7 +948,7 @@ def get_workflow(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
wf = iface.getWorkflow(workflowId)
if not wf:
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
return wf
@@ -979,7 +981,7 @@ def update_workflow(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
updated = iface.updateWorkflow(workflowId, body)
if not updated:
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
return updated
@@ -995,7 +997,7 @@ def delete_workflow(
mandateId = _validateInstanceAccess(instanceId, context)
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
if not iface.deleteWorkflow(workflowId):
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
return {"success": True}
@@ -1015,20 +1017,20 @@ async def post_workflow_webhook(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
wf = iface.getWorkflow(workflowId)
if not wf or not wf.get("graph"):
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
inv = find_invocation(wf, entryPointId)
if not inv:
- raise HTTPException(status_code=404, detail="Entry point not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Entry point not found"))
if inv.get("kind") != "webhook":
- raise HTTPException(status_code=400, detail="Entry point is not a webhook")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is not a webhook"))
if not inv.get("enabled", True):
- raise HTTPException(status_code=400, detail="Entry point is disabled")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is disabled"))
cfg = inv.get("config") or {}
secret = cfg.get("webhookSecret")
if secret:
hdr = request.headers.get("X-Webhook-Secret")
if hdr != str(secret):
- raise HTTPException(status_code=403, detail="Invalid webhook secret")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Invalid webhook secret"))
services = getGraphicalEditorServices(
context.user,
@@ -1083,14 +1085,14 @@ async def post_workflow_form_submit(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
wf = iface.getWorkflow(workflowId)
if not wf or not wf.get("graph"):
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
inv = find_invocation(wf, entryPointId)
if not inv:
- raise HTTPException(status_code=404, detail="Entry point not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Entry point not found"))
if inv.get("kind") != "form":
- raise HTTPException(status_code=400, detail="Entry point is not a form")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is not a form"))
if not inv.get("enabled", True):
- raise HTTPException(status_code=400, detail="Entry point is disabled")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Entry point is disabled"))
services = getGraphicalEditorServices(
context.user,
@@ -1161,7 +1163,7 @@ def get_workflow_runs(
mandateId = _validateInstanceAccess(instanceId, context)
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
if not iface.getWorkflow(workflowId):
- raise HTTPException(status_code=404, detail="Workflow not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Workflow not found"))
runs = iface.getRunsByWorkflow(workflowId)
return {"runs": runs}
@@ -1200,16 +1202,16 @@ async def resume_run(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
run = iface.getRun(runId)
if not run:
- raise HTTPException(status_code=404, detail="Run not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
taskId = body.get("taskId")
result = body.get("result")
if not taskId or result is None:
- raise HTTPException(status_code=400, detail="taskId and result required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("taskId and result required"))
task = iface.getTask(taskId)
if not task or task.get("runId") != runId:
- raise HTTPException(status_code=404, detail="Task not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
if task.get("status") != "pending":
- raise HTTPException(status_code=400, detail="Task already completed")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Task already completed"))
iface.updateTask(taskId, status="completed", result=result)
nodeId = task.get("nodeId")
nodeOutputs = dict(run.get("nodeOutputs") or {})
@@ -1217,7 +1219,7 @@ async def resume_run(
workflowId = run.get("workflowId")
wf = iface.getWorkflow(workflowId) if workflowId else None
if not wf or not wf.get("graph"):
- raise HTTPException(status_code=400, detail="Workflow graph not found")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Workflow graph not found"))
graph = wf["graph"]
services = getGraphicalEditorServices(context.user, mandateId=mandateId, featureInstanceId=instanceId)
resume_result = await executeGraph(
@@ -1280,16 +1282,16 @@ async def complete_task(
iface = getGraphicalEditorInterface(context.user, mandateId, instanceId)
task = iface.getTask(taskId)
if not task:
- raise HTTPException(status_code=404, detail="Task not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Task not found"))
runId = task.get("runId")
result = body.get("result")
if result is None:
- raise HTTPException(status_code=400, detail="result required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("result required"))
run = iface.getRun(runId)
if not run:
- raise HTTPException(status_code=404, detail="Run not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
if task.get("status") != "pending":
- raise HTTPException(status_code=400, detail="Task already completed")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Task already completed"))
iface.updateTask(taskId, status="completed", result=result)
nodeId = task.get("nodeId")
nodeOutputs = dict(run.get("nodeOutputs") or {})
@@ -1297,7 +1299,7 @@ async def complete_task(
workflowId = run.get("workflowId")
wf = iface.getWorkflow(workflowId) if workflowId else None
if not wf or not wf.get("graph"):
- raise HTTPException(status_code=400, detail="Workflow graph not found")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Workflow graph not found"))
graph = wf["graph"]
services = getGraphicalEditorServices(context.user, mandateId=mandateId, featureInstanceId=instanceId)
return await executeGraph(
diff --git a/modules/features/neutralization/datamodelFeatureNeutralizer.py b/modules/features/neutralization/datamodelFeatureNeutralizer.py
index cc111950..0c353072 100644
--- a/modules/features/neutralization/datamodelFeatureNeutralizer.py
+++ b/modules/features/neutralization/datamodelFeatureNeutralizer.py
@@ -7,7 +7,7 @@ from enum import Enum
from typing import Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
class DataScope(str, Enum):
@@ -17,83 +17,128 @@ class DataScope(str, Enum):
GLOBAL = "global"
+@i18nModel("Daten-Neutralisierung Konfiguration")
class DataNeutraliserConfig(PowerOnModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the configuration", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- mandateId: str = Field(description="ID of the mandate this configuration belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- featureInstanceId: str = Field(description="ID of the feature instance this configuration belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- userId: str = Field(description="ID of the user who created this configuration", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- enabled: bool = Field(default=True, description="Whether data neutralization is enabled", json_schema_extra={"frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False})
- scope: str = Field(default="personal", description="Data visibility scope: personal, featureInstance, mandate, global", json_schema_extra={"frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
- {"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
- {"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
- {"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
- {"value": "global", "label": {"en": "Global", "de": "Global"}},
- ]})
- neutralizationStatus: str = Field(default="not_required", description="Status of neutralization: pending, completed, failed, not_required", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- namesToParse: str = Field(default="", description="Multiline list of names to parse for neutralization", json_schema_extra={"frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False})
- sharepointSourcePath: str = Field(default="", description="SharePoint path to read files for neutralization", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
- sharepointTargetPath: str = Field(default="", description="SharePoint path to store neutralized files", json_schema_extra={"frontend_type": "text", "frontend_readonly": False, "frontend_required": False})
-registerModelLabels(
- "DataNeutraliserConfig",
- {"en": "Data Neutralization Config", "fr": "Configuration de neutralisation des données"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "enabled": {"en": "Enabled", "fr": "Activé"},
- "scope": {"en": "Scope", "fr": "Portée"},
- "neutralizationStatus": {"en": "Neutralization Status", "fr": "Statut de neutralisation"},
- "namesToParse": {"en": "Names to Parse", "fr": "Noms à analyser"},
- "sharepointSourcePath": {"en": "Source Path", "fr": "Chemin source"},
- "sharepointTargetPath": {"en": "Target Path", "fr": "Chemin cible"},
- },
-)
+ """Konfiguration fuer die Daten-Neutralisierung."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Unique ID of the configuration",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ mandateId: str = Field(
+ description="ID of the mandate this configuration belongs to",
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ featureInstanceId: str = Field(
+ description="ID of the feature instance this configuration belongs to",
+ json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ userId: str = Field(
+ description="ID of the user who created this configuration",
+ json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ enabled: bool = Field(
+ default=True,
+ description="Whether data neutralization is enabled",
+ json_schema_extra={"label": "Aktiviert", "frontend_type": "checkbox", "frontend_readonly": False, "frontend_required": False},
+ )
+ scope: str = Field(
+ default="personal",
+ description="Data visibility scope: personal, featureInstance, mandate, global",
+ json_schema_extra={"label": "Sichtbarkeit", "frontend_type": "select", "frontend_readonly": False, "frontend_required": False, "frontend_options": [
+ {"value": "personal", "label": {"en": "Personal", "de": "Persönlich"}},
+ {"value": "featureInstance", "label": {"en": "Feature Instance", "de": "Feature-Instanz"}},
+ {"value": "mandate", "label": {"en": "Mandate", "de": "Mandant"}},
+ {"value": "global", "label": {"en": "Global", "de": "Global"}},
+ ]},
+ )
+ neutralizationStatus: str = Field(
+ default="not_required",
+ description="Status of neutralization: pending, completed, failed, not_required",
+ json_schema_extra={"label": "Neutralisierungsstatus", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ namesToParse: str = Field(
+ default="",
+ description="Multiline list of names to parse for neutralization",
+ json_schema_extra={"label": "Zu parsende Namen", "frontend_type": "textarea", "frontend_readonly": False, "frontend_required": False},
+ )
+ sharepointSourcePath: str = Field(
+ default="",
+ description="SharePoint path to read files for neutralization",
+ json_schema_extra={"label": "SharePoint Quellpfad", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
+ )
+ sharepointTargetPath: str = Field(
+ default="",
+ description="SharePoint path to store neutralized files",
+ json_schema_extra={"label": "SharePoint Zielpfad", "frontend_type": "text", "frontend_readonly": False, "frontend_required": False},
+ )
+
+@i18nModel("Neutralisiertes Datenattribut")
class DataNeutralizerAttributes(BaseModel):
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Unique ID of the attribute mapping (used as UID in neutralized files)", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- mandateId: str = Field(description="ID of the mandate this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- featureInstanceId: str = Field(description="ID of the feature instance this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- userId: str = Field(description="ID of the user who created this attribute", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- originalText: str = Field(description="Original text that was neutralized", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- fileId: Optional[str] = Field(default=None, description="ID of the file this attribute belongs to", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- patternType: str = Field(description="Type of pattern that matched (email, phone, name, etc.)", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
+ """Zuordnung Originaltext zu Platzhalter fuer neutralisierte Daten."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Unique ID of the attribute mapping (used as UID in neutralized files)",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ mandateId: str = Field(
+ description="ID of the mandate this attribute belongs to",
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ featureInstanceId: str = Field(
+ description="ID of the feature instance this attribute belongs to",
+ json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ userId: str = Field(
+ description="ID of the user who created this attribute",
+ json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ originalText: str = Field(
+ description="Original text that was neutralized",
+ json_schema_extra={"label": "Originaltext", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ fileId: Optional[str] = Field(
+ default=None,
+ description="ID of the file this attribute belongs to",
+ json_schema_extra={"label": "Datei-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ patternType: str = Field(
+ description="Type of pattern that matched (email, phone, name, etc.)",
+ json_schema_extra={"label": "Mustertyp", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+@i18nModel("Neutralisierungs-Snapshot")
class DataNeutralizationSnapshot(BaseModel):
- """Stores the full neutralized text (with embedded placeholders) per source."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- mandateId: str = Field(description="Mandate scope")
- featureInstanceId: str = Field(default="", description="Feature instance scope")
- userId: str = Field(description="User who triggered neutralization")
- sourceLabel: str = Field(description="Human label, e.g. 'Prompt', 'Kontext', 'Nachricht 3'")
- neutralizedText: str = Field(description="Full text with [type.uuid] placeholders embedded")
- placeholderCount: int = Field(default=0, description="Number of placeholders in the text")
-registerModelLabels(
- "DataNeutralizerAttributes",
- {"en": "Neutralized Data Attribute", "fr": "Attribut de données neutralisées"},
- {
- "id": {"en": "ID", "fr": "ID"},
- "mandateId": {"en": "Mandate ID", "fr": "ID de mandat"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance de fonctionnalité"},
- "userId": {"en": "User ID", "fr": "ID utilisateur"},
- "originalText": {"en": "Original Text", "fr": "Texte original"},
- "fileId": {"en": "File ID", "fr": "ID de fichier"},
- "patternType": {"en": "Pattern Type", "fr": "Type de modèle"},
- },
-)
-registerModelLabels(
- "DataNeutralizationSnapshot",
- {"en": "Neutralization Snapshot", "de": "Neutralisierungs-Snapshot"},
- {
- "id": {"en": "ID"},
- "mandateId": {"en": "Mandate ID"},
- "featureInstanceId": {"en": "Feature Instance ID"},
- "userId": {"en": "User ID"},
- "sourceLabel": {"en": "Source", "de": "Quelle"},
- "neutralizedText": {"en": "Neutralized Text", "de": "Neutralisierter Text"},
- "placeholderCount": {"en": "Placeholders", "de": "Platzhalter"},
- },
-)
-
-
+ """Speichert den vollstaendigen neutralisierten Text (mit Platzhaltern) pro Quelle."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ json_schema_extra={"label": "ID"},
+ )
+ mandateId: str = Field(
+ description="Mandate scope",
+ json_schema_extra={"label": "Mandanten-ID"},
+ )
+ featureInstanceId: str = Field(
+ default="",
+ description="Feature instance scope",
+ json_schema_extra={"label": "Feature-Instanz-ID"},
+ )
+ userId: str = Field(
+ description="User who triggered neutralization",
+ json_schema_extra={"label": "Benutzer-ID"},
+ )
+ sourceLabel: str = Field(
+ description="Human label, e.g. 'Prompt', 'Kontext', 'Nachricht 3'",
+ json_schema_extra={"label": "Quelle"},
+ )
+ neutralizedText: str = Field(
+ description="Full text with [type.uuid] placeholders embedded",
+ json_schema_extra={"label": "Neutralisierter Text"},
+ )
+ placeholderCount: int = Field(
+ default=0,
+ description="Number of placeholders in the text",
+ json_schema_extra={"label": "Platzhalter"},
+ )
diff --git a/modules/features/neutralization/mainNeutralization.py b/modules/features/neutralization/mainNeutralization.py
index bfe97a13..2c69fe7b 100644
--- a/modules/features/neutralization/mainNeutralization.py
+++ b/modules/features/neutralization/mainNeutralization.py
@@ -12,14 +12,14 @@ logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "neutralization"
-FEATURE_LABEL = {"en": "Neutralization", "de": "Neutralisierung", "fr": "Neutralisation"}
+FEATURE_LABEL = "Neutralisierung"
FEATURE_ICON = "mdi-shield-check"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.neutralization.playground",
- "label": {"en": "Playground", "de": "Spielwiese", "fr": "Bac à sable"},
+ "label": "Spielwiese",
"meta": {"area": "playground"}
}
]
@@ -28,17 +28,17 @@ UI_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.neutralization.process.text",
- "label": {"en": "Process Text", "de": "Text verarbeiten", "fr": "Traiter texte"},
+ "label": "Text verarbeiten",
"meta": {"endpoint": "/api/neutralization/process/text", "method": "POST"}
},
{
"objectKey": "resource.feature.neutralization.process.files",
- "label": {"en": "Process Files", "de": "Dateien verarbeiten", "fr": "Traiter fichiers"},
+ "label": "Dateien verarbeiten",
"meta": {"endpoint": "/api/neutralization/process/files", "method": "POST"}
},
{
"objectKey": "resource.feature.neutralization.config.update",
- "label": {"en": "Update Config", "de": "Konfiguration aktualisieren", "fr": "Mettre à jour config"},
+ "label": "Konfiguration aktualisieren",
"meta": {"endpoint": "/api/neutralization/config", "method": "PUT"}
},
]
@@ -47,11 +47,7 @@ RESOURCE_OBJECTS = [
TEMPLATE_ROLES = [
{
"roleLabel": "neutralization-viewer",
- "description": {
- "en": "Neutralization Viewer - View neutralization data (read-only)",
- "de": "Neutralisierungs-Betrachter - Neutralisierungsdaten einsehen (nur lesen)",
- "fr": "Visualiseur neutralisation - Consulter les données de neutralisation (lecture seule)",
- },
+ "description": "Neutralisierungs-Betrachter - Neutralisierungsdaten einsehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
@@ -59,11 +55,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "neutralization-user",
- "description": {
- "en": "Neutralization User - Use neutralization tools and manage own data",
- "de": "Neutralisierungs-Benutzer - Neutralisierungstools nutzen und eigene Daten verwalten",
- "fr": "Utilisateur neutralisation - Utiliser les outils et gérer ses propres données",
- },
+ "description": "Neutralisierungs-Benutzer - Neutralisierungstools nutzen und eigene Daten verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
{"context": "UI", "item": "ui.feature.neutralization.attributes", "view": True},
@@ -72,11 +64,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "neutralization-admin",
- "description": {
- "en": "Neutralization Administrator - Full access to neutralization settings and data",
- "de": "Neutralisierungs-Administrator - Vollzugriff auf Neutralisierungs-Einstellungen und Daten",
- "fr": "Administrateur neutralisation - Accès complet aux paramètres et données",
- },
+ "description": "Neutralisierungs-Administrator - Vollzugriff auf Neutralisierungs-Einstellungen und Daten",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
@@ -84,11 +72,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "neutralization-analyst",
- "description": {
- "en": "Neutralization Analyst - Analyze and process neutralization data",
- "de": "Neutralisierungs-Analyst - Neutralisierungsdaten analysieren und verarbeiten",
- "fr": "Analyste neutralisation - Analyser et traiter les données de neutralisation",
- },
+ "description": "Neutralisierungs-Analyst - Neutralisierungsdaten analysieren und verarbeiten",
"accessRules": [
{"context": "UI", "item": "ui.feature.neutralization.playground", "view": True},
{"context": "UI", "item": "ui.feature.neutralization.attributes", "view": True},
@@ -163,7 +147,8 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
-
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
+
rootInterface = getRootInterface()
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
@@ -180,7 +165,7 @@ def _syncTemplateRolesToDb() -> int:
else:
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
diff --git a/modules/features/neutralization/routeFeatureNeutralizer.py b/modules/features/neutralization/routeFeatureNeutralizer.py
index 2f36efef..bf396e3b 100644
--- a/modules/features/neutralization/routeFeatureNeutralizer.py
+++ b/modules/features/neutralization/routeFeatureNeutralizer.py
@@ -10,6 +10,8 @@ from modules.auth import limiter, getRequestContext, RequestContext
# Import interfaces
from .datamodelFeatureNeutralizer import DataNeutraliserConfig, DataNeutralizerAttributes, DataNeutralizationSnapshot
from .neutralizePlayground import NeutralizationPlayground
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureNeutralizer")
# Configure logger
logger = logging.getLogger(__name__)
@@ -22,7 +24,7 @@ def _assertFeatureInstancePathMatchesContext(featureInstanceIdFromPath: str, con
if ctxId and pathId and pathId != ctxId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Feature instance id in URL does not match request context (X-Instance-Id)",
+ detail=routeApiMsg("Feature instance id in URL does not match request context (X-Instance-Id)"),
)
@@ -123,13 +125,13 @@ async def neutralize_file(
if not file.filename or not file.filename.strip():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="File name is required"
+ detail=routeApiMsg("File name is required")
)
content = await file.read()
if not content:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="File is empty"
+ detail=routeApiMsg("File is empty")
)
service = NeutralizationPlayground(
context.user,
@@ -164,7 +166,7 @@ def neutralize_text(
if not text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Text content is required"
+ detail=routeApiMsg("Text content is required")
)
service = NeutralizationPlayground(
@@ -199,7 +201,7 @@ def resolve_text(
if not text:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Text content is required"
+ detail=routeApiMsg("Text content is required")
)
service = NeutralizationPlayground(
@@ -320,7 +322,7 @@ async def process_sharepoint_files(
if not source_path or not target_path:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Both source and target paths are required"
+ detail=routeApiMsg("Both source and target paths are required")
)
service = NeutralizationPlayground(
@@ -353,7 +355,7 @@ def batch_process_files(
if not files_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Files data is required"
+ detail=routeApiMsg("Files data is required")
)
service = NeutralizationPlayground(
@@ -453,7 +455,7 @@ def _retriggerNeutralizationBody(context: RequestContext, fileId: str) -> Dict[s
if not fileId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="fileId is required",
+ detail=routeApiMsg("fileId is required"),
)
service = NeutralizationPlayground(
context.user,
@@ -521,7 +523,7 @@ def cleanup_file_attributes(
else:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to cleanup file attributes"
+ detail=routeApiMsg("Failed to cleanup file attributes")
)
except HTTPException:
diff --git a/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py b/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py
index 4c0842d4..0911d0b7 100644
--- a/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py
+++ b/modules/features/neutralization/serviceNeutralization/mainServiceNeutralization.py
@@ -20,7 +20,7 @@ from modules.features.neutralization.interfaceFeatureNeutralizer import Interfac
# Import all necessary classes and functions for neutralization
from .subProcessCommon import CommonUtils, NeutralizationResult, NeutralizationAttribute
from .subProcessText import TextProcessor, PlainText
-from .subProcessList import ListProcessor, TableData
+from .subProcessList import ListProcessor, NeutralizationTableData
from .subProcessBinary import BinaryProcessor
from .subProcessPdfInPlace import neutralize_pdf_in_place
from .subPatterns import HeaderPatterns, DataPatterns, TextTablePatterns
diff --git a/modules/features/neutralization/serviceNeutralization/subProcessList.py b/modules/features/neutralization/serviceNeutralization/subProcessList.py
index 97721535..8f815e1e 100644
--- a/modules/features/neutralization/serviceNeutralization/subProcessList.py
+++ b/modules/features/neutralization/serviceNeutralization/subProcessList.py
@@ -15,7 +15,7 @@ from .subParseString import StringParser
from .subPatterns import getPatternForHeader, HeaderPatterns
@dataclass
-class TableData:
+class NeutralizationTableData:
"""Repräsentiert Tabellendaten"""
headers: List[str]
rows: List[List[str]]
@@ -34,17 +34,17 @@ class ListProcessor:
self.string_parser = StringParser(NamesToParse)
self.header_patterns = HeaderPatterns.patterns
- def _anonymizeTable(self, table: TableData) -> TableData:
+ def _anonymizeTable(self, table: NeutralizationTableData) -> NeutralizationTableData:
"""
Anonymize table data based on headers
Args:
- table: TableData object to anonymize
+ table: NeutralizationTableData object to anonymize
Returns:
- TableData: Anonymized table
+ NeutralizationTableData: Anonymized table
"""
- anonymizedTable = TableData(
+ anonymizedTable = NeutralizationTableData(
headers=table.headers.copy(),
rows=[row.copy() for row in table.rows],
source_type=table.source_type
@@ -76,7 +76,7 @@ class ListProcessor:
Tuple of (processed_data, mapping, replaced_fields, processed_info)
"""
df = pd.read_csv(StringIO(content), encoding='utf-8')
- table = TableData(
+ table = NeutralizationTableData(
headers=df.columns.tolist(),
rows=df.values.tolist(),
source_type='csv'
diff --git a/modules/features/realEstate/datamodelFeatureRealEstate.py b/modules/features/realEstate/datamodelFeatureRealEstate.py
index 8f136056..c12090d1 100644
--- a/modules/features/realEstate/datamodelFeatureRealEstate.py
+++ b/modules/features/realEstate/datamodelFeatureRealEstate.py
@@ -8,7 +8,7 @@ from typing import List, Dict, Any, Optional, ForwardRef
from enum import Enum
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
from modules.shared.timeUtils import getUtcTimestamp
import uuid
@@ -109,6 +109,7 @@ class GeoPolylinie(BaseModel):
)
+@i18nModel("Dokument")
class Dokument(BaseModel):
"""Supporting data object for file and URL management with versioning."""
id: str = Field(
@@ -117,24 +118,28 @@ class Dokument(BaseModel):
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="ID",
)
mandateId: str = Field(
description="ID of the mandate this document belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="Mandats-ID",
)
featureInstanceId: str = Field(
description="ID of the feature instance this document belongs to",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="Feature-Instanz-ID",
)
label: str = Field(
description="Document label",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
+ label="Bezeichnung",
)
versionsbezeichnung: Optional[str] = Field(
None,
@@ -369,6 +374,7 @@ class Gemeinde(BaseModel):
ParzelleRef = ForwardRef('Parzelle')
+@i18nModel("Parzelle")
class Parzelle(PowerOnModel):
"""Represents a plot with all building law properties."""
id: str = Field(
@@ -377,18 +383,21 @@ class Parzelle(PowerOnModel):
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="ID",
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="Mandats-ID",
)
featureInstanceId: str = Field(
description="ID of the feature instance",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="Feature-Instanz-ID",
)
# Grunddaten
@@ -397,6 +406,7 @@ class Parzelle(PowerOnModel):
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
+ label="Bezeichnung",
)
parzellenAliasTags: List[str] = Field(
default_factory=list,
@@ -595,6 +605,7 @@ class Parzelle(PowerOnModel):
)
+@i18nModel("Projekt")
class Projekt(PowerOnModel):
"""Core object representing a construction project."""
id: str = Field(
@@ -603,24 +614,28 @@ class Projekt(PowerOnModel):
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="ID",
)
mandateId: str = Field(
description="ID of the mandate",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="Mandats-ID",
)
featureInstanceId: str = Field(
description="ID of the feature instance",
frontend_type="text",
frontend_readonly=True,
frontend_required=False,
+ label="Feature-Instanz-ID",
)
label: str = Field(
description="Project designation",
frontend_type="text",
frontend_readonly=False,
frontend_required=True,
+ label="Bezeichnung",
)
statusProzess: Optional[StatusProzess] = Field(
None,
@@ -628,6 +643,7 @@ class Projekt(PowerOnModel):
frontend_type="select",
frontend_readonly=False,
frontend_required=False,
+ label="Prozessstatus",
)
perimeter: Optional[GeoPolylinie] = Field(
None,
@@ -670,39 +686,3 @@ class Projekt(PowerOnModel):
Parzelle.model_rebuild()
Projekt.model_rebuild()
-
-# Register labels for frontend
-registerModelLabels(
- "Projekt",
- {"en": "Project", "fr": "Projet", "de": "Projekt"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
- "statusProzess": {"en": "Process Status", "fr": "Statut du processus", "de": "Prozessstatus"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
- },
-)
-
-registerModelLabels(
- "Parzelle",
- {"en": "Plot", "fr": "Parcelle", "de": "Parzelle"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
- },
-)
-
-registerModelLabels(
- "Dokument",
- {"en": "Document", "fr": "Document", "de": "Dokument"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
- "mandateId": {"en": "Mandate ID", "fr": "ID du mandat", "de": "Mandats-ID"},
- "featureInstanceId": {"en": "Feature Instance ID", "fr": "ID de l'instance", "de": "Feature-Instanz-ID"},
- },
-)
-
diff --git a/modules/features/realEstate/mainRealEstate.py b/modules/features/realEstate/mainRealEstate.py
index dfe310d5..0ae29159 100644
--- a/modules/features/realEstate/mainRealEstate.py
+++ b/modules/features/realEstate/mainRealEstate.py
@@ -10,14 +10,14 @@ import logging
# Feature metadata for RBAC catalog
FEATURE_CODE = "realestate"
-FEATURE_LABEL = {"en": "Real Estate", "de": "Immobilien", "fr": "Immobilier"}
+FEATURE_LABEL = "Immobilien"
FEATURE_ICON = "mdi-home-city"
# UI Objects for RBAC catalog (only map view)
UI_OBJECTS = [
{
"objectKey": "ui.feature.realestate.dashboard",
- "label": {"en": "Map", "de": "Karte", "fr": "Carte"},
+ "label": "Karte",
"meta": {"area": "dashboard"}
},
]
@@ -26,12 +26,12 @@ UI_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.realestate.project.create",
- "label": {"en": "Create Project", "de": "Projekt erstellen", "fr": "Créer projet"},
+ "label": "Projekt erstellen",
"meta": {"endpoint": "/api/realestate/project", "method": "POST"}
},
{
"objectKey": "resource.feature.realestate.project.delete",
- "label": {"en": "Delete Project", "de": "Projekt löschen", "fr": "Supprimer projet"},
+ "label": "Projekt löschen",
"meta": {"endpoint": "/api/realestate/project/{projectId}", "method": "DELETE"}
},
]
@@ -41,11 +41,7 @@ RESOURCE_OBJECTS = [
TEMPLATE_ROLES = [
{
"roleLabel": "realestate-viewer",
- "description": {
- "en": "Real Estate Viewer - View property information (read-only)",
- "de": "Immobilien-Betrachter - Immobilien-Informationen einsehen (nur lesen)",
- "fr": "Visualiseur immobilier - Consulter les informations immobilières (lecture seule)",
- },
+ "description": "Immobilien-Betrachter - Immobilien-Informationen einsehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
@@ -53,11 +49,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "realestate-user",
- "description": {
- "en": "Real Estate User - Create and manage own property records",
- "de": "Immobilien-Benutzer - Eigene Immobilien-Daten erstellen und verwalten",
- "fr": "Utilisateur immobilier - Créer et gérer ses propres données immobilières",
- },
+ "description": "Immobilien-Benutzer - Eigene Immobilien-Daten erstellen und verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
@@ -66,11 +58,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "realestate-admin",
- "description": {
- "en": "Real Estate Administrator - Full access to all property data and settings",
- "de": "Immobilien-Administrator - Vollzugriff auf alle Immobiliendaten und Einstellungen",
- "fr": "Administrateur immobilier - Accès complet aux données et paramètres",
- },
+ "description": "Immobilien-Administrator - Vollzugriff auf alle Immobiliendaten und Einstellungen",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
@@ -80,11 +68,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "realestate-manager",
- "description": {
- "en": "Real Estate Manager - Manage properties and tenants",
- "de": "Immobilien-Verwalter - Immobilien und Mieter verwalten",
- "fr": "Gestionnaire immobilier - Gérer les propriétés et locataires",
- },
+ "description": "Immobilien-Verwalter - Immobilien und Mieter verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.realestate.dashboard", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
@@ -154,6 +138,7 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
rootInterface = getRootInterface()
db = rootInterface.db
@@ -174,7 +159,7 @@ def _syncTemplateRolesToDb() -> int:
else:
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
diff --git a/modules/features/realEstate/routeFeatureRealEstate.py b/modules/features/realEstate/routeFeatureRealEstate.py
index 82fa55ba..58faca8e 100644
--- a/modules/features/realEstate/routeFeatureRealEstate.py
+++ b/modules/features/realEstate/routeFeatureRealEstate.py
@@ -59,6 +59,8 @@ from modules.aicore.aicorePluginTavily import AiTavily
# Import attribute utilities for model schema
from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureRealEstate")
# Configure logger
logger = logging.getLogger(__name__)
@@ -339,7 +341,7 @@ def update_project(
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
updated = interface.updateProjekt(projectId, data)
if not updated:
- raise HTTPException(status_code=500, detail="Update failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
return updated
@@ -360,7 +362,7 @@ def delete_project(
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
if not interface.deleteProjekt(projectId):
- raise HTTPException(status_code=500, detail="Delete failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
# ----- Parcels CRUD -----
@@ -496,7 +498,7 @@ def update_parcel(
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
updated = interface.updateParzelle(parcelId, data)
if not updated:
- raise HTTPException(status_code=500, detail="Update failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
return updated
@@ -517,7 +519,7 @@ def delete_parcel(
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
if not interface.deleteParzelle(parcelId):
- raise HTTPException(status_code=500, detail="Delete failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
# ===== Helpers for Gemeinde/BZO routes =====
@@ -885,7 +887,7 @@ async def process_command(
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -893,7 +895,7 @@ async def process_command(
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -903,7 +905,7 @@ async def process_command(
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Processing command request from user {context.user.id} (mandate: {context.mandateId})")
@@ -957,7 +959,7 @@ def get_available_tables(
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -965,7 +967,7 @@ def get_available_tables(
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -975,7 +977,7 @@ def get_available_tables(
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Getting available tables for user {context.user.id} (mandate: {context.mandateId})")
@@ -1066,7 +1068,7 @@ def get_table_data(
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -1074,7 +1076,7 @@ def get_table_data(
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -1084,7 +1086,7 @@ def get_table_data(
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Getting table data for '{table}' from user {context.user.id} (mandate: {context.mandateId})")
@@ -1235,7 +1237,7 @@ async def create_table_record(
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -1243,7 +1245,7 @@ async def create_table_record(
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -1253,7 +1255,7 @@ async def create_table_record(
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Special handling for Projekt with parcel data
@@ -1265,7 +1267,7 @@ async def create_table_record(
if not label:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="label is required"
+ detail=routeApiMsg("label is required")
)
status_prozess = data.get("statusProzess", "Eingang")
@@ -1278,7 +1280,7 @@ async def create_table_record(
if not isinstance(parzellen_data, list):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="parzellen must be an array"
+ detail=routeApiMsg("parzellen must be an array")
)
elif "parzelle" in data:
# Single parcel
@@ -1289,7 +1291,7 @@ async def create_table_record(
if not parzellen_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="parzelle or parzellen data is required"
+ detail=routeApiMsg("parzelle or parzellen data is required")
)
# Use helper function to create project with parcel data
@@ -1402,7 +1404,7 @@ def get_parcels_wfs(
logger.error(f"Error fetching WFS parcels: {e}", exc_info=True)
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
- detail="Failed to fetch parcel data from WFS"
+ detail=routeApiMsg("Failed to fetch parcel data from WFS")
)
@@ -1441,7 +1443,7 @@ async def search_parcel(
logger.warning(f"CSRF token missing for GET /api/realestate/parcel/search from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
logger.info(f"Searching parcel for user {context.user.id} (mandate: {context.mandateId}) with location: {location}")
@@ -1817,7 +1819,7 @@ async def parcel_selection_summary(
if not csrf_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
parcels = body.get("parcels", [])
if not parcels:
@@ -1868,19 +1870,19 @@ async def add_adjacent_parcel(
if not csrf_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
location = body.get("location")
selected_parcels = body.get("selected_parcels", [])
if not location or "x" not in location or "y" not in location:
- raise HTTPException(status_code=400, detail="location with x,y required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("location with x,y required"))
loc_str = f"{location['x']},{location['y']}"
connector = SwissTopoMapServerConnector()
parcel_data = await connector.search_parcel(loc_str)
if not parcel_data:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No parcel found at this location"
+ detail=routeApiMsg("No parcel found at this location")
)
extracted = connector.extract_parcel_attributes(parcel_data)
attributes = parcel_data.get("attributes", {})
@@ -1932,7 +1934,7 @@ async def add_adjacent_parcel(
if not is_parcel_adjacent_to_selection(new_parcel_response, selected_parcels):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Nur angrenzende Parzellen können hinzugefügt werden"
+ detail=routeApiMsg("Nur angrenzende Parzellen können hinzugefügt werden")
)
bbox = parcel_data.get("bbox", [])
map_view["zoom_bounds"] = {
@@ -2020,21 +2022,21 @@ async def add_parcel_to_project(
logger.warning(f"CSRF token missing for POST /api/realestate/projekt/{projekt_id}/add-parcel from user {context.user.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Validate CSRF token format
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
try:
int(csrf_token, 16)
except ValueError:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Adding parcel to project {projekt_id} for user {context.user.id} (mandate: {context.mandateId})")
diff --git a/modules/features/teamsbot/mainTeamsbot.py b/modules/features/teamsbot/mainTeamsbot.py
index ea6d3b01..02d7c333 100644
--- a/modules/features/teamsbot/mainTeamsbot.py
+++ b/modules/features/teamsbot/mainTeamsbot.py
@@ -12,24 +12,24 @@ logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "teamsbot"
-FEATURE_LABEL = {"en": "Teams Bot", "de": "Teams Bot", "fr": "Teams Bot"}
+FEATURE_LABEL = "Teams Bot"
FEATURE_ICON = "mdi-headset"
# UI Objects for RBAC catalog
UI_OBJECTS = [
{
"objectKey": "ui.feature.teamsbot.dashboard",
- "label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
+ "label": "Dashboard",
"meta": {"area": "dashboard"}
},
{
"objectKey": "ui.feature.teamsbot.sessions",
- "label": {"en": "Sessions", "de": "Sitzungen", "fr": "Sessions"},
+ "label": "Sitzungen",
"meta": {"area": "sessions"}
},
{
"objectKey": "ui.feature.teamsbot.settings",
- "label": {"en": "Settings", "de": "Einstellungen", "fr": "Paramètres"},
+ "label": "Einstellungen",
"meta": {"area": "settings", "admin_only": True}
},
]
@@ -38,7 +38,7 @@ UI_OBJECTS = [
DATA_OBJECTS = [
{
"objectKey": "data.feature.teamsbot.TeamsbotSession",
- "label": {"en": "Session", "de": "Sitzung", "fr": "Session"},
+ "label": "Sitzung",
"meta": {
"table": "TeamsbotSession",
"fields": ["id", "meetingLink", "botName", "status", "startedAt", "endedAt"],
@@ -48,7 +48,7 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.teamsbot.TeamsbotTranscript",
- "label": {"en": "Transcript", "de": "Transkript", "fr": "Transcription"},
+ "label": "Transkript",
"meta": {
"table": "TeamsbotTranscript",
"fields": ["id", "sessionId", "speaker", "text", "timestamp"],
@@ -58,7 +58,7 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.teamsbot.TeamsbotBotResponse",
- "label": {"en": "Bot Response", "de": "Bot-Antwort", "fr": "Réponse du bot"},
+ "label": "Bot-Antwort",
"meta": {
"table": "TeamsbotBotResponse",
"fields": ["id", "sessionId", "responseText", "detectedIntent"],
@@ -68,7 +68,7 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.teamsbot.*",
- "label": {"en": "All Teams Bot Data", "de": "Alle Teams Bot Daten", "fr": "Toutes les données Teams Bot"},
+ "label": "Alle Teams Bot Daten",
"meta": {"wildcard": True, "description": "Wildcard for all teamsbot data tables"}
},
]
@@ -77,22 +77,22 @@ DATA_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.teamsbot.session.start",
- "label": {"en": "Start Session", "de": "Sitzung starten", "fr": "Démarrer session"},
+ "label": "Sitzung starten",
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions", "method": "POST"}
},
{
"objectKey": "resource.feature.teamsbot.session.stop",
- "label": {"en": "Stop Session", "de": "Sitzung beenden", "fr": "Arrêter session"},
+ "label": "Sitzung beenden",
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions/{sessionId}/stop", "method": "POST"}
},
{
"objectKey": "resource.feature.teamsbot.session.delete",
- "label": {"en": "Delete Session", "de": "Sitzung löschen", "fr": "Supprimer session"},
+ "label": "Sitzung löschen",
"meta": {"endpoint": "/api/teamsbot/{instanceId}/sessions/{sessionId}", "method": "DELETE"}
},
{
"objectKey": "resource.feature.teamsbot.config.edit",
- "label": {"en": "Edit Configuration", "de": "Konfiguration bearbeiten", "fr": "Modifier configuration"},
+ "label": "Konfiguration bearbeiten",
"meta": {"endpoint": "/api/teamsbot/{instanceId}/config", "method": "PUT", "admin_only": True}
},
]
@@ -101,11 +101,7 @@ RESOURCE_OBJECTS = [
TEMPLATE_ROLES = [
{
"roleLabel": "teamsbot-admin",
- "description": {
- "en": "Teams Bot Administrator - Full access to all sessions and settings",
- "de": "Teams Bot Administrator - Vollzugriff auf alle Sitzungen und Einstellungen",
- "fr": "Administrateur Teams Bot - Accès complet aux sessions et paramètres"
- },
+ "description": "Teams Bot Administrator - Vollzugriff auf alle Sitzungen und Einstellungen",
"accessRules": [
# Full UI access (all views including settings)
{"context": "UI", "item": None, "view": True},
@@ -120,11 +116,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "teamsbot-viewer",
- "description": {
- "en": "Teams Bot Viewer - View sessions and transcripts (read-only)",
- "de": "Teams Bot Betrachter - Sitzungen und Transkripte ansehen (nur lesen)",
- "fr": "Visualiseur Teams Bot - Consulter les sessions et transcriptions (lecture seule)",
- },
+ "description": "Teams Bot Betrachter - Sitzungen und Transkripte ansehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.teamsbot.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.teamsbot.sessions", "view": True},
@@ -133,11 +125,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "teamsbot-user",
- "description": {
- "en": "Teams Bot User - Can start/stop sessions and view transcripts",
- "de": "Teams Bot Benutzer - Kann Sitzungen starten/stoppen und Transkripte einsehen",
- "fr": "Utilisateur Teams Bot - Peut démarrer/arrêter des sessions et voir les transcriptions",
- },
+ "description": "Teams Bot Benutzer - Kann Sitzungen starten/stoppen und Transkripte einsehen",
"accessRules": [
{"context": "UI", "item": "ui.feature.teamsbot.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.teamsbot.sessions", "view": True},
@@ -223,7 +211,8 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
-
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
+
rootInterface = getRootInterface()
existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE)
templateRoles = [r for r in existingRoles if r.mandateId is None]
@@ -239,7 +228,7 @@ def _syncTemplateRolesToDb() -> int:
else:
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
diff --git a/modules/features/teamsbot/routeFeatureTeamsbot.py b/modules/features/teamsbot/routeFeatureTeamsbot.py
index c498a790..c2823a85 100644
--- a/modules/features/teamsbot/routeFeatureTeamsbot.py
+++ b/modules/features/teamsbot/routeFeatureTeamsbot.py
@@ -40,6 +40,8 @@ from .datamodelTeamsbot import (
# Import service
from .service import TeamsbotService
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureTeamsbot")
logger = logging.getLogger(__name__)
@@ -71,7 +73,7 @@ def _extractTeamsMeetingUrl(rawInput: str) -> str:
urls = re.findall(urlPattern, rawInput)
if not urls:
- raise HTTPException(status_code=400, detail="Kein gültiger Meeting-Link gefunden. Bitte einen Teams-Link eingeben.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Kein gültiger Meeting-Link gefunden. Bitte einen Teams-Link eingeben."))
# Step 2: Find the Teams URL (prefer direct teams.microsoft.com, then SafeLinks)
teamsUrl = None
@@ -101,7 +103,7 @@ def _extractTeamsMeetingUrl(rawInput: str) -> str:
if not teamsUrl or "teams.microsoft.com" not in teamsUrl:
raise HTTPException(
status_code=400,
- detail="Kein gültiger Teams-Meeting-Link gefunden. Der Link muss 'teams.microsoft.com' enthalten."
+ detail=routeApiMsg("Kein gültiger Teams-Meeting-Link gefunden. Der Link muss 'teams.microsoft.com' enthalten.")
)
logger.info(f"Extracted meeting URL: {teamsUrl[:80]}... (from input length {len(rawInput)})")
@@ -129,7 +131,7 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
mandateId = instance.get("mandateId") if isinstance(instance, dict) else getattr(instance, "mandateId", None)
if not mandateId:
- raise HTTPException(status_code=500, detail="Feature instance has no mandateId")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Feature instance has no mandateId"))
return str(mandateId)
@@ -463,7 +465,7 @@ async def deleteSession(
# Don't delete active sessions
currentStatus = session.get("status")
if currentStatus in [TeamsbotSessionStatus.ACTIVE.value, TeamsbotSessionStatus.JOINING.value]:
- raise HTTPException(status_code=400, detail="Cannot delete an active session. Stop it first.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Cannot delete an active session. Stop it first."))
interface.deleteSession(sessionId)
logger.info(f"Teamsbot session {sessionId} deleted")
@@ -639,7 +641,7 @@ async def listSystemBots(
):
"""List all system bot accounts for this mandate. Passwords are never returned."""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required to manage system bots")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required to manage system bots"))
mandateId = _validateInstanceAccess(instanceId, context)
interface = _getInterface(context, instanceId)
bots = interface.getSystemBots(mandateId)
@@ -655,7 +657,7 @@ async def createSystemBot(
):
"""Create a new system bot account. Password is encrypted before storage."""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required to manage system bots")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required to manage system bots"))
mandateId = _validateInstanceAccess(instanceId, context)
interface = _getInterface(context, instanceId)
@@ -666,7 +668,7 @@ async def createSystemBot(
if not email or not password:
from fastapi import HTTPException
- raise HTTPException(status_code=400, detail="Email and password are required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Email and password are required"))
# Encrypt the password
from modules.shared.configuration import encryptValue
@@ -698,7 +700,7 @@ async def deleteSystemBot(
):
"""Delete a system bot account."""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required to manage system bots")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required to manage system bots"))
_validateInstanceAccess(instanceId, context)
interface = _getInterface(context, instanceId)
@@ -750,7 +752,7 @@ async def saveUserAccount(
displayName = body.get("displayName")
if not email or not password:
- raise HTTPException(status_code=400, detail="Email and password are required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Email and password are required"))
from modules.shared.configuration import encryptValue
encryptedPassword = encryptValue(password, userId=userId, keyName="userAccountPassword")
@@ -827,7 +829,7 @@ async def submitMfaCode(
await queue.put({"action": mfaAction, "code": mfaCode})
return {"submitted": True}
else:
- raise HTTPException(status_code=404, detail="No active MFA challenge for this session")
+ raise HTTPException(status_code=404, detail=routeApiMsg("No active MFA challenge for this session"))
# =========================================================================
@@ -925,7 +927,7 @@ async def testAuth(
Does NOT join the meeting — only checks which page Teams serves.
"""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required for auth testing (uses system bot credentials)")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required for auth testing (uses system bot credentials)"))
import aiohttp
mandateId = _validateInstanceAccess(instanceId, context)
@@ -935,7 +937,7 @@ async def testAuth(
body = await request.json()
meetingUrl = body.get("meetingUrl")
if not meetingUrl:
- raise HTTPException(status_code=400, detail="meetingUrl is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("meetingUrl is required"))
# Load system bot credentials:
# 1. Use email/password from request body (direct override)
@@ -1000,7 +1002,7 @@ async def testAuth(
# Forward to browser bot service (single all-in-one call — may timeout with many variants)
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
if not browserBotUrl:
- raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
browserBotUrl = browserBotUrl.rstrip("/")
payload = {
@@ -1037,14 +1039,14 @@ async def getTestAuthVariants(
Frontend calls this once, then runs each variant individually.
"""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required for auth testing")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required for auth testing"))
import aiohttp
_validateInstanceAccess(instanceId, context)
effectiveConfig = _getInstanceConfig(instanceId)
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
if not browserBotUrl:
- raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
browserBotUrl = browserBotUrl.rstrip("/")
try:
@@ -1073,7 +1075,7 @@ async def testAuthSingleVariant(
Each call stays within Azure's 240s timeout.
"""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required for auth testing (uses system bot credentials)")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required for auth testing (uses system bot credentials)"))
import aiohttp
mandateId = _validateInstanceAccess(instanceId, context)
@@ -1084,7 +1086,7 @@ async def testAuthSingleVariant(
variantId = body.get("variantId")
meetingUrl = body.get("meetingUrl")
if not variantId or not meetingUrl:
- raise HTTPException(status_code=400, detail="variantId and meetingUrl are required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("variantId and meetingUrl are required"))
# Load credentials (same logic as testAuth)
email = body.get("botEmail")
@@ -1116,7 +1118,7 @@ async def testAuthSingleVariant(
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
if not browserBotUrl:
- raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
browserBotUrl = browserBotUrl.rstrip("/")
payload = {
@@ -1157,12 +1159,12 @@ async def listSessionScreenshots(
):
"""List debug screenshots for a session. Proxied from Browser Bot filesystem."""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required"))
_validateInstanceAccess(instanceId, context)
effectiveConfig = _getInstanceConfig(instanceId)
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
if not browserBotUrl:
- raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
import aiohttp
browserBotUrl = browserBotUrl.rstrip("/")
@@ -1194,16 +1196,16 @@ async def getScreenshotFile(
):
"""Serve a single debug screenshot image. Proxied from Browser Bot."""
if not context.isSysAdmin:
- raise HTTPException(status_code=403, detail="SysAdmin privileges required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("SysAdmin privileges required"))
_validateInstanceAccess(instanceId, context)
if not filename.endswith(".png") or ".." in filename or "/" in filename or "\\" in filename:
- raise HTTPException(status_code=400, detail="Invalid filename")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid filename"))
effectiveConfig = _getInstanceConfig(instanceId)
browserBotUrl = effectiveConfig._getEffectiveBrowserBotUrl()
if not browserBotUrl:
- raise HTTPException(status_code=503, detail="Browser Bot URL not configured")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Browser Bot URL not configured"))
import aiohttp
from fastapi.responses import Response as FastAPIResponse
@@ -1216,7 +1218,7 @@ async def getScreenshotFile(
imageBytes = await resp.read()
return FastAPIResponse(content=imageBytes, media_type="image/png")
else:
- raise HTTPException(status_code=resp.status, detail="Screenshot not found")
+ raise HTTPException(status_code=resp.status, detail=routeApiMsg("Screenshot not found"))
except aiohttp.ClientError as e:
logger.error(f"Screenshot file error: {e}")
raise HTTPException(status_code=503, detail=f"Browser Bot connection failed: {str(e)}")
diff --git a/modules/features/trustee/datamodelFeatureTrustee.py b/modules/features/trustee/datamodelFeatureTrustee.py
index 0889e361..8d0ed00c 100644
--- a/modules/features/trustee/datamodelFeatureTrustee.py
+++ b/modules/features/trustee/datamodelFeatureTrustee.py
@@ -7,15 +7,16 @@ from typing import Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
-
+@i18nModel("Organisation")
class TrusteeOrganisation(PowerOnModel):
"""Represents trustee organisations (companies) within the Trustee feature."""
id: str = Field( # Unique string label (PK), not UUID
description="Unique organisation identifier (label)",
json_schema_extra={
+ "label": "ID",
"frontend_type": "text",
"frontend_readonly": False, # Editable at creation, then readonly
"frontend_required": True
@@ -24,6 +25,7 @@ class TrusteeOrganisation(PowerOnModel):
label: str = Field(
description="Company name",
json_schema_extra={
+ "label": "Bezeichnung",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": True
@@ -33,6 +35,7 @@ class TrusteeOrganisation(PowerOnModel):
default=True,
description="Whether the organisation is enabled",
json_schema_extra={
+ "label": "Aktiviert",
"frontend_type": "checkbox",
"frontend_readonly": False,
"frontend_required": False
@@ -42,6 +45,7 @@ class TrusteeOrganisation(PowerOnModel):
default=None,
description="Mandate ID (system-level organisation)",
json_schema_extra={
+ "label": "Mandat",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -51,6 +55,7 @@ class TrusteeOrganisation(PowerOnModel):
default=None,
description="Feature Instance ID for instance-level isolation",
json_schema_extra={
+ "label": "Feature-Instanz",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -59,25 +64,13 @@ class TrusteeOrganisation(PowerOnModel):
# System attributes are automatically set by DatabaseConnector:
# sysCreatedAt, sysModifiedAt, sysCreatedBy, sysModifiedBy (PowerOnModel)
-
-registerModelLabels(
- "TrusteeOrganisation",
- {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
- "enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- },
-)
-
-
+@i18nModel("Rolle")
class TrusteeRole(PowerOnModel):
"""Defines roles within the Trustee feature."""
id: str = Field( # Unique string label (PK), not UUID
description="Unique role identifier (label)",
json_schema_extra={
+ "label": "ID",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": True
@@ -86,6 +79,7 @@ class TrusteeRole(PowerOnModel):
desc: str = Field(
description="Role description",
json_schema_extra={
+ "label": "Beschreibung",
"frontend_type": "textarea",
"frontend_readonly": False,
"frontend_required": True
@@ -95,6 +89,7 @@ class TrusteeRole(PowerOnModel):
default=None,
description="Mandate ID",
json_schema_extra={
+ "label": "Mandat",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -104,6 +99,7 @@ class TrusteeRole(PowerOnModel):
default=None,
description="Feature Instance ID for instance-level isolation",
json_schema_extra={
+ "label": "Feature-Instanz",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -111,25 +107,14 @@ class TrusteeRole(PowerOnModel):
)
# System attributes are automatically set by DatabaseConnector
-
-registerModelLabels(
- "TrusteeRole",
- {"en": "Role", "fr": "Rôle", "de": "Rolle"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- },
-)
-
-
+@i18nModel("Zugriff")
class TrusteeAccess(PowerOnModel):
"""Defines user access to organisations with specific roles."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique access ID",
json_schema_extra={
+ "label": "ID",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -138,6 +123,7 @@ class TrusteeAccess(PowerOnModel):
organisationId: str = Field(
description="Reference to TrusteeOrganisation.id",
json_schema_extra={
+ "label": "Organisation",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": True,
@@ -147,6 +133,7 @@ class TrusteeAccess(PowerOnModel):
roleId: str = Field(
description="Reference to TrusteeRole.id",
json_schema_extra={
+ "label": "Rolle",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": True,
@@ -156,6 +143,7 @@ class TrusteeAccess(PowerOnModel):
userId: str = Field(
description="User ID assigned to this role",
json_schema_extra={
+ "label": "Benutzer",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": True,
@@ -166,6 +154,7 @@ class TrusteeAccess(PowerOnModel):
default=None,
description="Optional reference to TrusteeContract.id. If None, access is for full organisation. If set, access is limited to this specific contract.",
json_schema_extra={
+ "label": "Vertrag (optional)",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": False,
@@ -177,6 +166,7 @@ class TrusteeAccess(PowerOnModel):
default=None,
description="Mandate ID",
json_schema_extra={
+ "label": "Mandat",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -186,6 +176,7 @@ class TrusteeAccess(PowerOnModel):
default=None,
description="Feature Instance ID for instance-level isolation",
json_schema_extra={
+ "label": "Feature-Instanz",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -193,28 +184,14 @@ class TrusteeAccess(PowerOnModel):
)
# System attributes are automatically set by DatabaseConnector
-
-registerModelLabels(
- "TrusteeAccess",
- {"en": "Access", "fr": "Accès", "de": "Zugriff"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
- "roleId": {"en": "Role", "fr": "Rôle", "de": "Rolle"},
- "userId": {"en": "User", "fr": "Utilisateur", "de": "Benutzer"},
- "contractId": {"en": "Contract (optional)", "fr": "Contrat (optionnel)", "de": "Vertrag (optional)"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- },
-)
-
-
+@i18nModel("Vertrag")
class TrusteeContract(PowerOnModel):
"""Defines customer contracts within organisations."""
id: str = Field(
default_factory=lambda: str(uuid.uuid4()),
description="Unique contract ID",
json_schema_extra={
+ "label": "ID",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -223,6 +200,7 @@ class TrusteeContract(PowerOnModel):
organisationId: str = Field(
description="Reference to TrusteeOrganisation.id (immutable after creation)",
json_schema_extra={
+ "label": "Organisation",
"frontend_type": "select",
"frontend_readonly": False, # Editable at creation, then readonly
"frontend_required": True,
@@ -232,6 +210,7 @@ class TrusteeContract(PowerOnModel):
label: str = Field(
description="Label for the customer contract (e.g., 'Muster AG 2026')",
json_schema_extra={
+ "label": "Bezeichnung",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": True
@@ -241,6 +220,7 @@ class TrusteeContract(PowerOnModel):
default=True,
description="Whether the contract is enabled",
json_schema_extra={
+ "label": "Aktiviert",
"frontend_type": "checkbox",
"frontend_readonly": False,
"frontend_required": False
@@ -250,6 +230,7 @@ class TrusteeContract(PowerOnModel):
default=None,
description="Mandate ID",
json_schema_extra={
+ "label": "Mandat",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -259,6 +240,7 @@ class TrusteeContract(PowerOnModel):
default=None,
description="Feature Instance ID for instance-level isolation",
json_schema_extra={
+ "label": "Feature-Instanz",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -266,21 +248,6 @@ class TrusteeContract(PowerOnModel):
)
# System attributes are automatically set by DatabaseConnector
-
-registerModelLabels(
- "TrusteeContract",
- {"en": "Contract", "fr": "Contrat", "de": "Vertrag"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "organisationId": {"en": "Organisation", "fr": "Organisation", "de": "Organisation"},
- "label": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
- "enabled": {"en": "Enabled", "fr": "Activé", "de": "Aktiviert"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- },
-)
-
-
class TrusteeDocumentTypeEnum(str, Enum):
"""Document type for trustee documents (expense extraction, ingest, sync)."""
INVOICE = "invoice"
@@ -290,7 +257,7 @@ class TrusteeDocumentTypeEnum(str, Enum):
UNKNOWN = "unknown"
AUTO = "auto"
-
+@i18nModel("Dokument")
class TrusteeDocument(PowerOnModel):
"""Contains document references for bookings.
@@ -305,6 +272,7 @@ class TrusteeDocument(PowerOnModel):
default_factory=lambda: str(uuid.uuid4()),
description="Unique document ID",
json_schema_extra={
+ "label": "ID",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -314,6 +282,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="Reference to central Files table (Files.id)",
json_schema_extra={
+ "label": "Datei-Referenz",
"frontend_type": "file_reference",
"frontend_readonly": False,
"frontend_required": False
@@ -322,6 +291,7 @@ class TrusteeDocument(PowerOnModel):
documentName: str = Field(
description="File name (e.g., 'Beleg.pdf')",
json_schema_extra={
+ "label": "Dokumentname",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": True
@@ -331,6 +301,7 @@ class TrusteeDocument(PowerOnModel):
default="application/octet-stream",
description="MIME type of the document",
json_schema_extra={
+ "label": "MIME-Typ",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": True,
@@ -341,6 +312,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="Source type (e.g., 'sharepoint', 'upload', 'email')",
json_schema_extra={
+ "label": "Quelltyp",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -350,6 +322,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="Original source location (e.g., SharePoint path)",
json_schema_extra={
+ "label": "Quellort",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -359,6 +332,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="Mandate ID (auto-set from context)",
json_schema_extra={
+ "label": "Mandat",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -369,6 +343,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="Feature Instance ID for instance-level isolation (auto-set from context)",
json_schema_extra={
+ "label": "Feature-Instanz",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -379,6 +354,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="Document type (e.g. invoice, expense_receipt, bank_document, contract); use TrusteeDocumentTypeEnum values",
json_schema_extra={
+ "label": "Dokumenttyp",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -388,6 +364,7 @@ class TrusteeDocument(PowerOnModel):
default=None,
description="External Beleg-ID in accounting system (e.g. RMA); set on first successful upload, reused on re-sync",
json_schema_extra={
+ "label": "Beleg-ID (Buchhaltung)",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -396,25 +373,7 @@ class TrusteeDocument(PowerOnModel):
)
# System attributes are automatically set by DatabaseConnector
-
-registerModelLabels(
- "TrusteeDocument",
- {"en": "Document", "fr": "Document", "de": "Dokument"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "fileId": {"en": "File Reference", "fr": "Référence du fichier", "de": "Datei-Referenz"},
- "documentName": {"en": "Document Name", "fr": "Nom du document", "de": "Dokumentname"},
- "documentMimeType": {"en": "MIME Type", "fr": "Type MIME", "de": "MIME-Typ"},
- "sourceType": {"en": "Source Type", "fr": "Type de source", "de": "Quelltyp"},
- "sourceLocation": {"en": "Source Location", "fr": "Emplacement source", "de": "Quellort"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- "documentType": {"en": "Document Type", "fr": "Type de document", "de": "Dokumenttyp"},
- "externalBelegId": {"en": "Beleg ID (Accounting)", "fr": "ID Beleg (Comptabilité)", "de": "Beleg-ID (Buchhaltung)"},
- },
-)
-
-
+@i18nModel("Position")
class TrusteePosition(PowerOnModel):
"""Contains booking positions (expense entries).
@@ -425,6 +384,7 @@ class TrusteePosition(PowerOnModel):
default_factory=lambda: str(uuid.uuid4()),
description="Unique position ID",
json_schema_extra={
+ "label": "ID",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False
@@ -434,6 +394,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Reference to TrusteeDocument.id (Beleg / primary document)",
json_schema_extra={
+ "label": "Dokument",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": False,
@@ -444,6 +405,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Reference to TrusteeDocument.id (Bank-Referenz / second document)",
json_schema_extra={
+ "label": "Bank-Referenz",
"frontend_type": "select",
"frontend_readonly": False,
"frontend_required": False,
@@ -454,6 +416,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Value date (ISO format: YYYY-MM-DD)",
json_schema_extra={
+ "label": "Valutadatum",
"frontend_type": "date",
"frontend_readonly": False,
"frontend_required": True
@@ -463,6 +426,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Transaction timestamp (UTC timestamp in seconds)",
json_schema_extra={
+ "label": "Transaktionszeitpunkt",
"frontend_type": "timestamp",
"frontend_readonly": False,
"frontend_required": True
@@ -472,6 +436,7 @@ class TrusteePosition(PowerOnModel):
default="",
description="Company name",
json_schema_extra={
+ "label": "Firma",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -481,6 +446,7 @@ class TrusteePosition(PowerOnModel):
default="",
description="Description",
json_schema_extra={
+ "label": "Beschreibung",
"frontend_type": "textarea",
"frontend_readonly": False,
"frontend_required": False
@@ -490,6 +456,7 @@ class TrusteePosition(PowerOnModel):
default="",
description="Tags (comma-separated)",
json_schema_extra={
+ "label": "Tags",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -514,6 +481,7 @@ class TrusteePosition(PowerOnModel):
default=0.0,
description="Booking amount",
json_schema_extra={
+ "label": "Buchungsbetrag",
"frontend_type": "number",
"frontend_readonly": False,
"frontend_required": True
@@ -538,6 +506,7 @@ class TrusteePosition(PowerOnModel):
default=0.0,
description="Original amount (manual input, no automatic currency conversion)",
json_schema_extra={
+ "label": "Originalbetrag",
"frontend_type": "number",
"frontend_readonly": False,
"frontend_required": True
@@ -547,6 +516,7 @@ class TrusteePosition(PowerOnModel):
default=0.0,
description="VAT percentage",
json_schema_extra={
+ "label": "MwSt-Prozentsatz",
"frontend_type": "number",
"frontend_readonly": False,
"frontend_required": False
@@ -556,6 +526,7 @@ class TrusteePosition(PowerOnModel):
default=0.0,
description="VAT amount (calculated: bookingAmount * vatPercentage / 100, can be manually overridden)",
json_schema_extra={
+ "label": "MwSt-Betrag",
"frontend_type": "number",
"frontend_readonly": False,
"frontend_required": False
@@ -565,6 +536,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Debit account number (e.g. '4200' for expenses)",
json_schema_extra={
+ "label": "Soll-Konto",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -574,6 +546,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Credit account number (e.g. '1020' for bank)",
json_schema_extra={
+ "label": "Haben-Konto",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -583,6 +556,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Tax code for the accounting system",
json_schema_extra={
+ "label": "Steuercode",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -592,6 +566,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Cost center identifier",
json_schema_extra={
+ "label": "Kostenstelle",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -601,6 +576,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Booking reference (e.g. voucher number)",
json_schema_extra={
+ "label": "Buchungsreferenz",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -626,6 +602,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="IBAN of the payment recipient (from invoice / QR code)",
json_schema_extra={
+ "label": "Empfänger-IBAN",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -635,6 +612,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Bank or account holder name of the payment recipient",
json_schema_extra={
+ "label": "Empfänger-Name",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -644,6 +622,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="BIC / SWIFT code of the recipient bank",
json_schema_extra={
+ "label": "Empfänger-BIC",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -653,6 +632,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Structured payment reference (QR-Referenz, ESR, SCOR, Mitteilung)",
json_schema_extra={
+ "label": "Zahlungsreferenz",
"frontend_type": "text",
"frontend_readonly": False,
"frontend_required": False
@@ -662,6 +642,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Payment due date (ISO format: YYYY-MM-DD)",
json_schema_extra={
+ "label": "Fälligkeitsdatum",
"frontend_type": "date",
"frontend_readonly": False,
"frontend_required": False
@@ -671,6 +652,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Mandate ID (auto-set from context)",
json_schema_extra={
+ "label": "Mandat",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -681,6 +663,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="Feature Instance ID for instance-level isolation (auto-set from context)",
json_schema_extra={
+ "label": "Feature-Instanz",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -691,6 +674,7 @@ class TrusteePosition(PowerOnModel):
default=None,
description="External ID (UUID) of the synced record in the accounting system; set by sync, used for duplicate check",
json_schema_extra={
+ "label": "Buha-Sync-ID",
"frontend_type": "text",
"frontend_readonly": True,
"frontend_required": False,
@@ -698,283 +682,118 @@ class TrusteePosition(PowerOnModel):
}
)
-registerModelLabels(
- "TrusteePosition",
- {"en": "Position", "fr": "Position", "de": "Position"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "documentId": {"en": "Document", "fr": "Document", "de": "Dokument"},
- "bankDocumentId": {"en": "Bank Reference", "fr": "Référence bancaire", "de": "Bank-Referenz"},
- "valuta": {"en": "Value Date", "fr": "Date de valeur", "de": "Valutadatum"},
- "transactionDateTime": {"en": "Transaction Date/Time", "fr": "Date/Heure de transaction", "de": "Transaktionszeitpunkt"},
- "company": {"en": "Company", "fr": "Entreprise", "de": "Firma"},
- "desc": {"en": "Description", "fr": "Description", "de": "Beschreibung"},
- "tags": {"en": "Tags", "fr": "Tags", "de": "Tags"},
- "bookingCurrency": {"en": "Booking Currency", "fr": "Devise de comptabilisation", "de": "Buchungswährung"},
- "bookingAmount": {"en": "Booking Amount", "fr": "Montant de comptabilisation", "de": "Buchungsbetrag"},
- "originalCurrency": {"en": "Original Currency", "fr": "Devise d'origine", "de": "Originalwährung"},
- "originalAmount": {"en": "Original Amount", "fr": "Montant d'origine", "de": "Originalbetrag"},
- "vatPercentage": {"en": "VAT Percentage", "fr": "Pourcentage TVA", "de": "MwSt-Prozentsatz"},
- "vatAmount": {"en": "VAT Amount", "fr": "Montant TVA", "de": "MwSt-Betrag"},
- "debitAccountNumber": {"en": "Debit Account", "fr": "Compte débit", "de": "Soll-Konto"},
- "creditAccountNumber": {"en": "Credit Account", "fr": "Compte crédit", "de": "Haben-Konto"},
- "taxCode": {"en": "Tax Code", "fr": "Code TVA", "de": "Steuercode"},
- "costCenter": {"en": "Cost Center", "fr": "Centre de coûts", "de": "Kostenstelle"},
- "bookingReference": {"en": "Booking Reference", "fr": "Référence de réservation", "de": "Buchungsreferenz"},
- "documentType": {"en": "Document Type", "fr": "Type de document", "de": "Dokumenttyp"},
- "payeeIban": {"en": "Payee IBAN", "fr": "IBAN bénéficiaire", "de": "Empfänger-IBAN"},
- "payeeName": {"en": "Payee Name", "fr": "Nom du bénéficiaire", "de": "Empfänger-Name"},
- "payeeBic": {"en": "Payee BIC/SWIFT", "fr": "BIC/SWIFT bénéficiaire", "de": "Empfänger-BIC"},
- "paymentReference": {"en": "Payment Reference", "fr": "Référence de paiement", "de": "Zahlungsreferenz"},
- "dueDate": {"en": "Due Date", "fr": "Date d'échéance", "de": "Fälligkeitsdatum"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance de fonctionnalité", "de": "Feature-Instanz"},
- "accountingSyncId": {"en": "Accounting Sync ID", "fr": "ID sync comptabilité", "de": "Buha-Sync-ID"},
- },
-)
-
-
# ── TrusteeData* tables (synced from external accounting apps for analysis) ──
-
+@i18nModel("Konto (Sync)")
class TrusteeDataAccount(PowerOnModel):
"""Chart of accounts synced from external accounting system."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- accountNumber: str = Field(description="Account number (e.g. '1020')")
- label: str = Field(default="", description="Account name")
- accountType: Optional[str] = Field(default=None, description="asset / liability / equity / revenue / expense")
- accountGroup: Optional[str] = Field(default=None, description="Account group/category")
- currency: str = Field(default="CHF", description="Account currency")
- isActive: bool = Field(default=True)
- mandateId: Optional[str] = Field(default=None)
- featureInstanceId: Optional[str] = Field(default=None)
-
-
-registerModelLabels(
- "TrusteeDataAccount",
- {"en": "Account (Synced)", "de": "Konto (Sync)", "fr": "Compte (Sync)"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "accountNumber": {"en": "Account Number", "de": "Kontonummer", "fr": "Numéro de compte"},
- "label": {"en": "Name", "de": "Bezeichnung", "fr": "Libellé"},
- "accountType": {"en": "Type", "de": "Typ", "fr": "Type"},
- "accountGroup": {"en": "Group", "de": "Gruppe", "fr": "Groupe"},
- "currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
- "isActive": {"en": "Active", "de": "Aktiv", "fr": "Actif"},
- "mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ accountNumber: str = Field(description="Account number (e.g. '1020')", json_schema_extra={"label": "Kontonummer"})
+ label: str = Field(default="", description="Account name", json_schema_extra={"label": "Bezeichnung"})
+ accountType: Optional[str] = Field(default=None, description="asset / liability / equity / revenue / expense", json_schema_extra={"label": "Typ"})
+ accountGroup: Optional[str] = Field(default=None, description="Account group/category", json_schema_extra={"label": "Gruppe"})
+ currency: str = Field(default="CHF", description="Account currency", json_schema_extra={"label": "Währung"})
+ isActive: bool = Field(default=True, json_schema_extra={"label": "Aktiv"})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
+ featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
+@i18nModel("Buchung (Sync)")
class TrusteeDataJournalEntry(PowerOnModel):
"""Journal entry header synced from external accounting system."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- externalId: Optional[str] = Field(default=None, description="ID in the source system")
- bookingDate: Optional[str] = Field(default=None, description="Booking date (YYYY-MM-DD)")
- reference: Optional[str] = Field(default=None, description="Booking reference / voucher number")
- description: str = Field(default="", description="Booking text")
- currency: str = Field(default="CHF")
- totalAmount: float = Field(default=0.0, description="Total amount of entry")
- mandateId: Optional[str] = Field(default=None)
- featureInstanceId: Optional[str] = Field(default=None)
-
-
-registerModelLabels(
- "TrusteeDataJournalEntry",
- {"en": "Journal Entry (Synced)", "de": "Buchung (Sync)", "fr": "Écriture (Sync)"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "externalId": {"en": "External ID", "de": "Externe ID", "fr": "ID externe"},
- "bookingDate": {"en": "Date", "de": "Datum", "fr": "Date"},
- "reference": {"en": "Reference", "de": "Referenz", "fr": "Référence"},
- "description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
- "currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
- "totalAmount": {"en": "Amount", "de": "Betrag", "fr": "Montant"},
- "mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ externalId: Optional[str] = Field(default=None, description="ID in the source system", json_schema_extra={"label": "Externe ID"})
+ bookingDate: Optional[str] = Field(default=None, description="Booking date (YYYY-MM-DD)", json_schema_extra={"label": "Datum"})
+ reference: Optional[str] = Field(default=None, description="Booking reference / voucher number", json_schema_extra={"label": "Referenz"})
+ description: str = Field(default="", description="Booking text", json_schema_extra={"label": "Beschreibung"})
+ currency: str = Field(default="CHF", json_schema_extra={"label": "Währung"})
+ totalAmount: float = Field(default=0.0, description="Total amount of entry", json_schema_extra={"label": "Betrag"})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
+ featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
+@i18nModel("Buchungszeile (Sync)")
class TrusteeDataJournalLine(PowerOnModel):
"""Journal entry line (debit/credit) synced from external accounting system."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- journalEntryId: str = Field(description="FK → TrusteeDataJournalEntry.id")
- accountNumber: str = Field(description="Account number")
- debitAmount: float = Field(default=0.0)
- creditAmount: float = Field(default=0.0)
- currency: str = Field(default="CHF")
- taxCode: Optional[str] = Field(default=None)
- costCenter: Optional[str] = Field(default=None)
- description: str = Field(default="")
- mandateId: Optional[str] = Field(default=None)
- featureInstanceId: Optional[str] = Field(default=None)
-
-
-registerModelLabels(
- "TrusteeDataJournalLine",
- {"en": "Journal Line (Synced)", "de": "Buchungszeile (Sync)", "fr": "Ligne écriture (Sync)"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "journalEntryId": {"en": "Journal Entry", "de": "Buchung", "fr": "Écriture"},
- "accountNumber": {"en": "Account", "de": "Konto", "fr": "Compte"},
- "debitAmount": {"en": "Debit", "de": "Soll", "fr": "Débit"},
- "creditAmount": {"en": "Credit", "de": "Haben", "fr": "Crédit"},
- "currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
- "taxCode": {"en": "Tax Code", "de": "Steuercode", "fr": "Code TVA"},
- "costCenter": {"en": "Cost Center", "de": "Kostenstelle", "fr": "Centre de coûts"},
- "description": {"en": "Description", "de": "Beschreibung", "fr": "Description"},
- "mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ journalEntryId: str = Field(description="FK → TrusteeDataJournalEntry.id", json_schema_extra={"label": "Buchung"})
+ accountNumber: str = Field(description="Account number", json_schema_extra={"label": "Konto"})
+ debitAmount: float = Field(default=0.0, json_schema_extra={"label": "Soll"})
+ creditAmount: float = Field(default=0.0, json_schema_extra={"label": "Haben"})
+ currency: str = Field(default="CHF", json_schema_extra={"label": "Währung"})
+ taxCode: Optional[str] = Field(default=None, json_schema_extra={"label": "Steuercode"})
+ costCenter: Optional[str] = Field(default=None, json_schema_extra={"label": "Kostenstelle"})
+ description: str = Field(default="", json_schema_extra={"label": "Beschreibung"})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
+ featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
+@i18nModel("Kontakt (Sync)")
class TrusteeDataContact(PowerOnModel):
"""Customer or vendor synced from external accounting system."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- externalId: Optional[str] = Field(default=None, description="ID in the source system")
- contactType: str = Field(default="customer", description="customer / vendor / both")
- contactNumber: Optional[str] = Field(default=None, description="Customer/vendor number")
- name: str = Field(default="", description="Name / company")
- address: Optional[str] = Field(default=None)
- zip: Optional[str] = Field(default=None)
- city: Optional[str] = Field(default=None)
- country: Optional[str] = Field(default=None)
- email: Optional[str] = Field(default=None)
- phone: Optional[str] = Field(default=None)
- vatNumber: Optional[str] = Field(default=None)
- mandateId: Optional[str] = Field(default=None)
- featureInstanceId: Optional[str] = Field(default=None)
-
-
-registerModelLabels(
- "TrusteeDataContact",
- {"en": "Contact (Synced)", "de": "Kontakt (Sync)", "fr": "Contact (Sync)"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "externalId": {"en": "External ID", "de": "Externe ID", "fr": "ID externe"},
- "contactType": {"en": "Type", "de": "Typ", "fr": "Type"},
- "contactNumber": {"en": "Number", "de": "Nummer", "fr": "Numéro"},
- "name": {"en": "Name", "de": "Name", "fr": "Nom"},
- "address": {"en": "Address", "de": "Adresse", "fr": "Adresse"},
- "zip": {"en": "ZIP", "de": "PLZ", "fr": "NPA"},
- "city": {"en": "City", "de": "Ort", "fr": "Ville"},
- "country": {"en": "Country", "de": "Land", "fr": "Pays"},
- "email": {"en": "Email", "de": "E-Mail", "fr": "E-mail"},
- "phone": {"en": "Phone", "de": "Telefon", "fr": "Téléphone"},
- "vatNumber": {"en": "VAT Number", "de": "MWST-Nr.", "fr": "N° TVA"},
- "mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ externalId: Optional[str] = Field(default=None, description="ID in the source system", json_schema_extra={"label": "Externe ID"})
+ contactType: str = Field(default="customer", description="customer / vendor / both", json_schema_extra={"label": "Typ"})
+ contactNumber: Optional[str] = Field(default=None, description="Customer/vendor number", json_schema_extra={"label": "Nummer"})
+ name: str = Field(default="", description="Name / company", json_schema_extra={"label": "Name"})
+ address: Optional[str] = Field(default=None, json_schema_extra={"label": "Adresse"})
+ zip: Optional[str] = Field(default=None, json_schema_extra={"label": "PLZ"})
+ city: Optional[str] = Field(default=None, json_schema_extra={"label": "Ort"})
+ country: Optional[str] = Field(default=None, json_schema_extra={"label": "Land"})
+ email: Optional[str] = Field(default=None, json_schema_extra={"label": "E-Mail"})
+ phone: Optional[str] = Field(default=None, json_schema_extra={"label": "Telefon"})
+ vatNumber: Optional[str] = Field(default=None, json_schema_extra={"label": "MWST-Nr."})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
+ featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
+@i18nModel("Kontosaldo (Sync)")
class TrusteeDataAccountBalance(PowerOnModel):
"""Account balance per period, derived from journal lines or directly from accounting system."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- accountNumber: str = Field(description="Account number")
- periodYear: int = Field(description="Fiscal year")
- periodMonth: int = Field(default=0, description="Month (1-12); 0 = annual total")
- openingBalance: float = Field(default=0.0)
- debitTotal: float = Field(default=0.0)
- creditTotal: float = Field(default=0.0)
- closingBalance: float = Field(default=0.0)
- currency: str = Field(default="CHF")
- mandateId: Optional[str] = Field(default=None)
- featureInstanceId: Optional[str] = Field(default=None)
-
-
-registerModelLabels(
- "TrusteeDataAccountBalance",
- {"en": "Account Balance (Synced)", "de": "Kontosaldo (Sync)", "fr": "Solde compte (Sync)"},
- {
- "id": {"en": "ID", "de": "ID", "fr": "ID"},
- "accountNumber": {"en": "Account", "de": "Konto", "fr": "Compte"},
- "periodYear": {"en": "Year", "de": "Jahr", "fr": "Année"},
- "periodMonth": {"en": "Month", "de": "Monat", "fr": "Mois"},
- "openingBalance": {"en": "Opening Balance", "de": "Eröffnungssaldo", "fr": "Solde d'ouverture"},
- "debitTotal": {"en": "Debit Total", "de": "Soll-Umsatz", "fr": "Total débit"},
- "creditTotal": {"en": "Credit Total", "de": "Haben-Umsatz", "fr": "Total crédit"},
- "closingBalance": {"en": "Closing Balance", "de": "Schlusssaldo", "fr": "Solde de clôture"},
- "currency": {"en": "Currency", "de": "Währung", "fr": "Devise"},
- "mandateId": {"en": "Mandate", "de": "Mandat", "fr": "Mandat"},
- "featureInstanceId": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ accountNumber: str = Field(description="Account number", json_schema_extra={"label": "Konto"})
+ periodYear: int = Field(description="Fiscal year", json_schema_extra={"label": "Jahr"})
+ periodMonth: int = Field(default=0, description="Month (1-12); 0 = annual total", json_schema_extra={"label": "Monat"})
+ openingBalance: float = Field(default=0.0, json_schema_extra={"label": "Eröffnungssaldo"})
+ debitTotal: float = Field(default=0.0, json_schema_extra={"label": "Soll-Umsatz"})
+ creditTotal: float = Field(default=0.0, json_schema_extra={"label": "Haben-Umsatz"})
+ closingBalance: float = Field(default=0.0, json_schema_extra={"label": "Schlusssaldo"})
+ currency: str = Field(default="CHF", json_schema_extra={"label": "Währung"})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
+ featureInstanceId: Optional[str] = Field(default=None, json_schema_extra={"label": "Feature-Instanz"})
+@i18nModel("Buchhaltungs-Konfiguration")
class TrusteeAccountingConfig(PowerOnModel):
"""Per-instance accounting system configuration with encrypted credentials.
Each feature instance can connect to exactly one accounting system.
Credentials are stored encrypted (decrypted at runtime by the AccountingBridge).
"""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- featureInstanceId: str = Field(description="FK -> FeatureInstance.id (1:1)")
- connectorType: str = Field(description="Connector type key, e.g. 'rma', 'bexio', 'abacus'")
- displayLabel: str = Field(default="", description="User-visible label for this integration")
- encryptedConfig: str = Field(default="", description="Encrypted JSON blob with connector credentials")
- isActive: bool = Field(default=True)
- lastSyncAt: Optional[float] = Field(default=None, description="Timestamp of last sync attempt")
- lastSyncStatus: Optional[str] = Field(default=None, description="Last sync result: success, error, partial")
- lastSyncErrorMessage: Optional[str] = Field(default=None, description="Error message when lastSyncStatus is error")
- cachedChartOfAccounts: Optional[str] = Field(default=None, description="JSON-serialised chart of accounts cache (list of {accountNumber, label, accountType})")
- chartCachedAt: Optional[float] = Field(default=None, description="Timestamp when cachedChartOfAccounts was last refreshed")
- mandateId: Optional[str] = Field(default=None)
-
-
-registerModelLabels(
- "TrusteeAccountingConfig",
- {"en": "Accounting Configuration", "de": "Buchhaltungs-Konfiguration", "fr": "Configuration comptable"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "featureInstanceId": {"en": "Feature Instance", "fr": "Instance", "de": "Feature-Instanz"},
- "connectorType": {"en": "System", "fr": "Système", "de": "System"},
- "displayLabel": {"en": "Label", "fr": "Libellé", "de": "Bezeichnung"},
- "isActive": {"en": "Active", "fr": "Actif", "de": "Aktiv"},
- "lastSyncAt": {"en": "Last Sync", "fr": "Dernière sync.", "de": "Letzte Synchronisation"},
- "lastSyncStatus": {"en": "Status", "fr": "Statut", "de": "Status"},
- "lastSyncErrorMessage": {"en": "Error", "fr": "Erreur", "de": "Fehlermeldung"},
- "cachedChartOfAccounts": {"en": "Cached Chart", "de": "Cached Kontoplan", "fr": "Plan comptable en cache"},
- "chartCachedAt": {"en": "Chart Cached At", "de": "Kontoplan-Cache-Zeitpunkt", "fr": "Horodatage cache plan comptable"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- },
-)
-
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ featureInstanceId: str = Field(description="FK -> FeatureInstance.id (1:1)", json_schema_extra={"label": "Feature-Instanz"})
+ connectorType: str = Field(description="Connector type key, e.g. 'rma', 'bexio', 'abacus'", json_schema_extra={"label": "System"})
+ displayLabel: str = Field(default="", description="User-visible label for this integration", json_schema_extra={"label": "Bezeichnung"})
+ encryptedConfig: str = Field(default="", description="Encrypted JSON blob with connector credentials", json_schema_extra={"label": "Verschlüsselte Konfiguration"})
+ isActive: bool = Field(default=True, json_schema_extra={"label": "Aktiv"})
+ lastSyncAt: Optional[float] = Field(default=None, description="Timestamp of last sync attempt", json_schema_extra={"label": "Letzte Synchronisation"})
+ lastSyncStatus: Optional[str] = Field(default=None, description="Last sync result: success, error, partial", json_schema_extra={"label": "Status"})
+ lastSyncErrorMessage: Optional[str] = Field(default=None, description="Error message when lastSyncStatus is error", json_schema_extra={"label": "Fehlermeldung"})
+ cachedChartOfAccounts: Optional[str] = Field(default=None, description="JSON-serialised chart of accounts cache (list of {accountNumber, label, accountType})", json_schema_extra={"label": "Cached Kontoplan"})
+ chartCachedAt: Optional[float] = Field(default=None, description="Timestamp when cachedChartOfAccounts was last refreshed", json_schema_extra={"label": "Kontoplan-Cache-Zeitpunkt"})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
+@i18nModel("Buchhaltungs-Synchronisation")
class TrusteeAccountingSync(PowerOnModel):
"""Tracks which position was synced to which external system and when.
Used for duplicate prevention, audit trail, and retry logic.
"""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()))
- positionId: str = Field(description="FK -> TrusteePosition.id")
- featureInstanceId: str = Field(description="FK -> FeatureInstance.id")
- connectorType: str = Field(description="Connector type at time of sync")
- externalId: Optional[str] = Field(default=None, description="ID assigned by the external system")
- externalReference: Optional[str] = Field(default=None, description="Reference in the external system")
- syncStatus: str = Field(default="pending", description="pending | synced | error | cancelled")
- syncDirection: str = Field(default="push", description="push (local->ext) or pull (ext->local)")
- syncedAt: Optional[float] = Field(default=None, description="Timestamp of successful sync")
- errorMessage: Optional[str] = Field(default=None)
- bookingPayload: Optional[dict] = Field(default=None, description="Payload sent to the external system (audit)")
- mandateId: Optional[str] = Field(default=None)
+ id: str = Field(default_factory=lambda: str(uuid.uuid4()), json_schema_extra={"label": "ID"})
+ positionId: str = Field(description="FK -> TrusteePosition.id", json_schema_extra={"label": "Position"})
+ featureInstanceId: str = Field(description="FK -> FeatureInstance.id", json_schema_extra={"label": "Feature-Instanz"})
+ connectorType: str = Field(description="Connector type at time of sync", json_schema_extra={"label": "System"})
+ externalId: Optional[str] = Field(default=None, description="ID assigned by the external system", json_schema_extra={"label": "Externe ID"})
+ externalReference: Optional[str] = Field(default=None, description="Reference in the external system", json_schema_extra={"label": "Externe Referenz"})
+ syncStatus: str = Field(default="pending", description="pending | synced | error | cancelled", json_schema_extra={"label": "Status"})
+ syncDirection: str = Field(default="push", description="push (local->ext) or pull (ext->local)", json_schema_extra={"label": "Richtung"})
+ syncedAt: Optional[float] = Field(default=None, description="Timestamp of successful sync", json_schema_extra={"label": "Synchronisiert am"})
+ errorMessage: Optional[str] = Field(default=None, json_schema_extra={"label": "Fehler"})
+ bookingPayload: Optional[dict] = Field(default=None, description="Payload sent to the external system (audit)", json_schema_extra={"label": "Buchungs-Payload"})
+ mandateId: Optional[str] = Field(default=None, json_schema_extra={"label": "Mandat"})
-
-registerModelLabels(
- "TrusteeAccountingSync",
- {"en": "Accounting Sync", "de": "Buchhaltungs-Synchronisation", "fr": "Synchronisation comptable"},
- {
- "id": {"en": "ID", "fr": "ID", "de": "ID"},
- "positionId": {"en": "Position", "fr": "Position", "de": "Position"},
- "connectorType": {"en": "System", "fr": "Système", "de": "System"},
- "externalId": {"en": "External ID", "fr": "ID Externe", "de": "Externe ID"},
- "syncStatus": {"en": "Status", "fr": "Statut", "de": "Status"},
- "syncDirection": {"en": "Direction", "fr": "Direction", "de": "Richtung"},
- "syncedAt": {"en": "Synced At", "fr": "Synchronisé à", "de": "Synchronisiert am"},
- "errorMessage": {"en": "Error", "fr": "Erreur", "de": "Fehler"},
- "mandateId": {"en": "Mandate", "fr": "Mandat", "de": "Mandat"},
- },
-)
diff --git a/modules/features/trustee/mainTrustee.py b/modules/features/trustee/mainTrustee.py
index 2fd82bc5..65f5f7e4 100644
--- a/modules/features/trustee/mainTrustee.py
+++ b/modules/features/trustee/mainTrustee.py
@@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
# Feature metadata
FEATURE_CODE = "trustee"
-FEATURE_LABEL = {"en": "Trustee", "de": "Treuhand", "fr": "Fiduciaire"}
+FEATURE_LABEL = "Treuhand"
FEATURE_ICON = "mdi-briefcase"
# UI Objects for RBAC catalog
@@ -20,37 +20,47 @@ FEATURE_ICON = "mdi-briefcase"
UI_OBJECTS = [
{
"objectKey": "ui.feature.trustee.dashboard",
- "label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
+ "label": "Dashboard",
"meta": {"area": "dashboard"}
},
{
"objectKey": "ui.feature.trustee.positions",
- "label": {"en": "Positions", "de": "Positionen", "fr": "Positions"},
+ "label": "Positionen",
"meta": {"area": "positions"}
},
{
"objectKey": "ui.feature.trustee.documents",
- "label": {"en": "Documents", "de": "Dokumente", "fr": "Documents"},
+ "label": "Dokumente",
"meta": {"area": "documents"}
},
{
"objectKey": "ui.feature.trustee.expense-import",
- "label": {"en": "Expense Import", "de": "Spesen Import", "fr": "Import de dépenses"},
+ "label": "Spesen Import",
"meta": {"area": "expense-import"}
},
{
"objectKey": "ui.feature.trustee.scan-upload",
- "label": {"en": "Scan / Upload", "de": "Scannen / Hochladen", "fr": "Scanner / Téléverser"},
+ "label": "Scannen / Hochladen",
"meta": {"area": "scan-upload"}
},
+ {
+ "objectKey": "ui.feature.trustee.analyse",
+ "label": "Analyse & Reporting",
+ "meta": {"area": "analyse"}
+ },
+ {
+ "objectKey": "ui.feature.trustee.abschluss",
+ "label": "Abschluss & Prüfung",
+ "meta": {"area": "abschluss"}
+ },
{
"objectKey": "ui.feature.trustee.settings",
- "label": {"en": "Accounting Settings", "de": "Buchhaltungs-Einstellungen", "fr": "Paramètres comptables"},
+ "label": "Buchhaltungs-Einstellungen",
"meta": {"area": "settings", "admin_only": True}
},
{
"objectKey": "ui.feature.trustee.instance-roles",
- "label": {"en": "Instance Roles & Permissions", "de": "Instanz-Rollen & Berechtigungen", "fr": "Rôles et permissions d'instance"},
+ "label": "Instanz-Rollen & Berechtigungen",
"meta": {"area": "admin", "admin_only": True}
},
]
@@ -60,7 +70,7 @@ UI_OBJECTS = [
DATA_OBJECTS = [
{
"objectKey": "data.feature.trustee.TrusteeOrganisation",
- "label": {"en": "Organisation", "de": "Organisation", "fr": "Organisation"},
+ "label": "Organisation",
"meta": {
"table": "TrusteeOrganisation",
"fields": ["id", "label", "enabled"],
@@ -70,7 +80,7 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.trustee.TrusteePosition",
- "label": {"en": "Position", "de": "Position", "fr": "Position"},
+ "label": "Position",
"meta": {
"table": "TrusteePosition",
"fields": ["id", "label", "description", "organisationId"],
@@ -80,12 +90,12 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.trustee.TrusteeDocument",
- "label": {"en": "Document", "de": "Dokument", "fr": "Document"},
+ "label": "Dokument",
"meta": {"table": "TrusteeDocument", "fields": ["id", "filename", "mimeType", "fileSize", "uploadDate"]}
},
{
"objectKey": "data.feature.trustee.TrusteeAccountingConfig",
- "label": {"en": "Accounting Config", "de": "Buchhaltungs-Konfiguration", "fr": "Config. comptable"},
+ "label": "Buchhaltungs-Konfiguration",
"meta": {
"table": "TrusteeAccountingConfig",
"fields": ["id", "connectorType", "displayLabel", "encryptedConfig", "isActive"],
@@ -95,37 +105,37 @@ DATA_OBJECTS = [
},
{
"objectKey": "data.feature.trustee.TrusteeAccountingSync",
- "label": {"en": "Accounting Sync", "de": "Buchhaltungs-Synchronisation", "fr": "Sync. comptable"},
+ "label": "Buchhaltungs-Synchronisation",
"meta": {"table": "TrusteeAccountingSync", "fields": ["id", "positionId", "syncStatus", "externalId"]}
},
{
"objectKey": "data.feature.trustee.TrusteeDataAccount",
- "label": {"en": "Accounts (Synced)", "de": "Kontenplan (Sync)", "fr": "Plan comptable (Sync)"},
+ "label": "Kontenplan (Sync)",
"meta": {"table": "TrusteeDataAccount", "fields": ["id", "accountNumber", "label", "accountType", "accountGroup", "currency", "isActive"]}
},
{
"objectKey": "data.feature.trustee.TrusteeDataJournalEntry",
- "label": {"en": "Journal Entries (Synced)", "de": "Buchungen (Sync)", "fr": "Écritures (Sync)"},
+ "label": "Buchungen (Sync)",
"meta": {"table": "TrusteeDataJournalEntry", "fields": ["id", "externalId", "bookingDate", "reference", "description", "currency", "totalAmount"]}
},
{
"objectKey": "data.feature.trustee.TrusteeDataJournalLine",
- "label": {"en": "Journal Lines (Synced)", "de": "Buchungszeilen (Sync)", "fr": "Lignes écriture (Sync)"},
+ "label": "Buchungszeilen (Sync)",
"meta": {"table": "TrusteeDataJournalLine", "fields": ["id", "journalEntryId", "accountNumber", "debitAmount", "creditAmount", "currency", "taxCode", "costCenter", "description"]}
},
{
"objectKey": "data.feature.trustee.TrusteeDataContact",
- "label": {"en": "Contacts (Synced)", "de": "Kontakte (Sync)", "fr": "Contacts (Sync)"},
+ "label": "Kontakte (Sync)",
"meta": {"table": "TrusteeDataContact", "fields": ["id", "externalId", "contactType", "contactNumber", "name", "address", "zip", "city", "country", "email", "phone", "vatNumber"]}
},
{
"objectKey": "data.feature.trustee.TrusteeDataAccountBalance",
- "label": {"en": "Account Balances (Synced)", "de": "Kontosalden (Sync)", "fr": "Soldes comptes (Sync)"},
+ "label": "Kontosalden (Sync)",
"meta": {"table": "TrusteeDataAccountBalance", "fields": ["id", "accountNumber", "periodYear", "periodMonth", "openingBalance", "debitTotal", "creditTotal", "closingBalance", "currency"]}
},
{
"objectKey": "data.feature.trustee.*",
- "label": {"en": "All Trustee Data", "de": "Alle Treuhand-Daten", "fr": "Toutes les données fiduciaires"},
+ "label": "Alle Treuhand-Daten",
"meta": {"wildcard": True, "description": "Wildcard for all trustee data tables"}
},
]
@@ -135,127 +145,379 @@ DATA_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.trustee.documents.create",
- "label": {"en": "Upload Document", "de": "Dokument hochladen", "fr": "Télécharger document"},
+ "label": "Dokument hochladen",
"meta": {"endpoint": "/api/trustee/{instanceId}/documents", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.documents.update",
- "label": {"en": "Update Document", "de": "Dokument aktualisieren", "fr": "Modifier document"},
+ "label": "Dokument aktualisieren",
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "PUT"}
},
{
"objectKey": "resource.feature.trustee.documents.delete",
- "label": {"en": "Delete Document", "de": "Dokument löschen", "fr": "Supprimer document"},
+ "label": "Dokument löschen",
"meta": {"endpoint": "/api/trustee/{instanceId}/documents/{documentId}", "method": "DELETE"}
},
{
"objectKey": "resource.feature.trustee.positions.create",
- "label": {"en": "Create Position", "de": "Position erstellen", "fr": "Créer position"},
+ "label": "Position erstellen",
"meta": {"endpoint": "/api/trustee/{instanceId}/positions", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.positions.update",
- "label": {"en": "Update Position", "de": "Position aktualisieren", "fr": "Modifier position"},
+ "label": "Position aktualisieren",
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "PUT"}
},
{
"objectKey": "resource.feature.trustee.positions.delete",
- "label": {"en": "Delete Position", "de": "Position löschen", "fr": "Supprimer position"},
+ "label": "Position löschen",
"meta": {"endpoint": "/api/trustee/{instanceId}/positions/{positionId}", "method": "DELETE"}
},
{
"objectKey": "resource.feature.trustee.instance-roles.manage",
- "label": {"en": "Manage Instance Roles", "de": "Instanz-Rollen verwalten", "fr": "Gérer les rôles d'instance"},
+ "label": "Instanz-Rollen verwalten",
"meta": {"endpoint": "/api/trustee/{instanceId}/instance-roles", "method": "ALL", "admin_only": True}
},
{
"objectKey": "resource.feature.trustee.accounting.manage",
- "label": {"en": "Manage Accounting Integration", "de": "Buchhaltungs-Integration verwalten", "fr": "Gérer l'intégration comptable"},
+ "label": "Buchhaltungs-Integration verwalten",
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/config", "method": "ALL", "admin_only": True}
},
{
"objectKey": "resource.feature.trustee.accounting.sync",
- "label": {"en": "Sync to Accounting", "de": "Buchhaltung synchronisieren", "fr": "Synchroniser la comptabilité"},
+ "label": "Buchhaltung synchronisieren",
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync", "method": "POST"}
},
{
"objectKey": "resource.feature.trustee.accounting.view",
- "label": {"en": "View Sync Status", "de": "Sync-Status einsehen", "fr": "Voir le statut de synchronisation"},
+ "label": "Sync-Status einsehen",
"meta": {"endpoint": "/api/trustee/{instanceId}/accounting/sync-status", "method": "GET"}
},
+ {
+ "objectKey": "resource.feature.trustee.workflows.view",
+ "label": "Workflows einsehen",
+ "meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "GET"}
+ },
+ {
+ "objectKey": "resource.feature.trustee.workflows.execute",
+ "label": "Workflows ausführen",
+ "meta": {"endpoint": "/api/workflows/{instanceId}/execute", "method": "POST"}
+ },
+ {
+ "objectKey": "resource.feature.trustee.workflows.manage",
+ "label": "Workflows verwalten",
+ "meta": {"endpoint": "/api/workflows/{instanceId}/workflows", "method": "ALL", "admin_only": True}
+ },
]
# Template roles for this feature with AccessRules
# Each role defines default UI and DATA permissions
# Note: UI item=None means ALL views, specific items restrict to named views
# IMPORTANT: item uses vollqualifizierte ObjectKeys (gemäss Navigation-API-Konzept)
+QUICK_ACTION_CATEGORIES = [
+ {"id": "import", "label": "Import & Verarbeitung", "sortOrder": 1},
+ {"id": "analyse", "label": "Analyse & Reporting", "sortOrder": 2},
+ {"id": "abschluss", "label": "Abschluss & Prüfung", "sortOrder": 3},
+]
+
+QUICK_ACTIONS = [
+ {
+ "id": "trustee-process-receipts",
+ "label": "Belege verarbeiten",
+ "description": "Belege aus SharePoint importieren, klassifizieren und verbuchen",
+ "icon": "mdi-file-document-check-outline",
+ "color": "#4CAF50",
+ "category": "import",
+ "actionType": "link",
+ "config": {"targetView": "expense-import"},
+ "requiredRoles": ["trustee-user", "trustee-accountant", "trustee-admin"],
+ "sortOrder": 1,
+ },
+ {
+ "id": "trustee-sync-accounting",
+ "label": "Daten synchronisieren",
+ "description": "Buchhaltungsdaten aus dem externen System aktualisieren",
+ "icon": "mdi-sync",
+ "color": "#FF9800",
+ "category": "import",
+ "actionType": "link",
+ "config": {"targetView": "settings"},
+ "requiredRoles": ["trustee-accountant", "trustee-admin"],
+ "sortOrder": 2,
+ },
+ {
+ "id": "trustee-upload-receipt",
+ "label": "Beleg hochladen",
+ "description": "Beleg scannen oder als Datei hochladen",
+ "icon": "mdi-camera-document-outline",
+ "color": "#607D8B",
+ "category": "import",
+ "actionType": "link",
+ "config": {"targetView": "scan-upload"},
+ "requiredRoles": ["trustee-user", "trustee-client", "trustee-accountant", "trustee-admin"],
+ "sortOrder": 3,
+ },
+ {
+ "id": "trustee-budget-comparison",
+ "label": "Budget-Vergleich",
+ "description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel",
+ "icon": "mdi-chart-bar",
+ "color": "#2196F3",
+ "category": "analyse",
+ "actionType": "link",
+ "config": {"targetView": "analyse", "tab": "budget"},
+ "requiredRoles": ["trustee-accountant", "trustee-admin"],
+ "sortOrder": 4,
+ },
+ {
+ "id": "trustee-kpi-dashboard",
+ "label": "KPI-Dashboard",
+ "description": "Kennzahlen berechnen und visualisieren",
+ "icon": "mdi-view-dashboard-outline",
+ "color": "#9C27B0",
+ "category": "analyse",
+ "actionType": "link",
+ "config": {"targetView": "analyse", "tab": "kpi"},
+ "requiredRoles": ["trustee-accountant", "trustee-admin"],
+ "sortOrder": 5,
+ },
+ {
+ "id": "trustee-cashflow",
+ "label": "Cashflow-Rechnung",
+ "description": "Cashflow berechnen und analysieren",
+ "icon": "mdi-cash-multiple",
+ "color": "#009688",
+ "category": "analyse",
+ "actionType": "link",
+ "config": {"targetView": "analyse", "tab": "cashflow"},
+ "requiredRoles": ["trustee-accountant", "trustee-admin"],
+ "sortOrder": 6,
+ },
+ {
+ "id": "trustee-forecast",
+ "label": "Prognose erstellen",
+ "description": "Trend-Analyse und Prognose der nächsten Monate",
+ "icon": "mdi-chart-timeline-variant",
+ "color": "#E91E63",
+ "category": "analyse",
+ "actionType": "link",
+ "config": {"targetView": "analyse", "tab": "forecast"},
+ "requiredRoles": ["trustee-accountant", "trustee-admin"],
+ "sortOrder": 7,
+ },
+ {
+ "id": "trustee-year-end-check",
+ "label": "Jahresabschluss prüfen",
+ "description": "Automatische Prüfungen für den Jahresabschluss",
+ "icon": "mdi-clipboard-check-outline",
+ "color": "#795548",
+ "category": "abschluss",
+ "actionType": "link",
+ "config": {"targetView": "abschluss", "tab": "year-end"},
+ "requiredRoles": ["trustee-accountant", "trustee-admin"],
+ "sortOrder": 8,
+ },
+]
+
+
+# ---------------------------------------------------------------------------
+# Template Workflows — bootstrapped into each new feature instance.
+# Graphs use existing nodes: trigger.manual, trustee.refreshAccountingData, ai.prompt.
+# The placeholder {{featureInstanceId}} is replaced by _copyTemplateWorkflows.
+# ---------------------------------------------------------------------------
+
+def _buildAnalysisWorkflowGraph(prompt: str) -> Dict[str, Any]:
+ """Build a standard analysis graph: trigger → refreshAccountingData → ai.prompt."""
+ return {
+ "nodes": [
+ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
+ {"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten laden", "_method": "trustee", "_action": "refreshAccountingData",
+ "parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": False}, "position": {"x": 250, "y": 0}},
+ {"id": "analyse", "type": "ai.prompt", "label": "Analyse", "_method": "ai", "_action": "process",
+ "parameters": {"prompt": prompt, "simpleMode": False}, "position": {"x": 500, "y": 0}},
+ ],
+ "connections": [
+ {"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
+ {"source": "refresh", "sourcePort": 0, "target": "analyse", "targetPort": 0},
+ ],
+ }
+
+
+TEMPLATE_WORKFLOWS = [
+ {
+ "id": "trustee-receipt-import",
+ "label": "Beleg-Import Pipeline",
+ "description": "Belege extrahieren, verarbeiten und in Buchhaltung synchronisieren",
+ "tags": ["feature:trustee", "template:trustee-receipt-import"],
+ "graph": {
+ "nodes": [
+ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
+ {"id": "extract", "type": "trustee.extractFromFiles", "label": "Dokumente extrahieren", "_method": "trustee", "_action": "extractFromFiles",
+ "parameters": {"featureInstanceId": "{{featureInstanceId}}", "prompt": ""}, "position": {"x": 250, "y": 0}},
+ {"id": "process", "type": "trustee.processDocuments", "label": "Verarbeiten", "_method": "trustee", "_action": "processDocuments",
+ "parameters": {"documentList": [], "featureInstanceId": "{{featureInstanceId}}"}, "position": {"x": 500, "y": 0}},
+ {"id": "sync", "type": "trustee.syncToAccounting", "label": "Synchronisieren", "_method": "trustee", "_action": "syncToAccounting",
+ "parameters": {"documentList": [], "featureInstanceId": "{{featureInstanceId}}"}, "position": {"x": 750, "y": 0}},
+ ],
+ "connections": [
+ {"source": "trigger", "sourcePort": 0, "target": "extract", "targetPort": 0},
+ {"source": "extract", "sourcePort": 0, "target": "process", "targetPort": 0},
+ {"source": "process", "sourcePort": 0, "target": "sync", "targetPort": 0},
+ ],
+ },
+ },
+ {
+ "id": "trustee-sync-accounting",
+ "label": "Buchhaltung synchronisieren",
+ "description": "Buchhaltungsdaten aus dem externen System aktualisieren",
+ "tags": ["feature:trustee", "template:trustee-sync-accounting"],
+ "graph": {
+ "nodes": [
+ {"id": "trigger", "type": "trigger.manual", "label": "Start", "_method": "", "_action": "", "parameters": {}, "position": {"x": 0, "y": 0}},
+ {"id": "refresh", "type": "trustee.refreshAccountingData", "label": "Daten aktualisieren", "_method": "trustee", "_action": "refreshAccountingData",
+ "parameters": {"featureInstanceId": "{{featureInstanceId}}", "forceRefresh": True}, "position": {"x": 250, "y": 0}},
+ ],
+ "connections": [
+ {"source": "trigger", "sourcePort": 0, "target": "refresh", "targetPort": 0},
+ ],
+ },
+ },
+ {
+ "id": "trustee-budget-comparison",
+ "label": "Budget-Vergleich",
+ "description": "Soll/Ist-Vergleich der Buchhaltung mit Budget-Excel",
+ "tags": ["feature:trustee", "template:trustee-budget-comparison"],
+ "graph": _buildAnalysisWorkflowGraph(
+ "Ich möchte einen Budget-Soll/Ist-Vergleich durchführen. Bitte:\n"
+ "1. Frage mich nach der Budget-Datei (Excel) oder suche im Workspace nach einer Datei mit 'Budget' im Namen\n"
+ "2. Lade die aktuellen Buchhaltungsdaten (refreshTrusteeData falls nötig)\n"
+ "3. Vergleiche die Soll-Werte aus dem Budget mit den Ist-Werten aus der Buchhaltung pro Konto\n"
+ "4. Berechne die Abweichung (absolut und prozentual)\n"
+ "5. Erstelle ein Abweichungs-Chart (Balkendiagramm: Soll vs. Ist pro Konto)\n"
+ "6. Markiere kritische Abweichungen (>10%) und gib eine kurze Einschätzung"
+ ),
+ },
+ {
+ "id": "trustee-kpi-dashboard",
+ "label": "KPI-Dashboard",
+ "description": "Kennzahlen berechnen und visualisieren",
+ "tags": ["feature:trustee", "template:trustee-kpi-dashboard"],
+ "graph": _buildAnalysisWorkflowGraph(
+ "Erstelle ein KPI-Dashboard basierend auf den aktuellen Buchhaltungsdaten. Berechne und visualisiere:\n"
+ "1. Bruttogewinn und Bruttogewinnmarge\n"
+ "2. EBIT (Betriebsergebnis)\n"
+ "3. Gewinnmarge (Reingewinn / Umsatz)\n"
+ "4. Eigenkapitalquote und Check auf hälftigen Kapitalverlust (OR Art. 725)\n"
+ "5. Liquiditätsgrad 1-3 (Cash Ratio, Quick Ratio, Current Ratio)\n"
+ "6. Überschuldungs-Check\n\n"
+ "Erstelle für jede Kennzahl einen kurzen Kommentar (gut/kritisch/Handlungsbedarf). "
+ "Erstelle mindestens 2 Charts: ein Übersichts-Chart der Margen und ein Liquiditäts-Chart."
+ ),
+ },
+ {
+ "id": "trustee-cashflow",
+ "label": "Cashflow-Rechnung",
+ "description": "Cashflow berechnen und analysieren",
+ "tags": ["feature:trustee", "template:trustee-cashflow"],
+ "graph": _buildAnalysisWorkflowGraph(
+ "Erstelle eine Cashflow-Rechnung basierend auf den aktuellen Buchhaltungsdaten:\n"
+ "1. Operativer Cashflow: Starte vom Reingewinn, bereinige um nicht-cash-wirksame Positionen\n"
+ "2. Investitions-Cashflow: Investitionen in Sachanlagen, Finanzanlagen\n"
+ "3. Finanzierungs-Cashflow: Darlehensaufnahmen/-rückzahlungen, Dividenden, Kapitalerhöhungen\n"
+ "4. Netto-Cashflow und Veränderung der liquiden Mittel\n\n"
+ "Warne bei kritischen Werten. Erstelle ein Wasserfall-Chart oder gestapeltes Balkendiagramm."
+ ),
+ },
+ {
+ "id": "trustee-forecast",
+ "label": "Prognose erstellen",
+ "description": "Trend-Analyse und Prognose der nächsten Monate",
+ "tags": ["feature:trustee", "template:trustee-forecast"],
+ "graph": _buildAnalysisWorkflowGraph(
+ "Erstelle eine Finanzprognose basierend auf den historischen Buchhaltungsdaten:\n"
+ "1. Analysiere die Umsatz- und Aufwandsentwicklung der letzten 6 Monate\n"
+ "2. Identifiziere Trends und Saisonalitäten\n"
+ "3. Prognostiziere Umsatz, Aufwand und Gewinn für die nächsten 3 Monate\n"
+ "4. Erstelle ein Chart mit Ist-Werten und Prognose-Korridor\n"
+ "5. Markiere Risiken\n\n"
+ "Nutze eine einfache lineare Extrapolation mit Saisonalitätskorrektur wo sinnvoll."
+ ),
+ },
+ {
+ "id": "trustee-year-end-check",
+ "label": "Jahresabschluss prüfen",
+ "description": "Automatische Prüfungen für den Jahresabschluss",
+ "tags": ["feature:trustee", "template:trustee-year-end-check"],
+ "graph": _buildAnalysisWorkflowGraph(
+ "Führe eine automatische Jahresabschluss-Prüfung durch:\n"
+ "1. Saldovalidierung: Prüfe alle Bilanzkonten auf Plausibilität\n"
+ "2. Vorjahresvergleich: Vergleiche Bilanz- und ER-Positionen mit dem Vorjahr, markiere Abweichungen >20%\n"
+ "3. Abgrenzungen: Identifiziere potenzielle transitorische Aktiven/Passiven\n"
+ "4. Gesetzliche Prüfungen: Hälftiger Kapitalverlust (OR 725), Überschuldung, Mindestkapital\n"
+ "5. MWST-Plausibilisierung: Vorsteuer vs. geschätzter Aufwand, Umsatzsteuer vs. Umsatz\n\n"
+ "Erstelle eine Checkliste mit Status (OK / Warnung / Kritisch) pro Prüfpunkt."
+ ),
+ },
+]
+
+
TEMPLATE_ROLES = [
{
"roleLabel": "trustee-viewer",
- "description": {
- "en": "Trustee Viewer - View trustee data (read-only)",
- "de": "Treuhand-Betrachter - Treuhand-Daten einsehen (nur lesen)",
- "fr": "Visualiseur fiduciaire - Consulter les données fiduciaires (lecture seule)",
- },
+ "description": "Treuhand-Betrachter - Treuhand-Daten einsehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"},
],
},
{
"roleLabel": "trustee-user",
- "description": {
- "en": "Trustee User - Create and manage own trustee records",
- "de": "Treuhand-Benutzer - Eigene Treuhand-Daten erstellen und verwalten",
- "fr": "Utilisateur fiduciaire - Créer et gérer ses propres données fiduciaires",
- },
+ "description": "Treuhand-Benutzer - Eigene Treuhand-Daten erstellen und verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
{"context": "UI", "item": "ui.feature.trustee.expense-import", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"},
],
},
{
"roleLabel": "trustee-admin",
- "description": {
- "en": "Trustee Administrator - Full access to all trustee data and settings",
- "de": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
- "fr": "Administrateur fiduciaire - Accès complet aux données et paramètres fiduciaires",
- },
+ "description": "Treuhand-Administrator - Vollzugriff auf alle Treuhand-Daten und Einstellungen",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "DATA", "item": None, "view": True, "read": "a", "create": "a", "update": "a", "delete": "a"},
{"context": "RESOURCE", "item": "resource.feature.trustee.instance-roles.manage", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.manage", "view": True},
],
},
{
"roleLabel": "trustee-accountant",
- "description": {
- "en": "Trustee Accountant - Manage accounting and financial data",
- "de": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
- "fr": "Comptable fiduciaire - Gérer les données comptables et financières",
- },
+ "description": "Treuhand-Buchhalter - Buchhaltungs- und Finanzdaten verwalten",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
{"context": "UI", "item": "ui.feature.trustee.documents", "view": True},
+ {"context": "UI", "item": "ui.feature.trustee.analyse", "view": True},
+ {"context": "UI", "item": "ui.feature.trustee.abschluss", "view": True},
{"context": "UI", "item": "ui.feature.trustee.settings", "view": True},
{"context": "DATA", "item": None, "view": True, "read": "g", "create": "g", "update": "g", "delete": "g"},
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.sync", "view": True},
{"context": "RESOURCE", "item": "resource.feature.trustee.accounting.view", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.view", "view": True},
+ {"context": "RESOURCE", "item": "resource.feature.trustee.workflows.execute", "view": True},
],
},
{
"roleLabel": "trustee-client",
- "description": {
- "en": "Trustee Client - View own accounting data and documents",
- "de": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
- "fr": "Client fiduciaire - Consulter ses propres données comptables et documents",
- },
+ "description": "Treuhand-Kunde - Eigene Buchhaltungsdaten und Dokumente einsehen",
"accessRules": [
{"context": "UI", "item": "ui.feature.trustee.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.trustee.positions", "view": True},
@@ -293,6 +555,21 @@ def getTemplateRoles() -> List[Dict[str, Any]]:
return TEMPLATE_ROLES
+def getTemplateWorkflows() -> List[Dict[str, Any]]:
+ """Return template workflow definitions for bootstrap on instance creation."""
+ return TEMPLATE_WORKFLOWS
+
+
+def getQuickActions() -> List[Dict[str, Any]]:
+ """Return quick action definitions for the Trustee dashboard."""
+ return QUICK_ACTIONS
+
+
+def getQuickActionCategories() -> List[Dict[str, Any]]:
+ """Return quick action category definitions."""
+ return QUICK_ACTION_CATEGORIES
+
+
def getDataObjects() -> List[Dict[str, Any]]:
"""Return DATA objects for RBAC catalog registration."""
return DATA_OBJECTS
@@ -358,7 +635,8 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
-
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
+
rootInterface = getRootInterface()
# Get existing template roles for this feature (Pydantic models)
@@ -378,7 +656,7 @@ def _syncTemplateRolesToDb() -> int:
# Create new template role
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None, # Global template
featureInstanceId=None,
diff --git a/modules/features/trustee/routeFeatureTrustee.py b/modules/features/trustee/routeFeatureTrustee.py
index ca8caf90..f4068e66 100644
--- a/modules/features/trustee/routeFeatureTrustee.py
+++ b/modules/features/trustee/routeFeatureTrustee.py
@@ -37,6 +37,10 @@ from modules.datamodels.datamodelPagination import (
PaginationMetadata,
normalize_pagination_dict,
)
+from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+from modules.shared.i18nRegistry import apiRouteContext
+
+routeApiMsg = apiRouteContext("routeFeatureTrustee")
logger = logging.getLogger(__name__)
@@ -116,6 +120,78 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext) -> str:
return str(instance.mandateId)
+# ============================================================================
+# QUICK ACTIONS ENDPOINT
+# ============================================================================
+
+@router.get("/{instanceId}/quick-actions")
+@limiter.limit("60/minute")
+def getQuickActions(
+ request: Request,
+ instanceId: str = Path(..., description="Feature Instance ID"),
+ language: str = Query(default="de", description="Language code for labels"),
+ context: RequestContext = Depends(getRequestContext),
+) -> Dict[str, Any]:
+ """Return RBAC-filtered quick actions for the Trustee dashboard."""
+ mandateId = _validateInstanceAccess(instanceId, context)
+
+ from .mainTrustee import QUICK_ACTIONS, QUICK_ACTION_CATEGORIES
+
+ userRoleLabels: set = set()
+ if context.hasSysAdminRole:
+ userRoleLabels.add("trustee-admin")
+ else:
+ rootInterface = getRootInterface()
+ featureAccesses = rootInterface.getFeatureAccessesForUser(str(context.user.id))
+ for fa in featureAccesses:
+ if str(fa.featureInstanceId) == instanceId and fa.enabled:
+ roleIds = fa.roleIds if hasattr(fa, "roleIds") and fa.roleIds else []
+ for rid in roleIds:
+ role = rootInterface.getRole(str(rid))
+ if role and role.roleLabel:
+ userRoleLabels.add(role.roleLabel)
+
+ def _resolveText(multilingual, lang: str) -> str:
+ if isinstance(multilingual, str):
+ return multilingual
+ if isinstance(multilingual, dict):
+ return multilingual.get(lang) or multilingual.get("en") or multilingual.get("de") or next(iter(multilingual.values()), "")
+ return ""
+
+ filteredActions = []
+ for action in QUICK_ACTIONS:
+ required = set(action.get("requiredRoles", []))
+ if not userRoleLabels and not context.hasSysAdminRole:
+ continue
+ if context.hasSysAdminRole or required.intersection(userRoleLabels):
+ resolved = {
+ "id": action["id"],
+ "label": _resolveText(action.get("label", {}), language),
+ "description": _resolveText(action.get("description", {}), language),
+ "icon": action.get("icon", ""),
+ "color": action.get("color", ""),
+ "category": action.get("category", ""),
+ "actionType": action.get("actionType", ""),
+ "config": action.get("config", {}),
+ "sortOrder": action.get("sortOrder", 99),
+ }
+ if resolved["actionType"] == "agentPrompt" and "config" in resolved:
+ cfg = dict(resolved["config"])
+ if "uploadHint" in cfg:
+ cfg["uploadHint"] = _resolveText(cfg["uploadHint"], language)
+ resolved["config"] = cfg
+ filteredActions.append(resolved)
+
+ filteredActions.sort(key=lambda a: a["sortOrder"])
+
+ resolvedCategories = [
+ {"id": c["id"], "label": _resolveText(c.get("label", {}), language), "sortOrder": c.get("sortOrder", 99)}
+ for c in QUICK_ACTION_CATEGORIES
+ ]
+
+ return {"actions": filteredActions, "categories": resolvedCategories}
+
+
# ============================================================================
# ATTRIBUTES ENDPOINT (for FormGeneratorTable)
# ============================================================================
@@ -385,7 +461,7 @@ def create_organisation(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createOrganisation(data.model_dump())
if not result:
- raise HTTPException(status_code=400, detail="Failed to create organisation")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create organisation"))
return result
@@ -408,7 +484,7 @@ def update_organisation(
result = interface.updateOrganisation(orgId, data.model_dump(exclude={"id"}))
if not result:
- raise HTTPException(status_code=400, detail="Failed to update organisation")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update organisation"))
return result
@@ -430,7 +506,7 @@ def delete_organisation(
success = interface.deleteOrganisation(orgId)
if not success:
- raise HTTPException(status_code=400, detail="Failed to delete organisation")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete organisation"))
return {"message": f"Organisation {orgId} deleted"}
@@ -498,7 +574,7 @@ def create_role(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createRole(data.model_dump())
if not result:
- raise HTTPException(status_code=400, detail="Failed to create role")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create role"))
return result
@@ -521,7 +597,7 @@ def update_role(
result = interface.updateRole(roleId, data.model_dump(exclude={"id"}))
if not result:
- raise HTTPException(status_code=400, detail="Failed to update role")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update role"))
return result
@@ -543,7 +619,7 @@ def delete_role(
success = interface.deleteRole(roleId)
if not success:
- raise HTTPException(status_code=400, detail="Failed to delete role (may be in use)")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete role (may be in use)"))
return {"message": f"Role {roleId} deleted"}
@@ -641,7 +717,7 @@ def create_access(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createAccess(data.model_dump())
if not result:
- raise HTTPException(status_code=400, detail="Failed to create access")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create access"))
return result
@@ -664,7 +740,7 @@ def update_access(
result = interface.updateAccess(accessId, data.model_dump(exclude={"id"}))
if not result:
- raise HTTPException(status_code=400, detail="Failed to update access")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update access"))
return result
@@ -686,7 +762,7 @@ def delete_access(
success = interface.deleteAccess(accessId)
if not success:
- raise HTTPException(status_code=400, detail="Failed to delete access")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete access"))
return {"message": f"Access {accessId} deleted"}
@@ -769,7 +845,7 @@ def create_contract(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createContract(data.model_dump())
if not result:
- raise HTTPException(status_code=400, detail="Failed to create contract")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create contract"))
return result
@@ -792,7 +868,7 @@ def update_contract(
result = interface.updateContract(contractId, data.model_dump(exclude={"id"}))
if not result:
- raise HTTPException(status_code=400, detail="Failed to update contract (organisationId cannot be changed)")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update contract (organisationId cannot be changed)"))
return result
@@ -814,7 +890,7 @@ def delete_contract(
success = interface.deleteContract(contractId)
if not success:
- raise HTTPException(status_code=400, detail="Failed to delete contract")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete contract"))
return {"message": f"Contract {contractId} deleted"}
@@ -938,7 +1014,7 @@ def get_document_data(
data = interface.getDocumentData(documentId)
if not data:
- raise HTTPException(status_code=404, detail="Document data not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Document data not found"))
return StreamingResponse(
io.BytesIO(data),
@@ -995,7 +1071,7 @@ async def create_document(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createDocument(body)
if not result:
- raise HTTPException(status_code=400, detail="Failed to create document")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create document"))
return result
@@ -1025,7 +1101,7 @@ async def upload_document(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createDocument(docData)
if not result:
- raise HTTPException(status_code=400, detail="Failed to create document")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create document"))
return result
@@ -1048,7 +1124,7 @@ def update_document(
result = interface.updateDocument(documentId, data.model_dump(exclude={"id"}))
if not result:
- raise HTTPException(status_code=400, detail="Failed to update document")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update document"))
return result
@@ -1070,7 +1146,7 @@ def delete_document(
success = interface.deleteDocument(documentId)
if not success:
- raise HTTPException(status_code=400, detail="Failed to delete document")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete document"))
return {"message": f"Document {documentId} deleted"}
@@ -1220,7 +1296,7 @@ def create_position(
interface = getInterface(context.user, mandateId=mandateId, featureInstanceId=instanceId)
result = interface.createPosition(data.model_dump())
if not result:
- raise HTTPException(status_code=400, detail="Failed to create position")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to create position"))
return result
@@ -1243,7 +1319,7 @@ def update_position(
result = interface.updatePosition(positionId, data.model_dump(exclude={"id"}))
if not result:
- raise HTTPException(status_code=400, detail="Failed to update position")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to update position"))
return result
@@ -1265,7 +1341,7 @@ def delete_position(
success = interface.deletePosition(positionId)
if not success:
- raise HTTPException(status_code=400, detail="Failed to delete position")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Failed to delete position"))
return {"message": f"Position {positionId} deleted"}
@@ -1398,7 +1474,7 @@ async def save_accounting_config(
if not plainConfig:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="config is required for new integration (e.g. clientName, apiKey)."
+ detail=routeApiMsg("config is required for new integration (e.g. clientName, apiKey).")
)
encryptedConfig = encryptValue(json.dumps(plainConfig), keyName="accountingConfig")
@@ -1511,7 +1587,7 @@ async def sync_positions_to_accounting(
positionIds = data.get("positionIds", [])
if not positionIds:
- raise HTTPException(status_code=400, detail="positionIds required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("positionIds required"))
results = await bridge.pushBatchToAccounting(instanceId, positionIds)
failed = [r for r in results if not r.success]
@@ -1678,8 +1754,6 @@ def get_positions_by_document(
# ===== Instance Roles Management =====
# These endpoints allow feature admins to manage instance-specific roles and their AccessRules
-from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
-
def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str:
"""
@@ -1711,7 +1785,7 @@ def _validateInstanceAdmin(instanceId: str, context: RequestContext) -> str:
if not hasAdminPermission:
raise HTTPException(
status_code=403,
- detail="Keine Berechtigung zur Rollenverwaltung"
+ detail=routeApiMsg("Keine Berechtigung zur Rollenverwaltung")
)
return mandateId
diff --git a/modules/features/workspace/datamodelFeatureWorkspace.py b/modules/features/workspace/datamodelFeatureWorkspace.py
index d7c292db..b01f0427 100644
--- a/modules/features/workspace/datamodelFeatureWorkspace.py
+++ b/modules/features/workspace/datamodelFeatureWorkspace.py
@@ -5,27 +5,32 @@
from typing import Optional
from pydantic import BaseModel, Field
from modules.datamodels.datamodelBase import PowerOnModel
-from modules.shared.attributeUtils import registerModelLabels
+from modules.shared.i18nRegistry import i18nModel
import uuid
+@i18nModel("Workspace Benutzereinstellungen")
class WorkspaceUserSettings(PowerOnModel):
- """Per-user workspace settings. None values mean 'use instance default'."""
- id: str = Field(default_factory=lambda: str(uuid.uuid4()), description="Primary key", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": False})
- userId: str = Field(description="User ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- mandateId: str = Field(description="Mandate ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- featureInstanceId: str = Field(description="Feature Instance ID", json_schema_extra={"frontend_type": "text", "frontend_readonly": True, "frontend_required": True})
- maxAgentRounds: Optional[int] = Field(default=None, description="Max agent rounds override (None = instance default)", json_schema_extra={"frontend_type": "number", "frontend_readonly": False, "frontend_required": False})
-
-
-registerModelLabels(
- "WorkspaceUserSettings",
- {"en": "Workspace User Settings", "de": "Workspace Benutzereinstellungen"},
- {
- "id": {"en": "ID", "de": "ID"},
- "userId": {"en": "User ID", "de": "Benutzer-ID"},
- "mandateId": {"en": "Mandate ID", "de": "Mandanten-ID"},
- "featureInstanceId": {"en": "Feature Instance ID", "de": "Feature-Instanz-ID"},
- "maxAgentRounds": {"en": "Max Agent Rounds", "de": "Max. Agenten-Runden"},
- },
-)
+ """Benutzerspezifische Workspace-Einstellungen. None = Instanz-Standard."""
+ id: str = Field(
+ default_factory=lambda: str(uuid.uuid4()),
+ description="Primary key",
+ json_schema_extra={"label": "ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": False},
+ )
+ userId: str = Field(
+ description="User ID",
+ json_schema_extra={"label": "Benutzer-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ mandateId: str = Field(
+ description="Mandate ID",
+ json_schema_extra={"label": "Mandanten-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ featureInstanceId: str = Field(
+ description="Feature Instance ID",
+ json_schema_extra={"label": "Feature-Instanz-ID", "frontend_type": "text", "frontend_readonly": True, "frontend_required": True},
+ )
+ maxAgentRounds: Optional[int] = Field(
+ default=None,
+ description="Max agent rounds override (None = instance default)",
+ json_schema_extra={"label": "Max. Agenten-Runden", "frontend_type": "number", "frontend_readonly": False, "frontend_required": False},
+ )
diff --git a/modules/features/workspace/mainWorkspace.py b/modules/features/workspace/mainWorkspace.py
index 5ef9b399..bb501f21 100644
--- a/modules/features/workspace/mainWorkspace.py
+++ b/modules/features/workspace/mainWorkspace.py
@@ -12,32 +12,28 @@ from typing import Dict, List, Any
logger = logging.getLogger(__name__)
FEATURE_CODE = "workspace"
-FEATURE_LABEL = {"en": "AI Workspace", "de": "AI Workspace", "fr": "AI Workspace"}
+FEATURE_LABEL = "AI Workspace"
FEATURE_ICON = "mdi-brain"
UI_OBJECTS = [
{
"objectKey": "ui.feature.workspace.dashboard",
- "label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"},
+ "label": "Dashboard",
"meta": {"area": "dashboard"}
},
{
"objectKey": "ui.feature.workspace.editor",
- "label": {"en": "Editor", "de": "Editor", "fr": "Editeur"},
+ "label": "Editor",
"meta": {"area": "editor"}
},
{
"objectKey": "ui.feature.workspace.settings",
- "label": {"en": "Settings", "de": "Einstellungen", "fr": "Parametres"},
+ "label": "Einstellungen",
"meta": {"area": "settings"}
},
{
"objectKey": "ui.feature.workspace.rag-insights",
- "label": {
- "en": "Knowledge insights",
- "de": "Wissens-Insights",
- "fr": "Aperçu des connaissances",
- },
+ "label": "Wissens-Insights",
"meta": {"area": "rag-insights"},
},
]
@@ -45,37 +41,37 @@ UI_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.feature.workspace.start",
- "label": {"en": "Start Agent", "de": "Agent starten", "fr": "Demarrer agent"},
+ "label": "Agent starten",
"meta": {"endpoint": "/api/workspace/{instanceId}/start/stream", "method": "POST"}
},
{
"objectKey": "resource.feature.workspace.stop",
- "label": {"en": "Stop Agent", "de": "Agent stoppen", "fr": "Arreter agent"},
+ "label": "Agent stoppen",
"meta": {"endpoint": "/api/workspace/{instanceId}/{workflowId}/stop", "method": "POST"}
},
{
"objectKey": "resource.feature.workspace.files",
- "label": {"en": "Manage Files", "de": "Dateien verwalten", "fr": "Gerer fichiers"},
+ "label": "Dateien verwalten",
"meta": {"endpoint": "/api/workspace/{instanceId}/files", "method": "GET"}
},
{
"objectKey": "resource.feature.workspace.folders",
- "label": {"en": "Manage Folders", "de": "Ordner verwalten", "fr": "Gerer dossiers"},
+ "label": "Ordner verwalten",
"meta": {"endpoint": "/api/workspace/{instanceId}/folders", "method": "GET"}
},
{
"objectKey": "resource.feature.workspace.datasources",
- "label": {"en": "Data Sources", "de": "Datenquellen", "fr": "Sources de donnees"},
+ "label": "Datenquellen",
"meta": {"endpoint": "/api/workspace/{instanceId}/datasources", "method": "GET"}
},
{
"objectKey": "resource.feature.workspace.voice",
- "label": {"en": "Voice Input/Output", "de": "Spracheingabe/-ausgabe", "fr": "Entree/sortie vocale"},
+ "label": "Spracheingabe/-ausgabe",
"meta": {"endpoint": "/api/workspace/{instanceId}/voice/*", "method": "POST"}
},
{
"objectKey": "resource.feature.workspace.edits",
- "label": {"en": "Review File Edits", "de": "Datei-Aenderungen pruefen", "fr": "Verifier les modifications de fichiers"},
+ "label": "Datei-Aenderungen pruefen",
"meta": {"endpoint": "/api/workspace/{instanceId}/edit/*", "method": "POST"}
},
]
@@ -83,11 +79,7 @@ RESOURCE_OBJECTS = [
TEMPLATE_ROLES = [
{
"roleLabel": "workspace-viewer",
- "description": {
- "en": "Workspace Viewer - View workspace (read-only)",
- "de": "Workspace Betrachter - Workspace ansehen (nur lesen)",
- "fr": "Visualiseur Workspace - Consulter le workspace (lecture seule)"
- },
+ "description": "Workspace Betrachter - Workspace ansehen (nur lesen)",
"accessRules": [
{"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.workspace.editor", "view": True},
@@ -98,11 +90,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "workspace-user",
- "description": {
- "en": "Workspace User - Use AI workspace and tools",
- "de": "Workspace Benutzer - AI Workspace und Tools nutzen",
- "fr": "Utilisateur Workspace - Utiliser l'espace de travail AI et les outils"
- },
+ "description": "Workspace Benutzer - AI Workspace und Tools nutzen",
"accessRules": [
{"context": "UI", "item": "ui.feature.workspace.dashboard", "view": True},
{"context": "UI", "item": "ui.feature.workspace.editor", "view": True},
@@ -120,11 +108,7 @@ TEMPLATE_ROLES = [
},
{
"roleLabel": "workspace-admin",
- "description": {
- "en": "Workspace Admin - All UI and API actions; data is always scoped to own records (same privacy as users)",
- "de": "Workspace Admin - Alle UI- und API-Aktionen; Daten immer nur eigene Datensätze (gleiche Privatsphäre wie User)",
- "fr": "Administrateur Workspace - Toute l'UI et les API; donnees limitees a ses propres enregistrements"
- },
+ "description": "Workspace Admin - Alle UI- und API-Aktionen; Daten immer nur eigene Datensätze (gleiche Privatsphäre wie User)",
"accessRules": [
{"context": "UI", "item": None, "view": True},
{"context": "RESOURCE", "item": None, "view": True},
@@ -194,6 +178,7 @@ def _syncTemplateRolesToDb() -> int:
try:
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext
+ from modules.datamodels.datamodelUtils import coerce_text_multilingual
rootInterface = getRootInterface()
@@ -211,7 +196,7 @@ def _syncTemplateRolesToDb() -> int:
else:
newRole = Role(
roleLabel=roleLabel,
- description=roleTemplate.get("description", {}),
+ description=coerce_text_multilingual(roleTemplate.get("description", {})),
featureCode=FEATURE_CODE,
mandateId=None,
featureInstanceId=None,
diff --git a/modules/features/workspace/routeFeatureWorkspace.py b/modules/features/workspace/routeFeatureWorkspace.py
index 85188c52..9fb8ca40 100644
--- a/modules/features/workspace/routeFeatureWorkspace.py
+++ b/modules/features/workspace/routeFeatureWorkspace.py
@@ -29,6 +29,8 @@ from modules.interfaces.interfaceAiObjects import AiObjects
from modules.serviceCenter.core.serviceStreaming import get_event_manager
from modules.serviceCenter.services.serviceAgent.datamodelAgent import AgentEventTypeEnum, PendingFileEdit
from modules.shared.timeUtils import parseTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeFeatureWorkspace")
logger = logging.getLogger(__name__)
@@ -127,7 +129,7 @@ def _validateInstanceAccess(instanceId: str, context: RequestContext):
raise HTTPException(status_code=404, detail=f"Feature instance {instanceId} not found")
featureAccess = rootInterface.getFeatureAccess(str(context.user.id), instanceId)
if not featureAccess or not featureAccess.enabled:
- raise HTTPException(status_code=403, detail="Access denied to this feature instance")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied to this feature instance"))
mandateId = str(instance.mandateId) if instance.mandateId else None
instanceConfig = instance.config if hasattr(instance, "config") and instance.config else {}
return mandateId, instanceConfig
@@ -1178,10 +1180,10 @@ async def getFileContent(
fileData = fileRecord if isinstance(fileRecord, dict) else fileRecord.model_dump()
filePath = fileData.get("filePath")
if not filePath:
- raise HTTPException(status_code=404, detail="File has no stored path")
+ raise HTTPException(status_code=404, detail=routeApiMsg("File has no stored path"))
import os
if not os.path.isfile(filePath):
- raise HTTPException(status_code=404, detail="File not found on disk")
+ raise HTTPException(status_code=404, detail=routeApiMsg("File not found on disk"))
mimeType = fileData.get("mimeType", "application/octet-stream")
with open(filePath, "rb") as fh:
content = fh.read()
@@ -1436,11 +1438,11 @@ async def listFeatureConnectionTables(
rootIf = getRootInterface()
inst = rootIf.getFeatureInstance(fiId)
if not inst:
- raise HTTPException(status_code=404, detail="Feature instance not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Feature instance not found"))
mandateId = str(inst.mandateId) if inst.mandateId else None
if wsMandateId and mandateId and mandateId != wsMandateId:
- raise HTTPException(status_code=403, detail="Feature instance does not belong to workspace mandate")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Feature instance does not belong to workspace mandate"))
catalog = getCatalogService()
try:
@@ -1495,12 +1497,12 @@ async def listParentObjects(
rootIf = getRootInterface()
inst = rootIf.getFeatureInstance(fiId)
if not inst:
- raise HTTPException(status_code=404, detail="Feature instance not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Feature instance not found"))
featureCode = inst.featureCode
mandateId = str(inst.mandateId) if inst.mandateId else ""
if wsMandateId and mandateId and mandateId != wsMandateId:
- raise HTTPException(status_code=403, detail="Feature instance does not belong to workspace mandate")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Feature instance does not belong to workspace mandate"))
catalog = getCatalogService()
parentObj = None
@@ -1614,7 +1616,7 @@ async def createFeatureDataSource(
inst = rootIf.getFeatureInstance(body.featureInstanceId)
mandateId = str(inst.mandateId) if inst else (str(context.mandateId) if context.mandateId else "")
if wsMandateId and mandateId and mandateId != wsMandateId:
- raise HTTPException(status_code=403, detail="Feature instance does not belong to workspace mandate")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Feature instance does not belong to workspace mandate"))
fds = FeatureDataSource(
featureInstanceId=body.featureInstanceId,
@@ -1814,7 +1816,7 @@ async def synthesizeVoice(
_validateInstanceAccess(instanceId, context)
text = body.get("text", "")
if not text:
- raise HTTPException(status_code=400, detail="text is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("text is required"))
return JSONResponse({"audio": None, "note": "TTS via browser Speech Synthesis API recommended"})
@@ -1858,7 +1860,7 @@ async def acceptEdit(
try:
success = dbMgmt.updateFileData(edit.fileId, edit.newContent.encode("utf-8"))
if not success:
- raise HTTPException(status_code=500, detail="Failed to update file data")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to update file data"))
except HTTPException:
raise
except Exception as e:
diff --git a/modules/interfaces/interfaceBootstrap.py b/modules/interfaces/interfaceBootstrap.py
index 0f424438..b323cb92 100644
--- a/modules/interfaces/interfaceBootstrap.py
+++ b/modules/interfaces/interfaceBootstrap.py
@@ -25,6 +25,7 @@ from modules.datamodels.datamodelRbac import (
AccessRuleContext,
Role,
)
+from modules.datamodels.datamodelUtils import coerce_text_multilingual
from modules.datamodels.datamodelUam import AccessLevel
from modules.datamodels.datamodelMembership import (
UserMandate,
@@ -547,7 +548,7 @@ def initRoles(db: DatabaseConnector) -> None:
standardRoles = [
Role(
roleLabel="admin",
- description={"en": "Administrator - Manage users and resources within mandate scope", "de": "Administrator - Benutzer und Ressourcen im Mandanten verwalten", "fr": "Administrateur - Gérer les utilisateurs et ressources dans le périmètre du mandat"},
+ description=coerce_text_multilingual("Administrator - Benutzer und Ressourcen im Mandanten verwalten"),
mandateId=None, # Global template role
featureInstanceId=None,
featureCode=None,
@@ -555,7 +556,7 @@ def initRoles(db: DatabaseConnector) -> None:
),
Role(
roleLabel="user",
- description={"en": "User - Standard user with access to own records", "de": "Benutzer - Standard-Benutzer mit Zugriff auf eigene Datensätze", "fr": "Utilisateur - Utilisateur standard avec accès à ses propres enregistrements"},
+ description="Benutzer - Standard-Benutzer mit Zugriff auf eigene Datensätze",
mandateId=None, # Global template role
featureInstanceId=None,
featureCode=None,
@@ -563,7 +564,7 @@ def initRoles(db: DatabaseConnector) -> None:
),
Role(
roleLabel="viewer",
- description={"en": "Viewer - Read-only access to group records", "de": "Betrachter - Nur-Lese-Zugriff auf Gruppen-Datensätze", "fr": "Visualiseur - Accès en lecture seule aux enregistrements du groupe"},
+ description=coerce_text_multilingual("Betrachter - Nur-Lese-Zugriff auf Gruppen-Datensätze"),
mandateId=None, # Global template role
featureInstanceId=None,
featureCode=None,
@@ -728,7 +729,7 @@ def copySystemRolesToMandate(db: DatabaseConnector, mandateId: str) -> int:
newRole = Role(
id=newRoleId,
roleLabel=roleLabel,
- description=templateRole.get("description", {}),
+ description=coerce_text_multilingual(templateRole.get("description", {})),
mandateId=mandateId,
featureInstanceId=None,
featureCode=None,
@@ -797,11 +798,7 @@ def _initSysAdminRole(db: DatabaseConnector, mandateId: str) -> Optional[str]:
logger.info("Creating sysadmin role in root mandate")
sysadminRole = Role(
roleLabel="sysadmin",
- description={
- "en": "System Administrator - Full administrative access across all mandates",
- "de": "System-Administrator - Vollständiger administrativer Zugriff über alle Mandanten",
- "fr": "Administrateur système - Accès administratif complet à tous les mandats"
- },
+ description=coerce_text_multilingual("System-Administrator - Vollständiger administrativer Zugriff über alle Mandanten"),
mandateId=mandateId,
featureInstanceId=None,
featureCode=None,
diff --git a/modules/interfaces/interfaceFeatures.py b/modules/interfaces/interfaceFeatures.py
index 6616218d..ba0f0428 100644
--- a/modules/interfaces/interfaceFeatures.py
+++ b/modules/interfaces/interfaceFeatures.py
@@ -15,6 +15,7 @@ from typing import List, Dict, Any, Optional
from modules.datamodels.datamodelFeatures import Feature, FeatureInstance
from modules.datamodels.datamodelRbac import Role, AccessRule
+from modules.datamodels.datamodelUtils import coerce_text_multilingual
from modules.connectors.connectorDbPostgre import DatabaseConnector
logger = logging.getLogger(__name__)
@@ -198,6 +199,9 @@ class FeatureInterface:
# Copy template roles if requested
if copyTemplateRoles:
self._copyTemplateRoles(featureCode, mandateId, instanceId)
+
+ # Copy template workflows (if feature defines TEMPLATE_WORKFLOWS)
+ self._copyTemplateWorkflows(featureCode, mandateId, instanceId)
cleanedRecord = dict(createdInstance)
return FeatureInstance(**cleanedRecord)
@@ -206,6 +210,72 @@ class FeatureInterface:
logger.error(f"Error creating feature instance: {e}")
raise ValueError(f"Failed to create feature instance: {e}")
+ def _copyTemplateWorkflows(self, featureCode: str, mandateId: str, instanceId: str) -> int:
+ """
+ Copy feature-specific template workflows to a new instance.
+
+ Loads TEMPLATE_WORKFLOWS from the feature module and creates
+ AutoWorkflow records in the graphicalEditor DB, scoped to
+ (mandateId, instanceId). The placeholder {{featureInstanceId}}
+ in graph parameters is replaced with the actual instanceId.
+
+ Args:
+ featureCode: Feature code (e.g. "trustee")
+ mandateId: Mandate ID
+ instanceId: New FeatureInstance ID
+
+ Returns:
+ Number of workflows copied
+ """
+ import json
+ import importlib
+
+ try:
+ featureModule = importlib.import_module(f"modules.features.{featureCode}.main{featureCode.capitalize()}")
+ getTemplateWorkflows = getattr(featureModule, "getTemplateWorkflows", None)
+ if not getTemplateWorkflows:
+ return 0
+
+ templateWorkflows = getTemplateWorkflows()
+ if not templateWorkflows:
+ return 0
+
+ from modules.features.graphicalEditor.interfaceFeatureGraphicalEditor import getGraphicalEditorInterface
+ from modules.auth.authModels import SystemUser
+ systemUser = SystemUser()
+ geInterface = getGraphicalEditorInterface(systemUser, mandateId, instanceId)
+
+ copied = 0
+ for template in templateWorkflows:
+ graphJson = json.dumps(template.get("graph", {}))
+ graphJson = graphJson.replace("{{featureInstanceId}}", instanceId)
+ graph = json.loads(graphJson)
+
+ labelDict = template.get("label", {})
+ label = labelDict.get("de") or labelDict.get("en") or str(labelDict) if isinstance(labelDict, dict) else str(labelDict)
+
+ geInterface.createWorkflow({
+ "label": label,
+ "graph": graph,
+ "tags": template.get("tags", [f"feature:{featureCode}"]),
+ "isTemplate": False,
+ "templateSourceId": template["id"],
+ "templateScope": "instance",
+ "active": True,
+ })
+ copied += 1
+
+ if copied > 0:
+ logger.info(f"Feature '{featureCode}': Copied {copied} template workflows to instance {instanceId}")
+ return copied
+
+ except ImportError:
+ logger.debug(f"No feature module found for '{featureCode}' — skipping workflow bootstrap")
+ return 0
+ except Exception as e:
+ logger.warning(f"Error copying template workflows for '{featureCode}' instance {instanceId}: {e}")
+ return 0
+
def _copyTemplateRoles(self, featureCode: str, mandateId: str, instanceId: str) -> int:
"""
Copy feature-specific template roles to a new instance.
@@ -268,7 +338,7 @@ class FeatureInterface:
newRole = Role(
id=newRoleId,
roleLabel=templateRole.get("roleLabel"),
- description=templateRole.get("description", {}),
+ description=coerce_text_multilingual(templateRole.get("description", {})),
featureCode=featureCode,
mandateId=mandateId,
featureInstanceId=instanceId,
@@ -354,7 +424,7 @@ class FeatureInterface:
newRole = Role(
id=newRoleId,
roleLabel=templateRole.get("roleLabel"),
- description=templateRole.get("description", {}),
+ description=coerce_text_multilingual(templateRole.get("description", {})),
featureCode=featureCode,
mandateId=mandateId,
featureInstanceId=featureInstanceId,
diff --git a/modules/routes/routeAdmin.py b/modules/routes/routeAdmin.py
index ed5bf42c..0f671f0a 100644
--- a/modules/routes/routeAdmin.py
+++ b/modules/routes/routeAdmin.py
@@ -13,6 +13,8 @@ from modules.shared.configuration import APP_CONFIG
from modules.auth import limiter, getCurrentUser
from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceDbApp import getRootInterface
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeAdmin")
# Static folder setup - using absolute path from app root
baseDir = FilePath(__file__).parent.parent.parent # Go up to gateway root
@@ -39,7 +41,7 @@ def root(request: Request) -> Dict[str, str]:
allowedOrigins = APP_CONFIG.get("APP_ALLOWED_ORIGINS")
if not allowedOrigins:
raise HTTPException(
- status_code=500, detail="APP_ALLOWED_ORIGINS configuration is required"
+ status_code=500, detail=routeApiMsg("APP_ALLOWED_ORIGINS configuration is required")
)
return {
@@ -59,17 +61,17 @@ def get_environment(
apiBaseUrl = APP_CONFIG.get("APP_API_URL")
if not apiBaseUrl:
raise HTTPException(
- status_code=500, detail="APP_API_URL configuration is required"
+ status_code=500, detail=routeApiMsg("APP_API_URL configuration is required")
)
environment = APP_CONFIG.get("APP_ENV")
if not environment:
- raise HTTPException(status_code=500, detail="APP_ENV configuration is required")
+ raise HTTPException(status_code=500, detail=routeApiMsg("APP_ENV configuration is required"))
instanceLabel = APP_CONFIG.get("APP_ENV_LABEL")
if not instanceLabel:
raise HTTPException(
- status_code=500, detail="APP_ENV_LABEL configuration is required"
+ status_code=500, detail=routeApiMsg("APP_ENV_LABEL configuration is required")
)
return {
@@ -91,5 +93,5 @@ def options_route(request: Request, fullPath: str) -> Response:
def favicon(request: Request) -> FileResponse:
favicon_path = staticFolder / "favicon.ico"
if not favicon_path.exists():
- raise HTTPException(status_code=404, detail="Favicon not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Favicon not found"))
return FileResponse(str(favicon_path), media_type="image/x-icon")
diff --git a/modules/routes/routeAdminFeatures.py b/modules/routes/routeAdminFeatures.py
index d7f6b7a9..855a0f80 100644
--- a/modules/routes/routeAdminFeatures.py
+++ b/modules/routes/routeAdminFeatures.py
@@ -27,6 +27,8 @@ from modules.interfaces.interfaceDbApp import getRootInterface
from modules.interfaces.interfaceFeatures import getFeatureInterface
from modules.security.rbacCatalog import getCatalogService
from modules.routes.routeNotifications import create_access_change_notification
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeAdminFeatures")
logger = logging.getLogger(__name__)
@@ -418,7 +420,7 @@ def list_feature_instances(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required"
+ detail=routeApiMsg("X-Mandate-Id header is required")
)
try:
@@ -483,7 +485,7 @@ def get_feature_instance_filter_values(
) -> list:
"""Return distinct filter values for a column in feature instances."""
if not context.mandateId:
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="X-Mandate-Id header is required")
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("X-Mandate-Id header is required"))
try:
from modules.routes.routeDataUsers import _handleFilterValuesRequest
rootInterface = getRootInterface()
@@ -530,7 +532,7 @@ def get_feature_instance(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
return instance.model_dump()
@@ -563,14 +565,14 @@ def create_feature_instance(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required"
+ detail=routeApiMsg("X-Mandate-Id header is required")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to create feature instances"
+ detail=routeApiMsg("Mandate-Admin role required to create feature instances")
)
try:
@@ -670,14 +672,14 @@ def delete_feature_instance(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to delete feature instances"
+ detail=routeApiMsg("Mandate-Admin role required to delete feature instances")
)
featureInterface.deleteFeatureInstance(instanceId)
@@ -737,14 +739,14 @@ def updateFeatureInstance(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to update feature instances"
+ detail=routeApiMsg("Mandate-Admin role required to update feature instances")
)
# Build update data (only non-None values)
@@ -763,7 +765,7 @@ def updateFeatureInstance(
if not updated:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to update feature instance"
+ detail=routeApiMsg("Failed to update feature instance")
)
# Clear chatbot config cache when config was updated for chatbot instances
@@ -820,14 +822,14 @@ def sync_instance_roles(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Check admin permission (Mandate-Admin or Feature-Admin)
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to sync roles"
+ detail=routeApiMsg("Admin role required to sync roles")
)
result = featureInterface.syncRolesFromTemplate(instanceId, addOnly)
@@ -1061,7 +1063,7 @@ def list_feature_instance_users(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Get all FeatureAccess records for this instance (Pydantic models)
@@ -1152,7 +1154,7 @@ def get_feature_instance_users_filter_values(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Feature instance '{instanceId}' not found")
if context.mandateId and str(instance.mandateId) != str(context.mandateId):
if not context.hasSysAdminRole:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Access denied to this feature instance")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Access denied to this feature instance"))
featureAccesses = rootInterface.getFeatureAccessesByInstance(instanceId)
result = []
for fa in featureAccesses:
@@ -1217,14 +1219,14 @@ def add_user_to_feature_instance(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Check admin permission
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to add users to feature instances"
+ detail=routeApiMsg("Admin role required to add users to feature instances")
)
# Verify user exists
@@ -1238,7 +1240,7 @@ def add_user_to_feature_instance(
if not data.roleIds:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="At least one role is required to grant feature access"
+ detail=routeApiMsg("At least one role is required to grant feature access")
)
from modules.datamodels.datamodelRbac import Role
@@ -1325,14 +1327,14 @@ def remove_user_from_feature_instance(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Check admin permission
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to remove users from feature instances"
+ detail=routeApiMsg("Admin role required to remove users from feature instances")
)
# Find FeatureAccess record
@@ -1341,7 +1343,7 @@ def remove_user_from_feature_instance(
if not existingAccess:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="User does not have access to this feature instance"
+ detail=routeApiMsg("User does not have access to this feature instance")
)
featureAccessId = str(existingAccess.id)
@@ -1415,14 +1417,14 @@ def update_feature_instance_user_roles(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Check admin permission
if not _hasMandateAdminRole(context) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to update user roles"
+ detail=routeApiMsg("Admin role required to update user roles")
)
# Find FeatureAccess record
@@ -1431,7 +1433,7 @@ def update_feature_instance_user_roles(
if not existingAccess:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="User does not have access to this feature instance"
+ detail=routeApiMsg("User does not have access to this feature instance")
)
featureAccessId = str(existingAccess.id)
@@ -1523,7 +1525,7 @@ def get_feature_instance_available_roles(
if not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this feature instance"
+ detail=routeApiMsg("Access denied to this feature instance")
)
# Get roles for this instance using interface method
@@ -1619,7 +1621,7 @@ def _renameFeatureInstance(
instance = featureInterface.getFeatureInstance(instanceId)
if not instance:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feature instance not found")
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Feature instance not found"))
userId = str(context.user.id)
isInstanceAdmin = False
@@ -1637,11 +1639,11 @@ def _renameFeatureInstance(
break
if not isInstanceAdmin:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Instance admin role required to rename")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Instance admin role required to rename"))
updated = featureInterface.updateFeatureInstance(instanceId, {"label": data.label.strip()})
if not updated:
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to update instance")
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=routeApiMsg("Failed to update instance"))
return {"id": instanceId, "label": updated.label}
diff --git a/modules/routes/routeAdminRbacExport.py b/modules/routes/routeAdminRbacExport.py
index c499a147..c6e3671e 100644
--- a/modules/routes/routeAdminRbacExport.py
+++ b/modules/routes/routeAdminRbacExport.py
@@ -21,8 +21,11 @@ from pydantic import BaseModel, Field
from modules.auth import limiter, getRequestContext, RequestContext, requireSysAdminRole
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelRbac import Role, AccessRule
+from modules.datamodels.datamodelUtils import coerce_text_multilingual
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.shared.timeUtils import getUtcTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeAdminRbacExport")
logger = logging.getLogger(__name__)
@@ -165,7 +168,7 @@ async def import_global_rbac(
if "roles" not in data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Missing 'roles' field in import data"
+ detail=routeApiMsg("Missing 'roles' field in import data")
)
rootInterface = getRootInterface()
@@ -227,7 +230,7 @@ async def import_global_rbac(
# Create new role
newRole = Role(
roleLabel=roleLabel,
- description=roleData.get("description", {}),
+ description=coerce_text_multilingual(roleData.get("description", {})),
featureCode=featureCode,
mandateId=None,
featureInstanceId=None,
@@ -298,14 +301,14 @@ def export_mandate_rbac(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required"
+ detail=routeApiMsg("X-Mandate-Id header is required")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to export RBAC"
+ detail=routeApiMsg("Mandate-Admin role required to export RBAC")
)
try:
@@ -392,14 +395,14 @@ async def import_mandate_rbac(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required"
+ detail=routeApiMsg("X-Mandate-Id header is required")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to import RBAC"
+ detail=routeApiMsg("Mandate-Admin role required to import RBAC")
)
try:
@@ -417,7 +420,7 @@ async def import_mandate_rbac(
if "roles" not in data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Missing 'roles' field in import data"
+ detail=routeApiMsg("Missing 'roles' field in import data")
)
rootInterface = getRootInterface()
@@ -482,7 +485,7 @@ async def import_mandate_rbac(
# Create new role at mandate level
newRole = Role(
roleLabel=roleLabel,
- description=roleData.get("description", {}),
+ description=coerce_text_multilingual(roleData.get("description", {})),
featureCode=featureCode,
mandateId=str(context.mandateId),
featureInstanceId=None,
diff --git a/modules/routes/routeAdminRbacRules.py b/modules/routes/routeAdminRbacRules.py
index 16336fae..14caf29c 100644
--- a/modules/routes/routeAdminRbacRules.py
+++ b/modules/routes/routeAdminRbacRules.py
@@ -23,6 +23,8 @@ from modules.datamodels.datamodelRbac import AccessRuleContext, AccessRule, Role
from modules.datamodels.datamodelMembership import UserMandate
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
from modules.interfaces.interfaceDbApp import getInterface, getRootInterface
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeAdminRbacRules")
# Configure logger
logger = logging.getLogger(__name__)
@@ -113,7 +115,7 @@ def get_permissions(
if not interface.rbac:
raise HTTPException(
status_code=500,
- detail="RBAC interface not available"
+ detail=routeApiMsg("RBAC interface not available")
)
# MULTI-TENANT: Get permissions using context (mandateId/featureInstanceId)
@@ -189,7 +191,7 @@ def get_all_permissions(
if not interface.rbac:
raise HTTPException(
status_code=500,
- detail="RBAC interface not available"
+ detail=routeApiMsg("RBAC interface not available")
)
# Determine which contexts to fetch
@@ -363,7 +365,7 @@ def get_access_rules(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# Get interface - uses root interface for admin access
interface = getRootInterface()
@@ -488,11 +490,11 @@ def get_access_rules_by_role(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# MandateAdmin: verify role belongs to their mandates
if not isSysAdmin and not _isRoleInAdminMandates(roleId, adminMandateIds):
- raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
interface = getRootInterface()
@@ -535,7 +537,7 @@ def get_access_rule(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# Get interface - uses root interface for admin access
interface = getRootInterface()
@@ -550,7 +552,7 @@ def get_access_rule(
# MandateAdmin: verify rule's role belongs to their mandates
if not isSysAdmin and not _isRoleInAdminMandates(str(rule.roleId), adminMandateIds):
- raise HTTPException(status_code=403, detail="Access denied: rule's role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: rule's role not in your mandates"))
# Convert to dict for JSON serialization
return rule.model_dump()
@@ -586,7 +588,7 @@ def create_access_rule(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# Get interface - uses root interface for admin access
interface = getRootInterface()
@@ -621,7 +623,7 @@ def create_access_rule(
# MandateAdmin: verify the rule's role belongs to their mandates
if not isSysAdmin and accessRule.roleId:
if not _isRoleInAdminMandates(str(accessRule.roleId), adminMandateIds):
- raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
# Create rule
createdRule = interface.createAccessRule(accessRule)
@@ -666,7 +668,7 @@ def update_access_rule(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# Get interface - uses root interface for admin access
interface = getRootInterface()
@@ -681,7 +683,7 @@ def update_access_rule(
# MandateAdmin: verify existing rule's role belongs to their mandates
if not isSysAdmin and not _isRoleInAdminMandates(str(existingRule.roleId), adminMandateIds):
- raise HTTPException(status_code=403, detail="Access denied: rule's role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: rule's role not in your mandates"))
# Validate and parse access rule data
try:
@@ -754,7 +756,7 @@ def delete_access_rule(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# Get interface - uses root interface for admin access
interface = getRootInterface()
@@ -769,7 +771,7 @@ def delete_access_rule(
# MandateAdmin: verify rule's role belongs to their mandates
if not isSysAdmin and not _isRoleInAdminMandates(str(existingRule.roleId), adminMandateIds):
- raise HTTPException(status_code=403, detail="Access denied: rule's role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: rule's role not in your mandates"))
# Delete rule
success = interface.deleteAccessRule(ruleId)
@@ -835,7 +837,7 @@ def list_roles(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
interface = getRootInterface()
@@ -1008,7 +1010,7 @@ def get_roles_filter_values(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
interface = getRootInterface()
dbRoles = interface.getAllRoles(pagination=None)
@@ -1083,12 +1085,12 @@ def create_role(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
# MandateAdmin: can only create roles in their own mandates
if not isSysAdmin:
if not role.mandateId or str(role.mandateId) not in adminMandateIds:
- raise HTTPException(status_code=403, detail="Access denied: can only create roles in your own mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: can only create roles in your own mandates"))
interface = getRootInterface()
@@ -1142,7 +1144,7 @@ def get_role(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
interface = getRootInterface()
@@ -1156,7 +1158,7 @@ def get_role(
# MandateAdmin: verify role belongs to their mandates
if not isSysAdmin:
if not role.mandateId or str(role.mandateId) not in adminMandateIds:
- raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
return {
"id": role.id,
@@ -1203,7 +1205,7 @@ def update_role(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
interface = getRootInterface()
@@ -1213,9 +1215,9 @@ def update_role(
if not existingRole:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
if existingRole.isSystemRole and not existingRole.mandateId:
- raise HTTPException(status_code=403, detail="Access denied: cannot modify template/system roles")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: cannot modify template/system roles"))
if not existingRole.mandateId or str(existingRole.mandateId) not in adminMandateIds:
- raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
updatedRole = interface.updateRole(roleId, role)
@@ -1267,7 +1269,7 @@ def delete_role(
isSysAdmin = reqContext.hasSysAdminRole
adminMandateIds = [] if isSysAdmin else _getAdminMandateIds(reqContext)
if not isSysAdmin and not adminMandateIds:
- raise HTTPException(status_code=403, detail="Admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Admin role required"))
interface = getRootInterface()
@@ -1277,9 +1279,9 @@ def delete_role(
if not existingRole:
raise HTTPException(status_code=404, detail=f"Role {roleId} not found")
if existingRole.isSystemRole and not existingRole.mandateId:
- raise HTTPException(status_code=403, detail="Access denied: cannot delete template/system roles")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: cannot delete template/system roles"))
if not existingRole.mandateId or str(existingRole.mandateId) not in adminMandateIds:
- raise HTTPException(status_code=403, detail="Access denied: role not in your mandates")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied: role not in your mandates"))
success = interface.deleteRole(roleId)
if not success:
diff --git a/modules/routes/routeAdminUserAccessOverview.py b/modules/routes/routeAdminUserAccessOverview.py
index 9b19fc41..6c122191 100644
--- a/modules/routes/routeAdminUserAccessOverview.py
+++ b/modules/routes/routeAdminUserAccessOverview.py
@@ -24,6 +24,8 @@ from modules.datamodels.datamodelMembership import (
)
from modules.datamodels.datamodelFeatures import FeatureInstance, Feature
from modules.interfaces.interfaceDbApp import getRootInterface
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeAdminUserAccessOverview")
# Configure logger
logger = logging.getLogger(__name__)
@@ -116,7 +118,7 @@ def listUsersForOverview(
- List of user dictionaries with basic info
"""
if not _hasMandateAdminRole(context):
- raise HTTPException(status_code=403, detail="Keine Berechtigung für die Benutzerzugriffsübersicht")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Keine Berechtigung für die Benutzerzugriffsübersicht"))
try:
interface = getRootInterface()
@@ -209,7 +211,7 @@ def getUserAccessOverview(
- Resource access (what resources the user can use)
"""
if not _hasMandateAdminRole(context):
- raise HTTPException(status_code=403, detail="Keine Berechtigung für die Benutzerzugriffsübersicht")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Keine Berechtigung für die Benutzerzugriffsübersicht"))
try:
interface = getRootInterface()
@@ -239,7 +241,7 @@ def getUserAccessOverview(
break
if not userInAdminMandate:
- raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Benutzer gehört nicht zu Ihrem Mandate"))
# Get user
user = interface.getUser(userId)
@@ -528,7 +530,7 @@ def getEffectivePermissions(
if not context.hasSysAdminRole:
# Check if user has admin role in any mandate
if not _hasMandateAdminRole(context):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required"))
try:
interface = getRootInterface()
@@ -550,7 +552,7 @@ def getEffectivePermissions(
break
if not adminMandateIds:
- raise HTTPException(status_code=403, detail="Insufficient permissions")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Insufficient permissions"))
userInAdminMandate = False
for mid in adminMandateIds:
@@ -559,7 +561,7 @@ def getEffectivePermissions(
break
if not userInAdminMandate:
- raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Benutzer gehört nicht zu Ihrem Mandate"))
# Get user
user = interface.getUser(userId)
diff --git a/modules/routes/routeAttributes.py b/modules/routes/routeAttributes.py
index e877e512..20ddb842 100644
--- a/modules/routes/routeAttributes.py
+++ b/modules/routes/routeAttributes.py
@@ -9,6 +9,9 @@ from modules.auth import limiter
# Import the attribute definition and helper functions
from modules.shared.attributeUtils import getModelClasses, getModelAttributeDefinitions, AttributeResponse, AttributeDefinition
+from modules.shared.i18nRegistry import apiRouteContext
+
+routeApiMsg = apiRouteContext("routeAttributes")
# Configure logger
logger = logging.getLogger(__name__)
@@ -42,8 +45,8 @@ def get_entity_attributes(
# Check if entity type is known
if entityType not in modelClasses:
raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND,
- detail=f"Entity type '{entityType}' not found."
+ status_code=status.HTTP_404_NOT_FOUND,
+ detail=routeApiMsg("Entitätstyp nicht gefunden.") + f" ({entityType})",
)
# Get model class and derive attributes from it
diff --git a/modules/routes/routeBilling.py b/modules/routes/routeBilling.py
index 110f563c..944131d6 100644
--- a/modules/routes/routeBilling.py
+++ b/modules/routes/routeBilling.py
@@ -38,6 +38,9 @@ from modules.datamodels.datamodelBilling import (
BillingStatisticsChartData,
BillingCheckResult,
)
+from modules.shared.i18nRegistry import apiRouteContext
+
+routeApiMsg = apiRouteContext("routeBilling")
# Configure logger
logger = logging.getLogger(__name__)
@@ -337,9 +340,9 @@ def _creditStripeSessionIfNeeded(
amount_chf_str = metadata.get("amountChf", "0")
if not session_id:
- raise HTTPException(status_code=400, detail="Stripe session id missing")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Stripe session id missing"))
if not mandate_id:
- raise HTTPException(status_code=400, detail="Invalid session metadata: mandateId missing")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid session metadata: mandateId missing"))
existing_payment_tx = billingInterface.getPaymentTransactionByReferenceId(session_id)
if existing_payment_tx:
@@ -363,11 +366,11 @@ def _creditStripeSessionIfNeeded(
if amount_total is not None:
amount_chf = amount_total / 100.0
else:
- raise HTTPException(status_code=400, detail="Invalid amount in Stripe session")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid amount in Stripe session"))
settings = billingInterface.getSettings(mandate_id)
if not settings:
- raise HTTPException(status_code=404, detail="Billing settings not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found"))
account = billingInterface.getOrCreateMandateAccount(mandate_id, initialBalance=0.0)
@@ -537,10 +540,10 @@ def getStatistics(
try:
# Validate period
if period not in ["day", "month", "year"]:
- raise HTTPException(status_code=400, detail="Invalid period. Use 'day', 'month', or 'year'")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid period. Use 'day', 'month', or 'year'"))
if period == "day" and not month:
- raise HTTPException(status_code=400, detail="Month is required for 'day' period")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Month is required for 'day' period"))
billingInterface = getBillingInterface(ctx.user, ctx.mandateId)
settings = billingInterface.getSettings(ctx.mandateId)
@@ -642,13 +645,13 @@ def getSettingsAdmin(
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
"""
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
try:
billingInterface = getBillingInterface(ctx.user, targetMandateId)
settings = billingInterface.getSettings(targetMandateId)
if not settings:
- raise HTTPException(status_code=404, detail="Billing settings not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found"))
return settings
@@ -672,7 +675,7 @@ def createOrUpdateSettings(
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
"""
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
try:
billingInterface = getBillingInterface(ctx.user, targetMandateId)
existingSettings = billingInterface.getSettings(targetMandateId)
@@ -742,12 +745,12 @@ def addCredit(
settings = billingInterface.getSettings(targetMandateId)
if not settings:
- raise HTTPException(status_code=404, detail="Billing settings not found for this mandate")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found for this mandate"))
account = billingInterface.getOrCreateMandateAccount(targetMandateId, initialBalance=0.0)
if creditRequest.amount == 0:
- raise HTTPException(status_code=400, detail="Amount must not be zero")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Amount must not be zero"))
from modules.datamodels.datamodelBilling import BillingTransaction
@@ -794,10 +797,10 @@ def createCheckoutSession(
settings = billingInterface.getSettings(targetMandateId)
if not settings:
- raise HTTPException(status_code=404, detail="Billing settings not found for this mandate")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found for this mandate"))
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=403, detail="Mandate admin role required to load mandate credit")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Mandate admin role required to load mandate credit"))
from modules.serviceCenter.services.serviceBilling.stripeCheckout import create_checkout_session
redirect_url = create_checkout_session(
@@ -832,7 +835,7 @@ def confirmCheckoutSession(
stripe = _getStripeClient()
session = stripe.checkout.Session.retrieve(confirmRequest.sessionId)
if not session:
- raise HTTPException(status_code=404, detail="Stripe Checkout Session not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Stripe Checkout Session not found"))
from modules.shared.stripeClient import stripeToDict
session_dict = stripeToDict(session)
@@ -841,7 +844,7 @@ def confirmCheckoutSession(
user_id = metadata.get("userId") or None
if not mandate_id:
- raise HTTPException(status_code=400, detail="Invalid session metadata: mandateId missing")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid session metadata: mandateId missing"))
payment_status = session_dict.get("payment_status")
if payment_status != "paid":
@@ -850,10 +853,10 @@ def confirmCheckoutSession(
billingInterface = getBillingInterface(ctx.user, mandate_id)
settings = billingInterface.getSettings(mandate_id)
if not settings:
- raise HTTPException(status_code=404, detail="Billing settings not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Billing settings not found"))
if not _isAdminOfMandate(ctx, mandate_id):
- raise HTTPException(status_code=403, detail="Mandate admin role required")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Mandate admin role required"))
root_billing_interface = _getRootInterface()
return _creditStripeSessionIfNeeded(root_billing_interface, session_dict, eventId=None)
@@ -880,10 +883,10 @@ async def stripeWebhook(
webhook_secret = APP_CONFIG.get("STRIPE_WEBHOOK_SECRET")
if not webhook_secret:
logger.error("STRIPE_WEBHOOK_SECRET not configured")
- raise HTTPException(status_code=500, detail="Webhook not configured")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Webhook not configured"))
if not stripe_signature:
- raise HTTPException(status_code=400, detail="Missing Stripe-Signature header")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Missing Stripe-Signature header"))
payload = await request.body()
@@ -894,10 +897,10 @@ async def stripeWebhook(
)
except ValueError as e:
logger.warning(f"Stripe webhook invalid payload: {e}")
- raise HTTPException(status_code=400, detail="Invalid payload")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid payload"))
except Exception as e:
logger.warning(f"Stripe webhook signature verification failed: {e}")
- raise HTTPException(status_code=400, detail="Invalid signature")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid signature"))
logger.info(f"Stripe webhook received: event={event.id}, type={event.type}")
@@ -1243,7 +1246,7 @@ def getAccounts(
Access: SysAdmin (any mandate) or MandateAdmin (own mandate).
"""
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
try:
billingInterface = getBillingInterface(ctx.user, targetMandateId)
@@ -1291,7 +1294,7 @@ def getUsersForMandate(
Used by billing admin to select users for credit assignment.
"""
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
try:
from modules.interfaces.interfaceDbApp import getInterface as getAppInterface
@@ -1414,7 +1417,7 @@ def getTransactionsAdmin(
):
"""Get all transactions for a mandate with pagination support."""
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
try:
paginationParams: Optional[PaginationParams] = None
if pagination:
@@ -1461,7 +1464,7 @@ def getTransactionFilterValues(
):
"""Return distinct filter values for a column in mandate transactions."""
if not _isAdminOfMandate(ctx, targetMandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required for this mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required for this mandate"))
try:
crossFilterParams: Optional[PaginationParams] = None
if pagination:
diff --git a/modules/routes/routeClickup.py b/modules/routes/routeClickup.py
index 1603fa23..07202791 100644
--- a/modules/routes/routeClickup.py
+++ b/modules/routes/routeClickup.py
@@ -12,6 +12,8 @@ from modules.auth import getCurrentUser, limiter
from modules.datamodels.datamodelUam import AuthAuthority, User, UserConnection
from modules.interfaces.interfaceDbApp import getInterface
from modules.serviceHub import getInterface as getServices
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeClickup")
logger = logging.getLogger(__name__)
@@ -42,12 +44,12 @@ def _getUserConnection(interface, connection_id: str, user_id: str) -> Optional[
def _clickup_connection_or_404(interface, connection_id: str, user_id: str) -> UserConnection:
connection = _getUserConnection(interface, connection_id, user_id)
if not connection:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Connection not found")
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Connection not found"))
authority = connection.authority.value if hasattr(connection.authority, "value") else str(connection.authority)
if authority.lower() != AuthAuthority.CLICKUP.value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Connection is not a ClickUp connection",
+ detail=routeApiMsg("Connection is not a ClickUp connection"),
)
return connection
@@ -57,7 +59,7 @@ def _svc_for_connection(current_user: User, connection: UserConnection):
if not services.clickup.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Failed to set ClickUp access token",
+ detail=routeApiMsg("Failed to set ClickUp access token"),
)
return services.clickup
diff --git a/modules/routes/routeDataConnections.py b/modules/routes/routeDataConnections.py
index d01992c5..5e7b2c7e 100644
--- a/modules/routes/routeDataConnections.py
+++ b/modules/routes/routeDataConnections.py
@@ -26,6 +26,8 @@ from modules.datamodels.datamodelPagination import PaginationParams, PaginatedRe
from modules.interfaces.interfaceDbApp import getInterface
from modules.shared.timeUtils import getUtcTimestamp, parseTimestamp
from modules.interfaces.interfaceDbManagement import ComponentObjects
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeDataConnections")
# Configure logger
logger = logging.getLogger(__name__)
@@ -414,7 +416,7 @@ def update_connection(
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Connection not found"
+ detail=routeApiMsg("Connection not found")
)
# Update connection fields
@@ -486,7 +488,7 @@ def connect_service(
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Connection not found"
+ detail=routeApiMsg("Connection not found")
)
# Data-app OAuth (JWT state issued server-side in /auth/connect)
@@ -542,7 +544,7 @@ def disconnect_service(
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Connection not found"
+ detail=routeApiMsg("Connection not found")
)
# Update connection status
@@ -592,7 +594,7 @@ def delete_connection(
if not connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Connection not found"
+ detail=routeApiMsg("Connection not found")
)
# Remove the connection - only need connectionId since permissions are verified
diff --git a/modules/routes/routeDataFiles.py b/modules/routes/routeDataFiles.py
index 17e0ef56..defc0d75 100644
--- a/modules/routes/routeDataFiles.py
+++ b/modules/routes/routeDataFiles.py
@@ -17,6 +17,8 @@ from modules.datamodels.datamodelFileFolder import FileFolder
from modules.shared.attributeUtils import getModelAttributeDefinitions
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeDataFiles")
# Configure logger
logger = logging.getLogger(__name__)
@@ -422,7 +424,7 @@ def create_folder(
name = body.get("name", "")
parentId = body.get("parentId")
if not name:
- raise HTTPException(status_code=400, detail="name is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("name is required"))
try:
mgmt = interfaceDbManagement.getInterface(
currentUser,
@@ -449,7 +451,7 @@ def rename_folder(
"""Rename a folder."""
newName = body.get("name", "")
if not newName:
- raise HTTPException(status_code=400, detail="name is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("name is required"))
try:
mgmt = interfaceDbManagement.getInterface(
currentUser,
@@ -554,7 +556,7 @@ def download_folder(
fileEntries = _collectFiles(folderId, "")
if not fileEntries:
- raise HTTPException(status_code=404, detail="Folder is empty")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Folder is empty"))
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
@@ -595,7 +597,7 @@ def batch_delete_items(
recursiveFolders = bool(body.get("recursiveFolders", True))
if not isinstance(fileIds, list) or not isinstance(folderIds, list):
- raise HTTPException(status_code=400, detail="fileIds and folderIds must be arrays")
+ raise HTTPException(status_code=400, detail=routeApiMsg("fileIds and folderIds must be arrays"))
try:
mgmt = interfaceDbManagement.getInterface(
@@ -638,7 +640,7 @@ def batch_move_items(
targetParentId = body.get("targetParentId")
if not isinstance(fileIds, list) or not isinstance(folderIds, list):
- raise HTTPException(status_code=400, detail="fileIds and folderIds must be arrays")
+ raise HTTPException(status_code=400, detail=routeApiMsg("fileIds and folderIds must be arrays"))
try:
mgmt = interfaceDbManagement.getInterface(
@@ -683,7 +685,7 @@ def updateFileScope(
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {validScopes}")
if scope == "global" and not context.hasSysAdminRole:
- raise HTTPException(status_code=403, detail="Only sysadmins can set global scope")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Only sysadmins can set global scope"))
managementInterface = interfaceDbManagement.getInterface(
context.user,
@@ -875,14 +877,14 @@ def update_file(
if file_info.get("scope") == "global" and not _hasSysAdminRole(str(currentUser.id)):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Only sysadmins can set global scope",
+ detail=routeApiMsg("Only sysadmins can set global scope"),
)
# Check if user has access to the file using RBAC
if not managementInterface.checkRbacPermission(FileItem, "update", fileId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Not authorized to update this file"
+ detail=routeApiMsg("Not authorized to update this file")
)
# Update the file
@@ -890,7 +892,7 @@ def update_file(
if not result:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to update file"
+ detail=routeApiMsg("Failed to update file")
)
# Get updated file
@@ -928,7 +930,7 @@ def delete_file(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error deleting the file"
+ detail=routeApiMsg("Error deleting the file")
)
return {"message": f"File with ID {fileId} successfully deleted"}
diff --git a/modules/routes/routeDataMandates.py b/modules/routes/routeDataMandates.py
index cb6a3efc..91b5b1b6 100644
--- a/modules/routes/routeDataMandates.py
+++ b/modules/routes/routeDataMandates.py
@@ -32,6 +32,8 @@ from modules.datamodels.datamodelRbac import Role
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
from modules.routes.routeNotifications import create_access_change_notification
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import SubscriptionCapacityException
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeDataMandates")
# =============================================================================
@@ -103,7 +105,7 @@ def get_mandates(
if not adminMandateIds:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required"
+ detail=routeApiMsg("Admin role required")
)
# Parse pagination parameter
@@ -180,7 +182,7 @@ def get_mandate_filter_values(
if not isSysAdmin:
adminMandateIds = _getAdminMandateIds(context)
if not adminMandateIds:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Admin role required"))
appInterface = interfaceDbApp.getRootInterface()
@@ -248,7 +250,7 @@ def get_mandate(
if mandateId not in adminMandateIds:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required for this mandate"
+ detail=routeApiMsg("Admin role required for this mandate")
)
appInterface = interfaceDbApp.getRootInterface()
@@ -289,7 +291,7 @@ def create_mandate(
if not name or (isinstance(name, str) and name.strip() == ''):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Mandate name is required"
+ detail=routeApiMsg("Mandate name is required")
)
# Get optional fields with defaults
@@ -308,7 +310,7 @@ def create_mandate(
if not newMandate:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to create mandate"
+ detail=routeApiMsg("Failed to create mandate")
)
try:
@@ -392,7 +394,7 @@ def update_mandate(
if not updatedMandate:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to update mandate"
+ detail=routeApiMsg("Failed to update mandate")
)
logger.info(f"Mandate {mandateId} updated by SysAdmin {currentUser.id}")
@@ -438,7 +440,7 @@ def delete_mandate(
if confirmName != mandateName:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Hard-delete requires X-Confirm-Name header matching the mandate name"
+ detail=routeApiMsg("Hard-delete requires X-Confirm-Name header matching the mandate name")
)
try:
@@ -487,7 +489,7 @@ def list_mandate_users(
if not _hasMandateAdminRole(context, targetMandateId) and not context.hasSysAdminRole:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required"
+ detail=routeApiMsg("Mandate-Admin role required")
)
try:
@@ -647,7 +649,7 @@ def get_mandate_users_filter_values(
) -> list:
"""Return distinct filter values for a column in mandate users."""
if not _hasMandateAdminRole(context, targetMandateId) and not context.hasSysAdminRole:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Mandate-Admin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Mandate-Admin role required"))
try:
from modules.routes.routeDataUsers import _handleFilterValuesRequest
@@ -714,7 +716,7 @@ def add_user_to_mandate(
if not _hasMandateAdminRole(context, targetMandateId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to add users"
+ detail=routeApiMsg("Mandate-Admin role required to add users")
)
try:
@@ -831,7 +833,7 @@ def remove_user_from_mandate(
if not _hasMandateAdminRole(context, targetMandateId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required"
+ detail=routeApiMsg("Mandate-Admin role required")
)
try:
@@ -857,7 +859,7 @@ def remove_user_from_mandate(
if _isLastMandateAdmin(rootInterface, targetMandateId, targetUserId):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Cannot remove the last admin from a mandate. Assign another admin first."
+ detail=routeApiMsg("Cannot remove the last admin from a mandate. Assign another admin first.")
)
# Delete UserMandate (CASCADE will delete UserMandateRole entries)
@@ -920,7 +922,7 @@ def update_user_roles_in_mandate(
if not _hasMandateAdminRole(context, targetMandateId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required"
+ detail=routeApiMsg("Mandate-Admin role required")
)
try:
@@ -953,7 +955,7 @@ def update_user_roles_in_mandate(
if _isLastMandateAdmin(rootInterface, targetMandateId, targetUserId):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Cannot remove admin role from the last admin. Assign another admin first."
+ detail=routeApiMsg("Cannot remove admin role from the last admin. Assign another admin first.")
)
# Remove existing role assignments
diff --git a/modules/routes/routeDataPrompts.py b/modules/routes/routeDataPrompts.py
index f9246ab6..2644b7e3 100644
--- a/modules/routes/routeDataPrompts.py
+++ b/modules/routes/routeDataPrompts.py
@@ -14,6 +14,8 @@ import modules.interfaces.interfaceDbManagement as interfaceDbManagement
from modules.datamodels.datamodelUtils import Prompt
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeDataPrompts")
# Configure logger
logger = logging.getLogger(__name__)
@@ -173,7 +175,7 @@ def update_prompt(
if not updatedPrompt:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error updating the prompt"
+ detail=routeApiMsg("Error updating the prompt")
)
return Prompt(**updatedPrompt)
@@ -207,7 +209,7 @@ def delete_prompt(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error deleting the prompt"
+ detail=routeApiMsg("Error deleting the prompt")
)
return {"message": f"Prompt with ID {promptId} successfully deleted"}
\ No newline at end of file
diff --git a/modules/routes/routeDataSources.py b/modules/routes/routeDataSources.py
index e210d094..db4b9a4f 100644
--- a/modules/routes/routeDataSources.py
+++ b/modules/routes/routeDataSources.py
@@ -10,6 +10,8 @@ from modules.auth import limiter, getRequestContext, RequestContext
from modules.auth.authentication import _hasSysAdminRole
from modules.datamodels.datamodelDataSource import DataSource
from modules.datamodels.datamodelFeatureDataSource import FeatureDataSource
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeDataSources")
logger = logging.getLogger(__name__)
@@ -52,7 +54,7 @@ def _updateDataSourceScope(
raise HTTPException(status_code=400, detail=f"Invalid scope: {scope}. Must be one of {_VALID_SCOPES}")
if scope == "global" and not _hasSysAdminRole(context.user):
- raise HTTPException(status_code=403, detail="Only sysadmins can set global scope")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Only sysadmins can set global scope"))
try:
from modules.interfaces.interfaceDbApp import getRootInterface
diff --git a/modules/routes/routeDataUsers.py b/modules/routes/routeDataUsers.py
index 23cd508f..42e65c70 100644
--- a/modules/routes/routeDataUsers.py
+++ b/modules/routes/routeDataUsers.py
@@ -24,6 +24,8 @@ from modules.auth import limiter, getRequestContext, RequestContext
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeDataUsers")
# Configure logger
logger = logging.getLogger(__name__)
@@ -297,7 +299,7 @@ def get_user_options(
elif context.hasSysAdminRole:
users = appInterface.getAllUsers()
else:
- raise HTTPException(status_code=403, detail="Access denied")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
return [
{"value": user.id, "label": user.fullName or user.username or user.email or user.id}
@@ -420,7 +422,7 @@ def get_users(
if not adminMandateIds:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="No admin access to any mandate"
+ detail=routeApiMsg("No admin access to any mandate")
)
from modules.datamodels.datamodelMembership import UserMandate as UserMandateModel
@@ -581,7 +583,7 @@ def get_user(
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="User not in your mandate"
+ detail=routeApiMsg("User not in your mandate")
)
return user
@@ -636,7 +638,7 @@ def create_user(
if not userRole:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="No 'user' role found in system — cannot assign user to mandate"
+ detail=routeApiMsg("No 'user' role found in system — cannot assign user to mandate")
)
appInterface.createUserMandate(
@@ -667,7 +669,7 @@ def update_user(
if not isSelfUpdate and not _isAdminForUser(context, userId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to update other users"
+ detail=routeApiMsg("Admin role required to update other users")
)
# Use rootInterface for user lookup/update (avoids RBAC filtering on User table)
@@ -687,7 +689,7 @@ def update_user(
if not updatedUser:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error updating the user"
+ detail=routeApiMsg("Error updating the user")
)
return updatedUser
@@ -709,7 +711,7 @@ def reset_user_password(
if not _isAdminForUser(context, userId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to reset passwords"
+ detail=routeApiMsg("Admin role required to reset passwords")
)
# Get user interface
@@ -719,7 +721,7 @@ def reset_user_password(
if len(newPassword) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Password must be at least 8 characters long"
+ detail=routeApiMsg("Password must be at least 8 characters long")
)
# Reset password
@@ -727,7 +729,7 @@ def reset_user_password(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to reset password"
+ detail=routeApiMsg("Failed to reset password")
)
# SECURITY: Automatically revoke all tokens for the user after password reset
@@ -792,14 +794,14 @@ def change_password(
if not appInterface.verifyPassword(currentPassword, context.user.passwordHash):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Current password is incorrect"
+ detail=routeApiMsg("Current password is incorrect")
)
# Validate new password strength
if len(newPassword) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="New password must be at least 8 characters long"
+ detail=routeApiMsg("New password must be at least 8 characters long")
)
# Change password
@@ -807,7 +809,7 @@ def change_password(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to change password"
+ detail=routeApiMsg("Failed to change password")
)
# SECURITY: Automatically revoke all tokens for the user after password change
@@ -877,7 +879,7 @@ def send_password_link(
if not _isAdminForUser(context, userId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin role required to send password links"
+ detail=routeApiMsg("Admin role required to send password links")
)
# Get user interface
@@ -888,14 +890,14 @@ def send_password_link(
if not targetUser:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="User not found"
+ detail=routeApiMsg("User not found")
)
# Check if user has an email
if not targetUser.email:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="User has no email address configured"
+ detail=routeApiMsg("User has no email address configured")
)
# Use root interface for token operations
@@ -942,7 +944,7 @@ def send_password_link(
logger.warning(f"Failed to send password setup email to {targetUser.email}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to send email"
+ detail=routeApiMsg("Failed to send email")
)
except HTTPException:
@@ -1010,7 +1012,7 @@ def delete_user(
if not userMandate:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Cannot delete user outside your mandate"
+ detail=routeApiMsg("Cannot delete user outside your mandate")
)
# Delete UserMandate entries for this user first
@@ -1022,7 +1024,7 @@ def delete_user(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error deleting the user"
+ detail=routeApiMsg("Error deleting the user")
)
return {"message": f"User with ID {userId} successfully deleted"}
diff --git a/modules/routes/routeGdpr.py b/modules/routes/routeGdpr.py
index f923932e..fce8ab69 100644
--- a/modules/routes/routeGdpr.py
+++ b/modules/routes/routeGdpr.py
@@ -25,6 +25,8 @@ from modules.interfaces.interfaceDbApp import getRootInterface
from modules.shared.timeUtils import getUtcTimestamp
from modules.shared.auditLogger import audit_logger
from modules.shared.gdprDeletion import deleteUserDataAcrossAllDatabases, buildDeletionSummary
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeGdpr")
logger = logging.getLogger(__name__)
@@ -316,14 +318,14 @@ def delete_account(
if not confirmDeletion:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Deletion not confirmed. Set confirmDeletion=true to proceed."
+ detail=routeApiMsg("Deletion not confirmed. Set confirmDeletion=true to proceed.")
)
# Prevent SysAdmin self-deletion (safety measure)
if getattr(currentUser, "isSysAdmin", False):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="SysAdmin accounts cannot be self-deleted. Contact another SysAdmin."
+ detail=routeApiMsg("SysAdmin accounts cannot be self-deleted. Contact another SysAdmin.")
)
try:
diff --git a/modules/routes/routeI18n.py b/modules/routes/routeI18n.py
index 31543f62..31813798 100644
--- a/modules/routes/routeI18n.py
+++ b/modules/routes/routeI18n.py
@@ -38,8 +38,11 @@ from modules.datamodels.datamodelNotification import NotificationType
from modules.interfaces.interfaceDbManagement import getInterface as getMgmtInterface
from modules.routes.routeNotifications import _createNotification
from modules.shared.configuration import APP_CONFIG
+from modules.shared.i18nRegistry import _loadCache as _reloadI18nCache, apiRouteContext
from modules.shared.timeUtils import getUtcTimestamp
+routeApiMsg = apiRouteContext("routeI18n")
+
logger = logging.getLogger(__name__)
router = APIRouter(
@@ -270,16 +273,28 @@ async def _translateBatch(
finally:
aiObjects.billingCallback = None
+ _matchCapitalization(keysToTranslate, result)
return result
+def _matchCapitalization(originals: Dict[str, str], translations: Dict[str, str]) -> None:
+ """Ensure translations preserve the capitalisation pattern of the original key."""
+ for key, translated in translations.items():
+ if not key or not translated:
+ continue
+ if key[0].isupper() and translated[0].islower():
+ translations[key] = translated[0].upper() + translated[1:]
+ elif key[0].islower() and translated[0].isupper():
+ translations[key] = translated[0].lower() + translated[1:]
+
+
def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
userId = str(currentUser.id)
memberIds = _userMemberMandateIds(currentUser)
if not memberIds:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Mindestens eine Mandats-Mitgliedschaft ist für die AI-Nutzung erforderlich.",
+ detail=routeApiMsg("Mindestens eine Mandats-Mitgliedschaft ist für die AI-Nutzung erforderlich."),
)
headerRaw = (
@@ -289,7 +304,7 @@ def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
if headerRaw not in memberIds:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="X-Mandate-Id ist kein Mandat Ihrer Mitgliedschaft.",
+ detail=routeApiMsg("X-Mandate-Id ist kein Mandat Ihrer Mitgliedschaft."),
)
if _mandatePassesAiPoolBilling(currentUser, headerRaw, userId):
return headerRaw
@@ -298,7 +313,7 @@ def _resolveMandateIdForAiI18n(request: Request, currentUser: User) -> str:
return mid
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
- detail="Nicht genügend AI-Guthaben (Mandats-Pool) für diese Aktion.",
+ detail=routeApiMsg("Nicht genügend AI-Guthaben (Mandats-Pool) für diese Aktion."),
)
@@ -348,7 +363,7 @@ async def _readOptionalEntriesFromBody(request: Request) -> Optional[List[dict]]
if not isinstance(entries, list):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Feld «entries» muss ein JSON-Array sein.",
+ detail=routeApiMsg("Feld «entries» muss ein JSON-Array sein."),
)
result = []
for e in entries:
@@ -363,11 +378,10 @@ async def _readOptionalEntriesFromBody(request: Request) -> Optional[List[dict]]
def _syncXxMaster(db, userId: Optional[str], incomingEntries: List[dict]) -> Dict[str, Any]:
- """Synchronise the xx base set with incoming entries (from build bundle or codebase scan).
+ """Synchronise the xx base set with incoming UI entries.
- - Keys in incoming but not in DB -> add
- - Keys in DB but not in incoming -> remove
- - Keys in both -> update context (value)
+ Only touches entries whose context is "ui". Gateway entries (api.*, table.*)
+ written by _syncRegistryToDb at boot are preserved untouched.
"""
if not incomingEntries:
logger.warning("i18n xx-sync: no entries — aborting")
@@ -394,39 +408,45 @@ def _syncXxMaster(db, userId: Optional[str], incomingEntries: List[dict]) -> Dic
row = dict(rows[0])
curEntries = _rowEntries(row)
- curByKey = {e["key"]: e for e in curEntries}
+
+ gatewayEntries = [e for e in curEntries if e.get("context", "ui") != "ui"]
+ curUiByKey = {e["key"]: e for e in curEntries if e.get("context", "ui") == "ui"}
incomingByKey = {e["key"]: e for e in incomingEntries}
incomingKeys = set(incomingByKey.keys())
- dbKeys = set(curByKey.keys())
+ dbUiKeys = set(curUiByKey.keys())
- added = sorted(incomingKeys - dbKeys)
- removed = sorted(dbKeys - incomingKeys)
+ added = sorted(incomingKeys - dbUiKeys)
+ removed = sorted(dbUiKeys - incomingKeys)
- newEntries = []
- for e in incomingEntries:
- newEntries.append({"context": e["context"], "key": e["key"], "value": e["value"]})
- for e in curEntries:
- if e["key"] not in incomingKeys:
- continue
+ newUiEntries = [
+ {"context": e["context"], "key": e["key"], "value": e["value"]}
+ for e in incomingEntries
+ ]
if not added and not removed and all(
- curByKey.get(e["key"], {}).get("value") == e["value"]
- and curByKey.get(e["key"], {}).get("context") == e["context"]
+ curUiByKey.get(e["key"], {}).get("value") == e["value"]
+ and curUiByKey.get(e["key"], {}).get("context") == e["context"]
for e in incomingEntries
):
- return {"added": [], "removed": [], "entriesCount": len(newEntries)}
+ total = len(newUiEntries) + len(gatewayEntries)
+ return {"added": [], "removed": [], "entriesCount": total}
+
+ mergedEntries = gatewayEntries + newUiEntries
now = getUtcTimestamp()
- row["entries"] = newEntries
+ row["entries"] = mergedEntries
if "keys" in row:
del row["keys"]
row["sysModifiedAt"] = now
row["sysModifiedBy"] = userId
db.recordModify(UiLanguageSet, "xx", row)
- logger.info("i18n xx-master sync: +%d added, -%d removed, total=%d", len(added), len(removed), len(newEntries))
- return {"added": added, "removed": removed, "entriesCount": len(newEntries)}
+ logger.info(
+ "i18n xx-master sync: +%d added, -%d removed (ui=%d, gateway=%d, total=%d)",
+ len(added), len(removed), len(newUiEntries), len(gatewayEntries), len(mergedEntries),
+ )
+ return {"added": added, "removed": removed, "entriesCount": len(mergedEntries)}
# --- Public -----------------------------------------------------------------
@@ -439,6 +459,8 @@ async def list_language_codes():
out = []
for r in rows:
entries = _rowEntries(r)
+ uiCount = sum(1 for e in entries if e.get("context", "ui") == "ui")
+ gatewayCount = len(entries) - uiCount
out.append(
{
"code": r["id"],
@@ -446,6 +468,8 @@ async def list_language_codes():
"status": r.get("status"),
"isDefault": bool(r.get("isDefault")),
"entriesCount": len(entries),
+ "uiCount": uiCount,
+ "gatewayCount": gatewayCount,
}
)
return sorted(out, key=lambda x: (not x.get("isDefault"), x["code"]))
@@ -456,7 +480,7 @@ async def get_language_set(code: str):
db = _publicMgmtDb()
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
if not rows:
- raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
return _row_to_public(rows[0])
@@ -472,7 +496,7 @@ def _validate_iso2_code(code: str) -> str:
c = code.strip().lower()
if not re.fullmatch(r"[a-z]{2}", c):
raise HTTPException(
- status_code=400, detail="Nur ISO-639-1 Zwei-Buchstaben-Codes erlaubt."
+ status_code=400, detail=routeApiMsg("Nur ISO-639-1 Zwei-Buchstaben-Codes erlaubt.")
)
return c
@@ -530,6 +554,7 @@ async def _run_create_language_job_async(userId: str, code: str, label: str, cur
title="Sprachset erstellt",
message=f"Die Sprache «{label}» ({code}) wurde per KI übersetzt{statusHint}.",
)
+ await _reloadI18nCache()
logger.info("i18n create job done: code=%s, translated=%d/%d", code, len(translated), len(xxEntries))
except Exception as e:
logger.exception("create language job failed: %s", e)
@@ -551,16 +576,16 @@ async def create_language_set(
mandateId = _resolveMandateIdForAiI18n(request, currentUser)
code = _validate_iso2_code(body.code)
if code == "xx":
- raise HTTPException(status_code=400, detail="Das Basisset «xx» kann nicht manuell angelegt werden.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Das Basisset «xx» kann nicht manuell angelegt werden."))
db = _publicMgmtDb()
existing = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
if existing:
- raise HTTPException(status_code=409, detail="Dieses Sprachset existiert bereits.")
+ raise HTTPException(status_code=409, detail=routeApiMsg("Dieses Sprachset existiert bereits."))
xxEntries = _loadMasterXxEntries(db)
if not xxEntries:
- raise HTTPException(status_code=503, detail="Basisset (xx) nicht vorhanden. Bitte zuerst UI-Keys einlesen.")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden. Bitte zuerst UI-Keys einlesen."))
resolvedLabel = (body.label or "").strip() if body.label else ""
if not resolvedLabel:
@@ -594,54 +619,59 @@ async def create_language_set(
def _compute_language_sync_diff(db, code: str) -> dict:
"""Return key sync metrics before AI translate (no DB writes)."""
if code == "xx":
- raise HTTPException(status_code=400, detail="Das xx-Set wird separat synchronisiert.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird separat synchronisiert."))
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
if not rows:
- raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
- xx_entries = _loadMasterXxEntries(db)
- if not xx_entries:
- raise HTTPException(status_code=503, detail="Basisset (xx) nicht vorhanden.")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
+ xxEntries = _loadMasterXxEntries(db)
+ if not xxEntries:
+ raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden."))
row = dict(rows[0])
- cur_entries = _rowEntries(row)
- cur_by_key = {e["key"]: e for e in cur_entries}
- xx_by_key = {e["key"]: e for e in xx_entries}
- master_keys = set(xx_by_key.keys())
- current_keys = set(cur_by_key.keys())
- added_count = len(master_keys - current_keys)
- removed_count = len(current_keys - master_keys)
+ curEntries = _rowEntries(row)
+ masterIds = {_entryId(e) for e in xxEntries}
+ currentIds = {_entryId(e) for e in curEntries}
return {
"code": code,
- "addedCount": added_count,
- "removedCount": removed_count,
- "masterEntryCount": len(master_keys),
- "currentEntryCount": len(current_keys),
+ "addedCount": len(masterIds - currentIds),
+ "removedCount": len(currentIds - masterIds),
+ "masterEntryCount": len(masterIds),
+ "currentEntryCount": len(currentIds),
}
+def _entryId(e: dict) -> tuple:
+ """Composite identifier for an i18n entry: (key, context)."""
+ return (e["key"], e.get("context", "ui"))
+
+
async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: Optional[User] = None) -> dict:
- """Synchronise a language set (incl. de) against the xx base set via AI."""
+ """Synchronise a language set (incl. de) against the xx base set via AI.
+
+ Entries are identified by (key, context) — the same text can appear
+ with different contexts (e.g. "ui" and "api.routeXyz").
+ """
if code == "xx":
- raise HTTPException(status_code=400, detail="Das xx-Set wird über 'UI-Keys einlesen' aktualisiert.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert."))
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code})
if not rows:
- raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
xxEntries = _loadMasterXxEntries(db)
if not xxEntries:
- raise HTTPException(status_code=503, detail="Basisset (xx) nicht vorhanden.")
+ raise HTTPException(status_code=503, detail=routeApiMsg("Basisset (xx) nicht vorhanden."))
row = dict(rows[0])
curEntries = _rowEntries(row)
- curByKey = {e["key"]: e for e in curEntries}
- xxByKey = {e["key"]: e for e in xxEntries}
+ curById = {_entryId(e): e for e in curEntries}
+ xxById = {_entryId(e): e for e in xxEntries}
- masterKeys = set(xxByKey.keys())
- currentKeys = set(curByKey.keys())
- removedKeys = sorted(currentKeys - masterKeys)
- addedKeys = sorted(masterKeys - currentKeys)
+ masterIds = set(xxById.keys())
+ currentIds = set(curById.keys())
+ removedIds = currentIds - masterIds
+ addedIds = masterIds - currentIds
translatedCount = 0
- if addedKeys:
- toTranslate = {k: xxByKey[k].get("value", "") for k in addedKeys}
+ if addedIds:
+ toTranslate = {xxById[eid]["key"]: xxById[eid].get("value", "") for eid in addedIds}
langLabel = row.get("label") or code
billingCb = None
if adminUser:
@@ -650,28 +680,29 @@ async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: O
billingCb = _makeBillingCallback(adminUser, memberIds[0])
try:
translated = await _translateBatch(toTranslate, langLabel, code, billingCallback=billingCb)
- translatedCount = sum(1 for k in addedKeys if k in translated)
+ translatedCount = sum(1 for eid in addedIds if xxById[eid]["key"] in translated)
except Exception as e:
logger.error("AI translation during sync failed for %s: %s", code, e)
translated = {}
- for k in addedKeys:
- curByKey[k] = {
- "context": xxByKey[k]["context"],
- "key": k,
- "value": translated.get(k, f"[{k}]"),
+ for eid in addedIds:
+ xxEntry = xxById[eid]
+ curById[eid] = {
+ "context": xxEntry["context"],
+ "key": xxEntry["key"],
+ "value": translated.get(xxEntry["key"], f"[{xxEntry['key']}]"),
}
- for k in removedKeys:
- del curByKey[k]
+ for eid in removedIds:
+ del curById[eid]
- for k in masterKeys & currentKeys:
- curByKey[k]["context"] = xxByKey[k]["context"]
+ for eid in masterIds & currentIds:
+ curById[eid]["context"] = xxById[eid]["context"]
- newEntries = [curByKey[k] for k in sorted(curByKey.keys(), key=lambda x: x.lower())]
+ newEntries = sorted(curById.values(), key=lambda e: (e["key"].lower(), e.get("context", "")))
now = getUtcTimestamp()
- untranslated = len(addedKeys) - translatedCount
+ untranslated = len(addedIds) - translatedCount
row["entries"] = newEntries
if "keys" in row:
del row["keys"]
@@ -681,8 +712,8 @@ async def _syncLanguageWithXx(db, code: str, userId: Optional[str], adminUser: O
db.recordModify(UiLanguageSet, code, row)
return {
"code": code,
- "added": addedKeys,
- "removed": removedKeys,
+ "added": sorted({xxById[eid]["key"] for eid in addedIds}),
+ "removed": sorted({eid[0] for eid in removedIds}),
"translated": translatedCount,
"entriesCount": len(newEntries),
}
@@ -701,7 +732,9 @@ async def sync_xx_master(
db = getMgmtInterface(adminUser, mandateId=None).db
fromBody = await _readOptionalEntriesFromBody(request)
entries = fromBody if fromBody is not None else _scanCodebaseKeys()
- return _syncXxMaster(db, str(adminUser.id), entries)
+ result = _syncXxMaster(db, str(adminUser.id), entries)
+ await _reloadI18nCache()
+ return result
@router.put("/sets/update-all")
@@ -727,6 +760,7 @@ async def update_all_language_sets(
continue
res = await _syncLanguageWithXx(db, cid, str(adminUser.id), adminUser=adminUser)
results.append(res)
+ await _reloadI18nCache()
return {"xxSync": xxSync, "updated": results}
@@ -738,7 +772,7 @@ async def get_language_sync_diff(
"""How many keys would be added/removed vs xx before running a full sync (SysAdmin)."""
c = code.strip().lower()
if c in ("update-all", "sync-xx", "sync-de"):
- raise HTTPException(status_code=400, detail="Ungültiger Sprachcode.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Ungültiger Sprachcode."))
db = getMgmtInterface(adminUser, mandateId=None).db
return _compute_language_sync_diff(db, c)
@@ -750,11 +784,13 @@ async def update_language_set(
):
c = code.strip().lower()
if c in ("update-all", "sync-xx", "sync-de"):
- raise HTTPException(status_code=400, detail="Ungültiger Sprachcode.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Ungültiger Sprachcode."))
if c == "xx":
- raise HTTPException(status_code=400, detail="Das xx-Set wird über 'UI-Keys einlesen' aktualisiert.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Das xx-Set wird über 'UI-Keys einlesen' aktualisiert."))
db = getMgmtInterface(adminUser, mandateId=None).db
- return await _syncLanguageWithXx(db, c, str(adminUser.id), adminUser=adminUser)
+ result = await _syncLanguageWithXx(db, c, str(adminUser.id), adminUser=adminUser)
+ await _reloadI18nCache()
+ return result
@router.delete("/sets/{code}")
@@ -768,7 +804,8 @@ async def delete_language_set(
db = getMgmtInterface(adminUser, mandateId=None).db
ok = db.recordDelete(UiLanguageSet, c)
if not ok:
- raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
+ await _reloadI18nCache()
return {"deleted": c}
@@ -780,7 +817,7 @@ async def download_language_set(
db = _publicMgmtDb()
rows = db.getRecordset(UiLanguageSet, recordFilter={"id": code.strip().lower()})
if not rows:
- raise HTTPException(status_code=404, detail="Sprachset nicht gefunden")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Sprachset nicht gefunden"))
payload = _row_to_public(rows[0])
raw = json.dumps(payload, ensure_ascii=False, indent=2)
return Response(
@@ -828,7 +865,7 @@ async def import_language_sets(
adminUser: User = Depends(requireSysAdminRole),
):
if not file.filename or not file.filename.endswith(".json"):
- raise HTTPException(status_code=400, detail="Nur .json-Dateien erlaubt.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Nur .json-Dateien erlaubt."))
try:
raw = await file.read()
@@ -837,7 +874,7 @@ async def import_language_sets(
raise HTTPException(status_code=400, detail=f"Ungültiges JSON: {e}")
if not isinstance(data, list):
- raise HTTPException(status_code=400, detail="JSON muss ein Array von Sprachsets sein.")
+ raise HTTPException(status_code=400, detail=routeApiMsg("JSON muss ein Array von Sprachsets sein."))
db = getMgmtInterface(adminUser, mandateId=None).db
now = getUtcTimestamp()
@@ -893,4 +930,44 @@ async def import_language_sets(
created.append(code)
logger.info("i18n import: created=%s, updated=%s", created, updated)
+ await _reloadI18nCache()
return {"created": created, "updated": updated, "totalProcessed": len(created) + len(updated)}
+
+
+# ---------------------------------------------------------------------------
+# Phase 7b: translate-field — on-demand translation for TextMultilingual fields
+# ---------------------------------------------------------------------------
+
+_TRANSLATE_FIELD_MAX_LEN = 2000
+
+
+class TranslateFieldRequest(BaseModel):
+ sourceText: str = Field(..., min_length=1, max_length=_TRANSLATE_FIELD_MAX_LEN)
+ sourceLang: str = Field(default="de", min_length=2, max_length=5)
+ targetLangs: List[str] = Field(..., min_length=1)
+
+
+@router.post("/translate-field")
+async def translateField(
+ body: TranslateFieldRequest,
+ request: Request,
+ currentUser: User = Depends(getCurrentUser),
+):
+ """Translate a single text into one or more target languages (for TextMultilingual fields)."""
+ targets = [c for c in body.targetLangs if c != body.sourceLang]
+ if not targets:
+ return {"translations": {}}
+
+ mandateId = _resolveMandateIdForAiI18n(request, currentUser)
+ billingCb = _makeBillingCallback(currentUser, mandateId)
+
+ results: Dict[str, str] = {}
+ for targetCode in targets:
+ targetLabel = _ISO_LABELS.get(targetCode, targetCode)
+ keysToTranslate = {body.sourceText: "TextMultilingual field"}
+ translated = await _translateBatch(keysToTranslate, targetLabel, targetCode, billingCb)
+ val = translated.get(body.sourceText, "")
+ if val:
+ results[targetCode] = val
+
+ return {"translations": results}
diff --git a/modules/routes/routeInvitations.py b/modules/routes/routeInvitations.py
index 43d803e3..9354c31c 100644
--- a/modules/routes/routeInvitations.py
+++ b/modules/routes/routeInvitations.py
@@ -25,6 +25,8 @@ from modules.routes.routeDataUsers import _applyFiltersAndSort
from modules.datamodels.datamodelInvitation import Invitation
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.shared.timeUtils import getUtcTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeInvitations")
logger = logging.getLogger(__name__)
@@ -161,7 +163,7 @@ def create_invitation(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required for mandate-level invitations"
+ detail=routeApiMsg("X-Mandate-Id header is required for mandate-level invitations")
)
mandateId = str(context.mandateId)
# Validate roles are mandate-level (no featureInstanceId)
@@ -188,12 +190,12 @@ def create_invitation(
if str(context.mandateId) != mandateId:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this mandate"
+ detail=routeApiMsg("Access denied to this mandate")
)
if not _hasMandateAdminRole(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to create invitations"
+ detail=routeApiMsg("Mandate-Admin role required to create invitations")
)
# Calculate expiration time
@@ -427,14 +429,14 @@ def list_invitations(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required"
+ detail=routeApiMsg("X-Mandate-Id header is required")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to list invitations"
+ detail=routeApiMsg("Mandate-Admin role required to list invitations")
)
try:
@@ -522,9 +524,9 @@ def get_invitation_filter_values(
) -> list:
"""Return distinct filter values for a column in invitations."""
if not context.mandateId:
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="X-Mandate-Id header is required")
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("X-Mandate-Id header is required"))
if not _hasMandateAdminRole(context):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Mandate-Admin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Mandate-Admin role required"))
try:
from modules.routes.routeDataUsers import _handleFilterValuesRequest
rootInterface = getRootInterface()
@@ -575,14 +577,14 @@ def revoke_invitation(
if not context.mandateId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="X-Mandate-Id header is required"
+ detail=routeApiMsg("X-Mandate-Id header is required")
)
# Check mandate admin permission
if not _hasMandateAdminRole(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Mandate-Admin role required to revoke invitations"
+ detail=routeApiMsg("Mandate-Admin role required to revoke invitations")
)
try:
@@ -601,14 +603,14 @@ def revoke_invitation(
if str(invitation.mandateId) != str(context.mandateId):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Access denied to this invitation"
+ detail=routeApiMsg("Access denied to this invitation")
)
# Already revoked?
if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation is already revoked"
+ detail=routeApiMsg("Invitation is already revoked")
)
# Revoke invitation
@@ -781,14 +783,14 @@ def accept_invitation(
if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Invitation not found"
+ detail=routeApiMsg("Invitation not found")
)
# Validate invitation
if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has been revoked"
+ detail=routeApiMsg("Invitation has been revoked")
)
currentTime = getUtcTimestamp()
@@ -796,7 +798,7 @@ def accept_invitation(
if expiresAt < currentTime:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has expired"
+ detail=routeApiMsg("Invitation has expired")
)
currentUses = invitation.currentUses or 0
@@ -804,7 +806,7 @@ def accept_invitation(
if currentUses >= maxUses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has reached maximum uses"
+ detail=routeApiMsg("Invitation has reached maximum uses")
)
# Validate user matches - invitation is bound by username or email
@@ -833,7 +835,7 @@ def accept_invitation(
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has no target user or email"
+ detail=routeApiMsg("Invitation has no target user or email")
)
mandateId = str(invitation.mandateId) if invitation.mandateId else None
diff --git a/modules/routes/routeMessaging.py b/modules/routes/routeMessaging.py
index 42e15f0e..c2e0766f 100644
--- a/modules/routes/routeMessaging.py
+++ b/modules/routes/routeMessaging.py
@@ -22,6 +22,8 @@ from modules.datamodels.datamodelMessaging import (
)
from modules.datamodels.datamodelUam import User
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeMessaging")
# Configure logger
logger = logging.getLogger(__name__)
@@ -139,7 +141,7 @@ def update_subscription(
if not updatedSubscription:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error updating the subscription"
+ detail=routeApiMsg("Error updating the subscription")
)
return MessagingSubscription(**updatedSubscription)
@@ -166,7 +168,7 @@ def delete_subscription(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error deleting the subscription"
+ detail=routeApiMsg("Error deleting the subscription")
)
return {"message": f"Subscription with ID {subscriptionId} successfully deleted"}
@@ -263,7 +265,7 @@ def unsubscribe_user(
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Registration not found"
+ detail=routeApiMsg("Registration not found")
)
return {"message": f"Successfully unsubscribed from {subscriptionId} for channel {channel.value}"}
@@ -339,7 +341,7 @@ def update_registration(
if not updatedRegistration:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error updating the registration"
+ detail=routeApiMsg("Error updating the registration")
)
return MessagingSubscriptionRegistration(**updatedRegistration)
@@ -366,7 +368,7 @@ def delete_registration(
if not success:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Error deleting the registration"
+ detail=routeApiMsg("Error deleting the registration")
)
return {"message": f"Registration with ID {registrationId} successfully deleted"}
@@ -397,7 +399,7 @@ def trigger_subscription(
if not _hasTriggerPermission(context):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Admin or Mandate-Admin role required to trigger subscriptions"
+ detail=routeApiMsg("Admin or Mandate-Admin role required to trigger subscriptions")
)
# Get messaging service from request app state
diff --git a/modules/routes/routeNotifications.py b/modules/routes/routeNotifications.py
index a533a535..41d7fe26 100644
--- a/modules/routes/routeNotifications.py
+++ b/modules/routes/routeNotifications.py
@@ -22,6 +22,8 @@ from modules.datamodels.datamodelNotification import (
from modules.datamodels.datamodelRbac import Role
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.shared.timeUtils import getUtcTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeNotifications")
logger = logging.getLogger(__name__)
@@ -238,14 +240,14 @@ def markAsRead(
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Notification not found"
+ detail=routeApiMsg("Notification not found")
)
# Verify ownership
if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Not authorized to access this notification"
+ detail=routeApiMsg("Not authorized to access this notification")
)
# Update status
@@ -332,21 +334,21 @@ def executeAction(
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Notification not found"
+ detail=routeApiMsg("Notification not found")
)
# Verify ownership
if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Not authorized to access this notification"
+ detail=routeApiMsg("Not authorized to access this notification")
)
# Check if already actioned
if notification.status == NotificationStatus.ACTIONED.value:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Notification has already been actioned"
+ detail=routeApiMsg("Notification has already been actioned")
)
# Validate action exists
@@ -416,7 +418,7 @@ def _handleInvitationAction(
if not invitationId:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="No invitation reference found"
+ detail=routeApiMsg("No invitation reference found")
)
# Get the invitation (Pydantic model)
@@ -425,7 +427,7 @@ def _handleInvitationAction(
if not invitation:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Invitation not found"
+ detail=routeApiMsg("Invitation not found")
)
# Verify user matches (username or email)
@@ -436,18 +438,18 @@ def _handleInvitationAction(
if currentUser.username != targetUsername:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="This invitation is for a different user"
+ detail=routeApiMsg("This invitation is for a different user")
)
elif invitationEmail:
if not currentUserEmail or currentUserEmail != invitationEmail:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="This invitation is for a different user"
+ detail=routeApiMsg("This invitation is for a different user")
)
else:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has no target user or email"
+ detail=routeApiMsg("Invitation has no target user or email")
)
# Check if invitation is still valid
@@ -456,13 +458,13 @@ def _handleInvitationAction(
if expiresAt < currentTime:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has expired"
+ detail=routeApiMsg("Invitation has expired")
)
if invitation.revokedAt:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has been revoked"
+ detail=routeApiMsg("Invitation has been revoked")
)
currentUses = invitation.currentUses or 0
@@ -470,7 +472,7 @@ def _handleInvitationAction(
if currentUses >= maxUses:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invitation has reached maximum uses"
+ detail=routeApiMsg("Invitation has reached maximum uses")
)
if actionId == "accept":
@@ -565,14 +567,14 @@ def deleteNotification(
if not notification:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Notification not found"
+ detail=routeApiMsg("Notification not found")
)
# Verify ownership
if str(notification.userId) != str(currentUser.id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Not authorized to delete this notification"
+ detail=routeApiMsg("Not authorized to delete this notification")
)
# Mark as dismissed (soft delete)
diff --git a/modules/routes/routeRealEstate.py b/modules/routes/routeRealEstate.py
index a3466aca..aa3d98f4 100644
--- a/modules/routes/routeRealEstate.py
+++ b/modules/routes/routeRealEstate.py
@@ -64,6 +64,8 @@ from modules.routes.routeRealEstateScraping import (
# Import attribute utilities for model schema
from modules.shared.attributeUtils import getModelAttributeDefinitions
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeRealEstate")
# Configure logger
logger = logging.getLogger(__name__)
@@ -308,7 +310,7 @@ async def update_project(
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
updated = interface.updateProjekt(projectId, data)
if not updated:
- raise HTTPException(status_code=500, detail="Update failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
return updated
@@ -329,7 +331,7 @@ async def delete_project(
if not projekt or str(getattr(projekt, "featureInstanceId", None)) != instanceId:
raise HTTPException(status_code=404, detail=f"Project '{projectId}' not found")
if not interface.deleteProjekt(projectId):
- raise HTTPException(status_code=500, detail="Delete failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
# ----- Parcels CRUD -----
@@ -429,7 +431,7 @@ async def update_parcel(
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
updated = interface.updateParzelle(parcelId, data)
if not updated:
- raise HTTPException(status_code=500, detail="Update failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Update failed"))
return updated
@@ -450,7 +452,7 @@ async def delete_parcel(
if not parzelle or str(getattr(parzelle, "featureInstanceId", None)) != instanceId:
raise HTTPException(status_code=404, detail=f"Parcel '{parcelId}' not found")
if not interface.deleteParzelle(parcelId):
- raise HTTPException(status_code=500, detail="Delete failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Delete failed"))
# ============================================================================
@@ -495,7 +497,7 @@ async def process_command(
logger.warning(f"CSRF token missing for POST /api/realestate/command from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -503,7 +505,7 @@ async def process_command(
logger.warning(f"Invalid CSRF token format for POST /api/realestate/command from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -513,7 +515,7 @@ async def process_command(
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/command from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Processing command request from user {currentUser.id} (mandate: {currentUser.mandateId})")
@@ -566,7 +568,7 @@ async def get_available_tables(
logger.warning(f"CSRF token missing for GET /api/realestate/tables from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -574,7 +576,7 @@ async def get_available_tables(
logger.warning(f"Invalid CSRF token format for GET /api/realestate/tables from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -584,7 +586,7 @@ async def get_available_tables(
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/tables from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Getting available tables for user {currentUser.id} (mandate: {currentUser.mandateId})")
@@ -675,7 +677,7 @@ async def get_table_data(
logger.warning(f"CSRF token missing for GET /api/realestate/table/{table} from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -683,7 +685,7 @@ async def get_table_data(
logger.warning(f"Invalid CSRF token format for GET /api/realestate/table/{table} from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -693,7 +695,7 @@ async def get_table_data(
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/table/{table} from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Getting table data for '{table}' from user {currentUser.id} (mandate: {currentUser.mandateId})")
@@ -844,7 +846,7 @@ async def create_table_record(
logger.warning(f"CSRF token missing for POST /api/realestate/table/{table} from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -852,7 +854,7 @@ async def create_table_record(
logger.warning(f"Invalid CSRF token format for POST /api/realestate/table/{table} from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -862,7 +864,7 @@ async def create_table_record(
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/table/{table} from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Special handling for Projekt with parcel data
@@ -874,7 +876,7 @@ async def create_table_record(
if not label:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="label is required"
+ detail=routeApiMsg("label is required")
)
status_prozess = data.get("statusProzess", "Eingang")
@@ -887,7 +889,7 @@ async def create_table_record(
if not isinstance(parzellen_data, list):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="parzellen must be an array"
+ detail=routeApiMsg("parzellen must be an array")
)
elif "parzelle" in data:
# Single parcel (backward compatibility)
@@ -898,7 +900,7 @@ async def create_table_record(
if not parzellen_data:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="parzelle or parzellen data is required"
+ detail=routeApiMsg("parzelle or parzellen data is required")
)
# Use helper function to create project with parcel data
@@ -1073,7 +1075,7 @@ async def search_parcel(
logger.warning(f"CSRF token missing for GET /api/realestate/parcel/search from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
logger.info(f"Searching parcel for user {currentUser.id} (mandate: {currentUser.mandateId}) with location: {location}")
@@ -2059,21 +2061,21 @@ async def add_parcel_to_project(
logger.warning(f"CSRF token missing for POST /api/realestate/projekt/{projekt_id}/add-parcel from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Validate CSRF token format
if not isinstance(csrf_token, str) or len(csrf_token) < 16 or len(csrf_token) > 64:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
try:
int(csrf_token, 16)
except ValueError:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Adding parcel to project {projekt_id} for user {currentUser.id} (mandate: {currentUser.mandateId})")
@@ -2294,7 +2296,7 @@ async def get_bzo_information(
logger.warning(f"CSRF token missing for GET /api/realestate/bzo-information from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -2302,7 +2304,7 @@ async def get_bzo_information(
logger.warning(f"Invalid CSRF token format for GET /api/realestate/bzo-information from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -2312,7 +2314,7 @@ async def get_bzo_information(
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/bzo-information from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Extracting BZO information for Gemeinde '{gemeinde}', Bauzone '{bauzone}' (user: {currentUser.id}, mandate: {currentUser.mandateId})")
diff --git a/modules/routes/routeRealEstateScraping.py b/modules/routes/routeRealEstateScraping.py
index 4b8d2d0d..abb54299 100644
--- a/modules/routes/routeRealEstateScraping.py
+++ b/modules/routes/routeRealEstateScraping.py
@@ -36,6 +36,8 @@ from modules.connectors.connectorOerebWfs import OerebWfsConnector
# Import Tavily connector for BZO document search
from modules.aicore.aicorePluginTavily import AiTavily
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeRealEstateScraping")
# Configure logger
logger = logging.getLogger(__name__)
@@ -107,7 +109,7 @@ async def scrape_switzerland_route(
logger.warning(f"CSRF token missing for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -115,7 +117,7 @@ async def scrape_switzerland_route(
logger.warning(f"Invalid CSRF token format for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -125,7 +127,7 @@ async def scrape_switzerland_route(
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/scrape-switzerland from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Extract parameters from body with defaults
@@ -137,19 +139,19 @@ async def scrape_switzerland_route(
if grid_size <= 0 or grid_size > 10000:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="grid_size must be between 0 and 10000 meters"
+ detail=routeApiMsg("grid_size must be between 0 and 10000 meters")
)
if max_concurrent <= 0 or max_concurrent > 200:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="max_concurrent must be between 1 and 200"
+ detail=routeApiMsg("max_concurrent must be between 1 and 200")
)
if batch_size <= 0 or batch_size > 1000:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="batch_size must be between 1 and 1000"
+ detail=routeApiMsg("batch_size must be between 1 and 1000")
)
logger.info(
@@ -246,7 +248,7 @@ async def get_all_gemeinden(
logger.warning(f"CSRF token missing for GET /api/realestate/gemeinden from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -254,7 +256,7 @@ async def get_all_gemeinden(
logger.warning(f"Invalid CSRF token format for GET /api/realestate/gemeinden from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -264,7 +266,7 @@ async def get_all_gemeinden(
logger.warning(f"CSRF token is not a valid hex string for GET /api/realestate/gemeinden from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Fetching all Gemeinden for user {currentUser.id} (mandate: {currentUser.mandateId}), only_current={only_current}")
@@ -548,7 +550,7 @@ async def fetch_bzo_documents(
logger.warning(f"CSRF token missing for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing. Please include X-CSRF-Token header."
+ detail=routeApiMsg("CSRF token missing. Please include X-CSRF-Token header.")
)
# Basic CSRF token format validation
@@ -556,7 +558,7 @@ async def fetch_bzo_documents(
logger.warning(f"Invalid CSRF token format for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
# Validate token is hex string
@@ -566,7 +568,7 @@ async def fetch_bzo_documents(
logger.warning(f"CSRF token is not a valid hex string for POST /api/realestate/gemeinden/fetch-bzo-documents from user {currentUser.id}")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="Invalid CSRF token format"
+ detail=routeApiMsg("Invalid CSRF token format")
)
logger.info(f"Starting BZO document fetch for user {currentUser.id} (mandate: {currentUser.mandateId})")
diff --git a/modules/routes/routeSecurityAdmin.py b/modules/routes/routeSecurityAdmin.py
index acba83b4..acc5cdc5 100644
--- a/modules/routes/routeSecurityAdmin.py
+++ b/modules/routes/routeSecurityAdmin.py
@@ -17,6 +17,8 @@ from modules.interfaces.interfaceDbApp import getRootInterface
from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority
from modules.datamodels.datamodelSecurity import Token
from modules.shared.configuration import APP_CONFIG
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSecurityAdmin")
logger = logging.getLogger(__name__)
@@ -132,7 +134,7 @@ def list_tokens(
raise
except Exception as e:
logger.error(f"Error listing tokens: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to list tokens")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to list tokens"))
@router.post("/tokens/revoke/user")
@@ -151,7 +153,7 @@ def revoke_tokens_by_user(
authority = payload.get("authority")
reason = payload.get("reason", "sysadmin revoke")
if not userId:
- raise HTTPException(status_code=400, detail="userId is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("userId is required"))
appInterface = getRootInterface()
# MULTI-TENANT: SysAdmin can revoke any user's tokens (no mandate restriction)
@@ -167,7 +169,7 @@ def revoke_tokens_by_user(
raise
except Exception as e:
logger.error(f"Error revoking tokens by user: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to revoke tokens")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke tokens"))
@router.post("/tokens/revoke/session")
@@ -187,7 +189,7 @@ def revoke_tokens_by_session(
authority = payload.get("authority", "local")
reason = payload.get("reason", "sysadmin session revoke")
if not userId or not sessionId:
- raise HTTPException(status_code=400, detail="userId and sessionId are required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("userId and sessionId are required"))
appInterface = getRootInterface()
# MULTI-TENANT: SysAdmin can revoke any session (no mandate check)
@@ -203,7 +205,7 @@ def revoke_tokens_by_session(
raise
except Exception as e:
logger.error(f"Error revoking tokens by session: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to revoke session tokens")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke session tokens"))
@router.post("/tokens/revoke/id")
@@ -221,7 +223,7 @@ def revoke_token_by_id(
tokenId = payload.get("tokenId")
reason = payload.get("reason", "sysadmin revoke")
if not tokenId:
- raise HTTPException(status_code=400, detail="tokenId is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("tokenId is required"))
appInterface = getRootInterface()
# MULTI-TENANT: SysAdmin can revoke any token (no mandate check)
ok = appInterface.revokeTokenById(tokenId, revokedBy=currentUser.id, reason=reason)
@@ -230,7 +232,7 @@ def revoke_token_by_id(
raise
except Exception as e:
logger.error(f"Error revoking token by id: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to revoke token")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke token"))
@router.post("/tokens/revoke/mandate")
@@ -249,7 +251,7 @@ def revoke_tokens_by_mandate(
authority = payload.get("authority", "local")
reason = payload.get("reason", "sysadmin mandate revoke")
if not mandateId:
- raise HTTPException(status_code=400, detail="mandateId is required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("mandateId is required"))
# MULTI-TENANT: SysAdmin can revoke tokens for any mandate
appInterface = getRootInterface()
@@ -271,7 +273,7 @@ def revoke_tokens_by_mandate(
raise
except Exception as e:
logger.error(f"Error revoking tokens by mandate: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to revoke mandate tokens")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to revoke mandate tokens"))
@@ -295,7 +297,7 @@ def list_databases(
return {"databases": databases}
except Exception as e:
logger.error(f"Failed to load databases from host: {e}")
- raise HTTPException(status_code=500, detail="Failed to load databases from host")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to load databases from host"))
@router.get("/databases/{database_name}/tables")
@@ -310,7 +312,7 @@ def get_database_tables(
MULTI-TENANT: SysAdmin-only (infrastructure management).
"""
if not database_name.startswith("poweron_"):
- raise HTTPException(status_code=400, detail="Invalid database name format")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid database name format"))
connector = None
try:
@@ -341,7 +343,7 @@ def drop_table(
MULTI-TENANT: SysAdmin-only (infrastructure management).
"""
if not database_name.startswith("poweron_"):
- raise HTTPException(status_code=400, detail="Invalid database name format")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid database name format"))
connector = None
try:
@@ -354,7 +356,7 @@ def drop_table(
WHERE table_schema = 'public' AND table_name = %s
""", (table_name,))
if not cursor.fetchone():
- raise HTTPException(status_code=404, detail="Table not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Table not found"))
# Drop the table
cursor.execute(f'DROP TABLE IF EXISTS "{table_name}" CASCADE')
@@ -369,7 +371,7 @@ def drop_table(
logger.error(f"Error dropping table: {str(e)}")
if connector and connector.connection:
connector.connection.rollback()
- raise HTTPException(status_code=500, detail="Failed to drop table")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to drop table"))
finally:
if connector:
connector.close()
@@ -389,7 +391,7 @@ def drop_database(
dbName = payload.get("database")
if not dbName or not dbName.startswith("poweron_"):
- raise HTTPException(status_code=400, detail="Invalid database name")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid database name"))
# Validate database exists
try:
@@ -425,7 +427,7 @@ def drop_database(
logger.error(f"Error dropping database tables: {str(e)}")
if connector and connector.connection:
connector.connection.rollback()
- raise HTTPException(status_code=500, detail="Failed to drop database tables")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to drop database tables"))
finally:
if connector:
connector.close()
diff --git a/modules/routes/routeSecurityClickup.py b/modules/routes/routeSecurityClickup.py
index 3d1aeed5..ca787391 100644
--- a/modules/routes/routeSecurityClickup.py
+++ b/modules/routes/routeSecurityClickup.py
@@ -19,6 +19,8 @@ from modules.datamodels.datamodelUam import AuthAuthority, User, ConnectionStatu
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
from modules.auth import getCurrentUser, limiter, SECRET_KEY, ALGORITHM
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSecurityClickup")
logger = logging.getLogger(__name__)
@@ -53,7 +55,7 @@ def _require_clickup_config():
if not CLIENT_ID or not CLIENT_SECRET or not REDIRECT_URI:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="ClickUp OAuth is not configured (Service_CLICKUP_CLIENT_ID, Service_CLICKUP_CLIENT_SECRET, Service_CLICKUP_OAUTH_REDIRECT_URI)",
+ detail=routeApiMsg("ClickUp OAuth is not configured (Service_CLICKUP_CLIENT_ID, Service_CLICKUP_CLIENT_SECRET, Service_CLICKUP_OAUTH_REDIRECT_URI)"),
)
@@ -87,7 +89,7 @@ def auth_connect(
connection = conn
break
if not connection:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="ClickUp connection not found")
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("ClickUp connection not found"))
state_jwt = _issue_oauth_state(
{
@@ -123,11 +125,11 @@ async def auth_connect_callback(
"""OAuth callback for ClickUp data connection."""
state_data = _parse_oauth_state(state)
if state_data.get("flow") != _FLOW_CONNECT:
- raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
connection_id = state_data.get("connectionId")
user_id = state_data.get("userId")
if not connection_id or not user_id:
- raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Missing connection or user in OAuth state"))
_require_clickup_config()
diff --git a/modules/routes/routeSecurityGoogle.py b/modules/routes/routeSecurityGoogle.py
index 2b380db0..6f227dcc 100644
--- a/modules/routes/routeSecurityGoogle.py
+++ b/modules/routes/routeSecurityGoogle.py
@@ -33,6 +33,8 @@ from modules.auth import (
from modules.auth.tokenManager import TokenManager
from modules.auth.oauthProviderConfig import googleAuthScopes, googleDataScopes
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSecurityGoogle")
logger = logging.getLogger(__name__)
@@ -131,7 +133,7 @@ def _require_google_auth_config():
if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Google Auth OAuth is not configured (Service_GOOGLE_AUTH_*)",
+ detail=routeApiMsg("Google Auth OAuth is not configured (Service_GOOGLE_AUTH_*)"),
)
@@ -139,7 +141,7 @@ def _require_google_data_config():
if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Google Data OAuth is not configured (Service_GOOGLE_DATA_*)",
+ detail=routeApiMsg("Google Data OAuth is not configured (Service_GOOGLE_DATA_*)"),
)
@@ -179,7 +181,7 @@ async def auth_login_callback(
"""OAuth callback for Google Auth app (login only)."""
state_data = _parse_oauth_state(state)
if state_data.get("flow") != _FLOW_LOGIN:
- raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
_require_google_auth_config()
oauth = OAuth2Session(client_id=AUTH_CLIENT_ID, redirect_uri=AUTH_REDIRECT_URI)
@@ -214,7 +216,7 @@ async def auth_login_callback(
if user_info_response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to get user info from Google",
+ detail=routeApiMsg("Failed to get user info from Google"),
)
user_info = user_info_response.json()
@@ -310,7 +312,7 @@ def auth_connect(
connection = conn
break
if not connection:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Google connection not found")
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Google connection not found"))
state_jwt = _issue_oauth_state(
{
@@ -359,11 +361,11 @@ async def auth_connect_callback(
"""OAuth callback for Google Data app (UserConnection)."""
state_data = _parse_oauth_state(state)
if state_data.get("flow") != _FLOW_CONNECT:
- raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
connection_id = state_data.get("connectionId")
user_id = state_data.get("userId")
if not connection_id or not user_id:
- raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Missing connection or user in OAuth state"))
_require_google_data_config()
oauth = OAuth2Session(client_id=DATA_CLIENT_ID, redirect_uri=DATA_REDIRECT_URI)
@@ -419,7 +421,7 @@ async def auth_connect_callback(
if user_info_response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to get user info from Google",
+ detail=routeApiMsg("Failed to get user info from Google"),
)
user_info = user_info_response.json()
@@ -557,7 +559,7 @@ def logout(
if not token:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="No token found",
+ detail=routeApiMsg("No token found"),
)
try:
@@ -568,7 +570,7 @@ def logout(
logger.error(f"Failed to decode JWT on Google logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invalid token",
+ detail=routeApiMsg("Invalid token"),
)
revoked = 0
@@ -635,13 +637,13 @@ async def verify_token(
if not google_connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No Google connection found for current user",
+ detail=routeApiMsg("No Google connection found for current user"),
)
current_token = TokenManager().getFreshToken(google_connection.id)
if not current_token:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No Google token found for this connection",
+ detail=routeApiMsg("No Google token found for this connection"),
)
token_verification = await verify_google_token(current_token.tokenAccess)
return {
@@ -690,7 +692,7 @@ async def refresh_token(
if not google_connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Requested Google connection not found for current user",
+ detail=routeApiMsg("Requested Google connection not found for current user"),
)
else:
for conn in connections:
@@ -700,13 +702,13 @@ async def refresh_token(
if not google_connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No Google connection found for current user",
+ detail=routeApiMsg("No Google connection found for current user"),
)
current_token = TokenManager().getFreshToken(google_connection.id)
if not current_token:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No Google token found for this connection",
+ detail=routeApiMsg("No Google token found for this connection"),
)
expiresAtValue = parseTimestamp(current_token.expiresAt)
google_connection.expiresAt = (
diff --git a/modules/routes/routeSecurityLocal.py b/modules/routes/routeSecurityLocal.py
index 9ec4fc38..daa128e0 100644
--- a/modules/routes/routeSecurityLocal.py
+++ b/modules/routes/routeSecurityLocal.py
@@ -21,6 +21,8 @@ from modules.datamodels.datamodelUam import User, UserInDB, AuthAuthority, Manda
from modules.datamodels.datamodelSecurity import Token, TokenPurpose
from modules.shared.configuration import APP_CONFIG
from modules.shared.timeUtils import getUtcTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSecurityLocal")
# Configure logger
logger = logging.getLogger(__name__)
@@ -231,7 +233,7 @@ def login(
if not csrf_token:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="CSRF token missing"
+ detail=routeApiMsg("CSRF token missing")
)
# Get gateway interface with root privileges for authentication
@@ -248,7 +250,7 @@ def login(
if not user:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Invalid username or password",
+ detail=routeApiMsg("Invalid username or password"),
headers={"WWW-Authenticate": "Bearer"},
)
@@ -280,7 +282,7 @@ def login(
expires_at = datetime.fromtimestamp(payload.get("exp"))
except Exception as e:
logger.error(f"Failed to decode access token: {str(e)}")
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to finalize token")
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=routeApiMsg("Failed to finalize token"))
# Get user-specific interface for token operations
userInterface = getInterface(user)
@@ -425,7 +427,7 @@ def register_user(
if not user:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Failed to register user"
+ detail=routeApiMsg("Failed to register user")
)
# Check for pending invitations BEFORE provisioning.
@@ -581,32 +583,32 @@ def refresh_token(
# Get refresh token from cookie
refresh_token = request.cookies.get('refresh_token')
if not refresh_token:
- raise HTTPException(status_code=401, detail="No refresh token found")
+ raise HTTPException(status_code=401, detail=routeApiMsg("No refresh token found"))
# Validate refresh token
try:
payload = jwt.decode(refresh_token, SECRET_KEY, algorithms=[ALGORITHM])
if payload.get("type") != "refresh":
- raise HTTPException(status_code=401, detail="Invalid refresh token type")
+ raise HTTPException(status_code=401, detail=routeApiMsg("Invalid refresh token type"))
except jwt.ExpiredSignatureError:
- raise HTTPException(status_code=401, detail="Refresh token expired")
+ raise HTTPException(status_code=401, detail=routeApiMsg("Refresh token expired"))
except jwt.JWTError:
- raise HTTPException(status_code=401, detail="Invalid refresh token")
+ raise HTTPException(status_code=401, detail=routeApiMsg("Invalid refresh token"))
# Get user information from refresh token payload
user_id = payload.get("userId")
if not user_id:
- raise HTTPException(status_code=401, detail="Invalid refresh token - missing user ID")
+ raise HTTPException(status_code=401, detail=routeApiMsg("Invalid refresh token - missing user ID"))
# Get user from database using the user ID from refresh token
try:
app_interface = getRootInterface()
current_user = app_interface.getUser(user_id)
if not current_user:
- raise HTTPException(status_code=401, detail="User not found")
+ raise HTTPException(status_code=401, detail=routeApiMsg("User not found"))
except Exception as e:
logger.error(f"Failed to get user from database: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to validate user")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to validate user"))
# Create new token data
# MULTI-TENANT: Token does NOT contain mandateId anymore
@@ -627,7 +629,7 @@ def refresh_token(
expires_at = datetime.fromtimestamp(payload.get("exp"))
except Exception as e:
logger.error(f"Failed to decode new access token: {str(e)}")
- raise HTTPException(status_code=500, detail="Failed to create new token")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Failed to create new token"))
return {
"type": "token_refresh_success",
@@ -643,7 +645,7 @@ def refresh_token(
raise
except Exception as e:
logger.error(f"Token refresh error: {str(e)}")
- raise HTTPException(status_code=500, detail="Token refresh failed")
+ raise HTTPException(status_code=500, detail=routeApiMsg("Token refresh failed"))
@router.post("/logout")
@limiter.limit("30/minute")
@@ -661,7 +663,7 @@ def logout(request: Request, response: Response, currentUser: User = Depends(get
token = auth_header.split(" ", 1)[1].strip()
if not token:
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="No token found")
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("No token found"))
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
@@ -669,7 +671,7 @@ def logout(request: Request, response: Response, currentUser: User = Depends(get
jti = payload.get("jti")
except Exception as e:
logger.error(f"Failed to decode JWT on logout: {str(e)}")
- raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Invalid token")
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=routeApiMsg("Invalid token"))
revoked = 0
if session_id:
@@ -927,14 +929,14 @@ def password_reset(
except ValueError:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Ungültiger oder abgelaufener Reset-Link"
+ detail=routeApiMsg("Ungültiger oder abgelaufener Reset-Link")
)
# Validate password strength
if len(password) < 8:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Passwort muss mindestens 8 Zeichen lang sein"
+ detail=routeApiMsg("Passwort muss mindestens 8 Zeichen lang sein")
)
rootInterface = getRootInterface()
@@ -945,7 +947,7 @@ def password_reset(
if not success:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Ungültiger oder abgelaufener Reset-Link"
+ detail=routeApiMsg("Ungültiger oder abgelaufener Reset-Link")
)
# Log success
@@ -968,7 +970,7 @@ def password_reset(
logger.error(f"Error in password reset: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Passwort-Zurücksetzung fehlgeschlagen"
+ detail=routeApiMsg("Passwort-Zurücksetzung fehlgeschlagen")
)
@@ -1005,10 +1007,10 @@ def _deleteNeutralizationMapping(
rootIf = getRootInterface()
records = rootIf.db.getRecordset(DataNeutralizerAttributes, recordFilter={"id": mappingId})
if not records:
- raise HTTPException(status_code=404, detail="Mapping not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Mapping not found"))
rec = records[0]
recUserId = rec.get("userId") if isinstance(rec, dict) else getattr(rec, "userId", None)
if recUserId != userId:
- raise HTTPException(status_code=403, detail="Not your mapping")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Not your mapping"))
rootIf.db.recordDelete(DataNeutralizerAttributes, mappingId)
return {"deleted": True, "id": mappingId}
diff --git a/modules/routes/routeSecurityMsft.py b/modules/routes/routeSecurityMsft.py
index d7fac372..72f7759a 100644
--- a/modules/routes/routeSecurityMsft.py
+++ b/modules/routes/routeSecurityMsft.py
@@ -34,6 +34,8 @@ from modules.auth import (
from modules.auth.tokenManager import TokenManager
from modules.auth.oauthProviderConfig import msftAuthScopes, msftDataScopes, msftDataScopesForRefresh
from modules.shared.timeUtils import createExpirationTimestamp, getUtcTimestamp, parseTimestamp
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSecurityMsft")
logger = logging.getLogger(__name__)
@@ -80,7 +82,7 @@ def _require_msft_auth_config():
if not AUTH_CLIENT_ID or not AUTH_CLIENT_SECRET or not AUTH_REDIRECT_URI:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Microsoft Auth OAuth is not configured (Service_MSFT_AUTH_*)",
+ detail=routeApiMsg("Microsoft Auth OAuth is not configured (Service_MSFT_AUTH_*)"),
)
@@ -88,7 +90,7 @@ def _require_msft_data_config():
if not DATA_CLIENT_ID or not DATA_CLIENT_SECRET or not DATA_REDIRECT_URI:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Microsoft Data OAuth is not configured (Service_MSFT_DATA_*)",
+ detail=routeApiMsg("Microsoft Data OAuth is not configured (Service_MSFT_DATA_*)"),
)
@@ -140,7 +142,7 @@ async def auth_login_callback(
) -> HTMLResponse:
state_data = _parse_oauth_state(state)
if state_data.get("flow") != _FLOW_LOGIN:
- raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
_require_msft_auth_config()
msal_app = msal.ConfidentialClientApplication(
@@ -171,7 +173,7 @@ async def auth_login_callback(
if user_info_response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to get user info from Microsoft",
+ detail=routeApiMsg("Failed to get user info from Microsoft"),
)
user_info = user_info_response.json()
@@ -256,7 +258,7 @@ def auth_connect(
break
if not connection:
raise HTTPException(
- status_code=status.HTTP_404_NOT_FOUND, detail="Microsoft connection not found"
+ status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Microsoft connection not found")
)
msal_app = msal.ConfidentialClientApplication(
@@ -301,11 +303,11 @@ async def auth_connect_callback(
) -> HTMLResponse:
state_data = _parse_oauth_state(state)
if state_data.get("flow") != _FLOW_CONNECT:
- raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
connection_id = state_data.get("connectionId")
user_id = state_data.get("userId")
if not connection_id or not user_id:
- raise HTTPException(status_code=400, detail="Missing connection or user in OAuth state")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Missing connection or user in OAuth state"))
_require_msft_data_config()
msal_app = msal.ConfidentialClientApplication(
@@ -343,7 +345,7 @@ async def auth_connect_callback(
if user_info_response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to get user info from Microsoft",
+ detail=routeApiMsg("Failed to get user info from Microsoft"),
)
user_info = user_info_response.json()
@@ -465,7 +467,7 @@ def adminconsent(request: Request) -> RedirectResponse:
if not redirect_uri:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Could not derive admin consent redirect URI from Service_MSFT_DATA_REDIRECT_URI",
+ detail=routeApiMsg("Could not derive admin consent redirect URI from Service_MSFT_DATA_REDIRECT_URI"),
)
state_jwt = _issue_oauth_state({"flow": "admin_consent"})
scope_param = _msft_data_admin_consent_scope_param()
@@ -528,7 +530,7 @@ def adminconsent_callback(
state_data = _parse_oauth_state(state)
if state_data.get("flow") != "admin_consent":
- raise HTTPException(status_code=400, detail="Invalid OAuth flow for this callback")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid OAuth flow for this callback"))
granted = str(admin_consent or "").strip().lower() in ("true", "1", "yes")
if not granted:
@@ -615,7 +617,7 @@ def logout(
if not token:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="No token found",
+ detail=routeApiMsg("No token found"),
)
try:
@@ -626,7 +628,7 @@ def logout(
logger.error(f"Failed to decode JWT on Microsoft logout: {str(e)}")
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
- detail="Invalid token",
+ detail=routeApiMsg("Invalid token"),
)
revoked = 0
@@ -720,7 +722,7 @@ async def refresh_token(
if not msft_connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="Requested Microsoft connection not found for current user",
+ detail=routeApiMsg("Requested Microsoft connection not found for current user"),
)
else:
for conn in connections:
@@ -730,13 +732,13 @@ async def refresh_token(
if not msft_connection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No Microsoft connection found for current user",
+ detail=routeApiMsg("No Microsoft connection found for current user"),
)
current_token = TokenManager().getFreshToken(msft_connection.id)
if not current_token:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
- detail="No Microsoft token found for this connection",
+ detail=routeApiMsg("No Microsoft token found for this connection"),
)
token_manager = TokenManager()
refreshed_token = token_manager.refreshToken(current_token)
@@ -760,7 +762,7 @@ async def refresh_token(
}
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
- detail="Failed to refresh token",
+ detail=routeApiMsg("Failed to refresh token"),
)
except HTTPException:
raise
diff --git a/modules/routes/routeSharepoint.py b/modules/routes/routeSharepoint.py
index 9bf5b633..4ab80679 100644
--- a/modules/routes/routeSharepoint.py
+++ b/modules/routes/routeSharepoint.py
@@ -13,6 +13,8 @@ from modules.auth import limiter, getCurrentUser
from modules.datamodels.datamodelUam import User, UserConnection
from modules.interfaces.interfaceDbApp import getInterface
from modules.serviceHub import getInterface as getServices
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSharepoint")
logger = logging.getLogger(__name__)
@@ -111,7 +113,7 @@ async def get_sharepoint_sites(
if not services.sharepoint.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Failed to set SharePoint access token. Connection may be expired or invalid."
+ detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
)
# Discover SharePoint sites
@@ -164,7 +166,7 @@ async def list_sharepoint_folders(
if not services.sharepoint.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Failed to set SharePoint access token. Connection may be expired or invalid."
+ detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
)
# Normalize folder path (empty string for root)
@@ -229,7 +231,7 @@ async def getSharepointFolderOptions(
if not services.sharepoint.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Failed to set SharePoint access token. Connection may be expired or invalid."
+ detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
)
# Mode 1: Return sites list if no siteId specified
@@ -343,7 +345,7 @@ async def getSharepointFolderOptionsByReference(
if not services.sharepoint.setAccessTokenFromConnection(connection):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
- detail="Failed to set SharePoint access token. Connection may be expired or invalid."
+ detail=routeApiMsg("Failed to set SharePoint access token. Connection may be expired or invalid.")
)
# Mode 1: Return sites list if no siteId specified
diff --git a/modules/routes/routeStore.py b/modules/routes/routeStore.py
index ab50087c..e8ffac79 100644
--- a/modules/routes/routeStore.py
+++ b/modules/routes/routeStore.py
@@ -23,6 +23,8 @@ from modules.interfaces.interfaceFeatures import getFeatureInterface
from modules.security.rbacCatalog import getCatalogService
from modules.security.rbac import RbacClass
from modules.security.rootAccess import getRootDbAppConnector
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeStore")
logger = logging.getLogger(__name__)
@@ -327,7 +329,7 @@ def activateStoreFeature(
mandateId = data.mandateId
if not _isUserAdminInMandate(db, userId, mandateId):
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Not admin in target mandate")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Not admin in target mandate"))
# ── 1. Resolve subscription & plan ──────────────────────────────
from modules.datamodels.datamodelSubscription import MandateSubscription, BUILTIN_PLANS, SubscriptionStatusEnum
@@ -353,7 +355,7 @@ def activateStoreFeature(
)
raise HTTPException(
status_code=status.HTTP_402_PAYMENT_REQUIRED,
- detail="Kein aktives Abonnement. Bitte zuerst ein Abo abschliessen.",
+ detail=routeApiMsg("Kein aktives Abonnement. Bitte zuerst ein Abo abschliessen."),
)
planKey = operative.get("planKey", "")
@@ -382,7 +384,7 @@ def activateStoreFeature(
)
if not instance:
- raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Failed to create feature instance")
+ raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=routeApiMsg("Failed to create feature instance"))
instanceId = instance.get("id") if isinstance(instance, dict) else instance.id
@@ -460,12 +462,12 @@ def deactivateStoreFeature(
# Verify instance exists in mandate
instances = db.getRecordset(FeatureInstance, recordFilter={"id": instanceId, "mandateId": mandateId})
if not instances:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Feature instance not found in mandate")
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("Feature instance not found in mandate"))
# Find user's FeatureAccess
accesses = db.getRecordset(FeatureAccess, recordFilter={"userId": userId, "featureInstanceId": instanceId})
if not accesses:
- raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="No active access found")
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=routeApiMsg("No active access found"))
featureAccessId = accesses[0].get("id")
db.recordDelete(FeatureAccess, featureAccessId)
diff --git a/modules/routes/routeSubscription.py b/modules/routes/routeSubscription.py
index 3e25ec39..2583316d 100644
--- a/modules/routes/routeSubscription.py
+++ b/modules/routes/routeSubscription.py
@@ -23,6 +23,8 @@ from pydantic import BaseModel, Field
from modules.auth import limiter, getRequestContext, RequestContext
from modules.datamodels.datamodelPagination import PaginationParams, PaginatedResponse, PaginationMetadata, normalize_pagination_dict
from modules.routes.routeDataUsers import _applyFiltersAndSort, _extractDistinctValues
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeSubscription")
logger = logging.getLogger(__name__)
@@ -53,7 +55,7 @@ def _assertMandateAdmin(context: RequestContext, mandateId: str) -> None:
return
except Exception:
pass
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Mandate admin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Mandate admin role required"))
# =============================================================================
@@ -169,7 +171,7 @@ def activatePlan(
)
mandateId = _resolveMandateId(context)
if not mandateId:
- raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
_assertMandateAdmin(context, mandateId)
try:
@@ -195,7 +197,7 @@ def cancelSubscription(
)
mandateId = _resolveMandateId(context)
if not mandateId:
- raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
_assertMandateAdmin(context, mandateId)
try:
@@ -221,7 +223,7 @@ def reactivateSubscription(
)
mandateId = _resolveMandateId(context)
if not mandateId:
- raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
_assertMandateAdmin(context, mandateId)
try:
@@ -243,7 +245,7 @@ def forceCancel(
):
"""Sysadmin: immediately expire any non-terminal subscription."""
if not context.hasSysAdminRole:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sysadmin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Sysadmin role required"))
from modules.serviceCenter.services.serviceSubscription.mainServiceSubscription import (
getService as getSubscriptionService,
@@ -251,7 +253,7 @@ def forceCancel(
from modules.interfaces.interfaceDbSubscription import _getRootInterface as getSubRootInterface
sub = getSubRootInterface().getById(data.subscriptionId)
if not sub:
- raise HTTPException(status_code=404, detail="Subscription not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Subscription not found"))
mandateId = sub["mandateId"]
try:
@@ -278,7 +280,7 @@ def verifyCheckout(
"""
mandateId = _resolveMandateId(context)
if not mandateId:
- raise HTTPException(status_code=400, detail="X-Mandate-Id header required")
+ raise HTTPException(status_code=400, detail=routeApiMsg("X-Mandate-Id header required"))
_assertMandateAdmin(context, mandateId)
try:
@@ -288,7 +290,7 @@ def verifyCheckout(
session = stripeToDict(rawSession)
except Exception as e:
logger.error("Failed to retrieve checkout session %s: %s", data.sessionId, e)
- raise HTTPException(status_code=400, detail="Invalid session ID")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Invalid session ID"))
payStatus = session.get("payment_status")
if session.get("status") != "complete":
@@ -297,7 +299,7 @@ def verifyCheckout(
return {"status": "pending", "message": "Checkout not yet completed"}
if session.get("mode") != "subscription":
- raise HTTPException(status_code=400, detail="Not a subscription checkout session")
+ raise HTTPException(status_code=400, detail=routeApiMsg("Not a subscription checkout session"))
from modules.routes.routeBilling import _handleSubscriptionCheckoutCompleted
@@ -421,7 +423,7 @@ def getAllSubscriptions(
):
"""SysAdmin: list ALL subscriptions across all mandates with enriched metadata."""
if not context.hasSysAdminRole:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sysadmin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Sysadmin role required"))
paginationParams: Optional[PaginationParams] = None
if pagination:
@@ -467,7 +469,7 @@ def getFilterValues(
):
"""Return distinct values for a column, respecting all active filters except the requested one."""
if not context.hasSysAdminRole:
- raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Sysadmin role required")
+ raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=routeApiMsg("Sysadmin role required"))
crossFilterParams: Optional[PaginationParams] = None
if pagination:
diff --git a/modules/routes/routeSystem.py b/modules/routes/routeSystem.py
index 03c58a18..a6d535c1 100644
--- a/modules/routes/routeSystem.py
+++ b/modules/routes/routeSystem.py
@@ -12,7 +12,7 @@ Navigation API Konzept:
import logging
from typing import Dict, List, Any, Optional
-from fastapi import APIRouter, Depends, Request, Query
+from fastapi import APIRouter, Depends, Request
from slowapi import Limiter
from slowapi.util import get_remote_address
@@ -130,11 +130,11 @@ def _getFeatureUiObjects(featureCode: str) -> List[Dict[str, Any]]:
def _buildDynamicBlock(
userId: str,
- language: str,
isSysAdmin: bool
) -> Optional[Dict[str, Any]]:
"""
Build the dynamic features block with mandates, features, and instances.
+ Labels are German base texts (i18n keys). Frontend translates via t().
Returns None if user has no feature instances.
"""
@@ -181,21 +181,29 @@ def _buildDynamicBlock(
if featureKey not in featuresMap:
feature = featureInterface.getFeature(instance.featureCode)
- # Handle featureLabel - could be a dict or a Pydantic model (TextMultilingual)
+ # Handle featureLabel — TextMultilingual dict, plain str (German key), or legacy object
if feature and hasattr(feature, 'label'):
featureLabel = feature.label
- # Convert Pydantic model to dict if needed
if hasattr(featureLabel, 'model_dump'):
featureLabel = featureLabel.model_dump()
+ elif isinstance(featureLabel, str):
+ pass
elif not isinstance(featureLabel, dict):
- # Fallback: try to access as attributes
- featureLabel = {"de": getattr(featureLabel, 'de', instance.featureCode), "en": getattr(featureLabel, 'en', instance.featureCode)}
+ featureLabel = {
+ "de": getattr(featureLabel, 'de', instance.featureCode),
+ "en": getattr(featureLabel, 'en', instance.featureCode),
+ }
else:
featureLabel = {"de": instance.featureCode, "en": instance.featureCode}
+ if isinstance(featureLabel, str):
+ resolvedFeatureLabel = featureLabel
+ else:
+ resolvedFeatureLabel = featureLabel.get("de", featureLabel.get("en", instance.featureCode))
+
featuresMap[featureKey] = {
"uiComponent": f"feature.{instance.featureCode}",
- "uiLabel": featureLabel.get(language, featureLabel.get("en", instance.featureCode)),
+ "uiLabel": resolvedFeatureLabel,
"order": 10,
"instances": [],
"_mandateId": mandateId,
@@ -228,9 +236,8 @@ def _buildDynamicBlock(
# Build path for this view
viewPath = f"/mandates/{mandateId}/{instance.featureCode}/{instance.id}/{viewName}"
- # Get label in requested language
label = uiObj.get("label", {})
- uiLabel = label.get(language, label.get("en", viewName))
+ uiLabel = label.get("de", label.get("en", viewName)) if isinstance(label, dict) else label
views.append({
"uiComponent": f"page.feature.{instance.featureCode}.{viewName}",
@@ -347,7 +354,6 @@ def _getInstanceViewPermissions(
def _filterItems(
items: List[Dict[str, Any]],
- language: str,
isSysAdmin: bool,
roleIds: List[str],
hasGlobalPermission: bool
@@ -361,19 +367,18 @@ def _filterItems(
if item.get("sysAdminOnly") and not isSysAdmin:
continue
if item.get("public"):
- filteredItems.append(_formatBlockItem(item, language))
+ filteredItems.append(_formatBlockItem(item))
continue
if isSysAdmin:
- filteredItems.append(_formatBlockItem(item, language))
+ filteredItems.append(_formatBlockItem(item))
continue
if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]):
- filteredItems.append(_formatBlockItem(item, language))
+ filteredItems.append(_formatBlockItem(item))
filteredItems.sort(key=lambda i: i["order"])
return filteredItems
def _buildStaticBlocks(
- language: str,
isSysAdmin: bool,
roleIds: List[str],
hasGlobalPermission: bool
@@ -381,8 +386,8 @@ def _buildStaticBlocks(
"""
Build static navigation blocks from NAVIGATION_SECTIONS.
- Returns list of blocks with items filtered by permissions.
- Supports subgroups within sections.
+ Labels/titles are plain German strings (i18n base keys).
+ The frontend translates them via t().
"""
blocks = []
@@ -397,12 +402,12 @@ def _buildStaticBlocks(
filteredSubgroups = []
for subgroup in section["subgroups"]:
subItems = _filterItems(
- subgroup.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
+ subgroup.get("items", []), isSysAdmin, roleIds, hasGlobalPermission
)
if subItems:
filteredSubgroups.append({
"id": subgroup["id"],
- "title": subgroup["title"].get(language, subgroup["title"].get("en", subgroup["id"])),
+ "title": subgroup["title"],
"order": subgroup.get("order", 50),
"items": subItems,
})
@@ -412,28 +417,28 @@ def _buildStaticBlocks(
topLevelItems = []
if hasItems:
topLevelItems = _filterItems(
- section["items"], language, isSysAdmin, roleIds, hasGlobalPermission
+ section["items"], isSysAdmin, roleIds, hasGlobalPermission
)
if filteredSubgroups or topLevelItems:
blocks.append({
"type": "static",
"id": section["id"],
- "title": section["title"].get(language, section["title"].get("en", section["id"])),
+ "title": section["title"],
"order": section.get("order", 50),
"items": topLevelItems,
"subgroups": filteredSubgroups,
})
else:
filteredItems = _filterItems(
- section.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission
+ section.get("items", []), isSysAdmin, roleIds, hasGlobalPermission
)
if filteredItems:
blocks.append({
"type": "static",
"id": section["id"],
- "title": section["title"].get(language, section["title"].get("en", section["id"])),
+ "title": section["title"],
"order": section.get("order", 50),
"items": filteredItems,
})
@@ -441,19 +446,19 @@ def _buildStaticBlocks(
return blocks
-def _formatBlockItem(item: Dict[str, Any], language: str) -> Dict[str, Any]:
+def _formatBlockItem(item: Dict[str, Any]) -> Dict[str, Any]:
"""
- Format a navigation item for the new API response.
+ Format a navigation item for the API response.
- Uses new field names: uiComponent, uiLabel, uiPath
- Does NOT include icon (UI maps via uiComponent)
+ Labels are plain German strings (i18n base keys).
+ The frontend translates them via t().
"""
objectKey = item["objectKey"]
uiComponent = _objectKeyToUiComponent(objectKey)
return {
"uiComponent": uiComponent,
- "uiLabel": item["label"].get(language, item["label"].get("en", item["id"])),
+ "uiLabel": item["label"],
"uiPath": item["path"],
"order": item.get("order", 50),
"objectKey": objectKey,
@@ -464,52 +469,15 @@ def _formatBlockItem(item: Dict[str, Any], language: str) -> Dict[str, Any]:
@limiter.limit("60/minute")
def get_navigation(
request: Request,
- language: str = Query("de", description="Language for labels (en, de, fr)"),
reqContext: RequestContext = Depends(getRequestContext)
) -> Dict[str, Any]:
"""
Get unified navigation structure with blocks.
- Single Source of Truth für Navigation - UI rendert nur was es erhält.
+ All labels are German base texts (i18n keys).
+ The frontend translates them via t().
Endpoint: GET /api/navigation
-
- Block order:
- - System (10)
- - Dynamic/Features (15) - only if user has feature instances
- - Workflows (20)
- - Basisdaten (30)
- - Migrate (40)
- - Administration (200)
-
- Response format:
- {
- "language": "de",
- "blocks": [
- {
- "type": "static",
- "id": "system",
- "title": "SYSTEM",
- "order": 10,
- "items": [
- {
- "uiComponent": "page.system.home",
- "uiLabel": "Übersicht",
- "uiPath": "/",
- "order": 10,
- "objectKey": "ui.system.home"
- }
- ]
- },
- {
- "type": "dynamic",
- "id": "features",
- "title": "MEINE FEATURES",
- "order": 15,
- "mandates": [...]
- }
- ]
- }
"""
try:
isSysAdmin = reqContext.hasSysAdminRole
@@ -526,11 +494,11 @@ def get_navigation(
hasGlobalPermission = _checkUiPermission(roleIds, "_global_check")
# Build static blocks from NAVIGATION_SECTIONS
- blocks = _buildStaticBlocks(language, isSysAdmin, roleIds, hasGlobalPermission)
+ blocks = _buildStaticBlocks(isSysAdmin, roleIds, hasGlobalPermission)
# Build dynamic block (features) if user has feature instances
if userId:
- dynamicBlock = _buildDynamicBlock(userId, language, isSysAdmin)
+ dynamicBlock = _buildDynamicBlock(userId, isSysAdmin)
if dynamicBlock:
blocks.append(dynamicBlock)
@@ -538,14 +506,12 @@ def get_navigation(
blocks.sort(key=lambda b: b["order"])
return {
- "language": language,
"blocks": blocks,
}
except Exception as e:
logger.error(f"Error getting navigation: {e}")
return {
- "language": language,
"blocks": [],
"error": str(e),
}
diff --git a/modules/routes/routeVoiceGoogle.py b/modules/routes/routeVoiceGoogle.py
index 309e59bb..6c5d99e4 100644
--- a/modules/routes/routeVoiceGoogle.py
+++ b/modules/routes/routeVoiceGoogle.py
@@ -18,6 +18,8 @@ from typing import Optional, Dict, Any, List
from modules.auth import getCurrentUser, getRequestContext, RequestContext, limiter
from modules.datamodels.datamodelUam import User
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface, VoiceObjects
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeVoiceGoogle")
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/voice-google", tags=["Voice Google"])
@@ -132,7 +134,7 @@ async def detect_language(
if not text.strip():
raise HTTPException(
status_code=400,
- detail="Empty text provided for language detection"
+ detail=routeApiMsg("Empty text provided for language detection")
)
# Get voice interface
@@ -176,7 +178,7 @@ async def translate_text(
if not text.strip():
raise HTTPException(
status_code=400,
- detail="Empty text provided for translation"
+ detail=routeApiMsg("Empty text provided for translation")
)
# Get voice interface
@@ -306,7 +308,7 @@ async def text_to_speech(
if not text.strip():
raise HTTPException(
status_code=400,
- detail="Empty text provided for text-to-speech"
+ detail=routeApiMsg("Empty text provided for text-to-speech")
)
mandateId = str(getattr(context, "mandateId", "") or "")
diff --git a/modules/routes/routeVoiceUser.py b/modules/routes/routeVoiceUser.py
index 2f21662b..a3c3fda7 100644
--- a/modules/routes/routeVoiceUser.py
+++ b/modules/routes/routeVoiceUser.py
@@ -17,6 +17,8 @@ from modules.auth import getCurrentUser, limiter
from modules.datamodels.datamodelUam import User, UserVoicePreferences, _normalizeTtsVoiceMap
from modules.interfaces.interfaceDbApp import getRootInterface
from modules.interfaces.interfaceVoiceObjects import getVoiceInterface
+from modules.shared.i18nRegistry import apiRouteContext
+routeApiMsg = apiRouteContext("routeVoiceUser")
logger = logging.getLogger(__name__)
@@ -176,7 +178,7 @@ def _resolveMandateIdForVoiceTestAi(request: Request, currentUser: User) -> str:
if headerRaw not in memberIds:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
- detail="X-Mandate-Id is not a mandate you belong to.",
+ detail=routeApiMsg("X-Mandate-Id is not a mandate you belong to."),
)
if _mandatePassesAiPoolBilling(currentUser, headerRaw, userId):
logger.info(
@@ -294,7 +296,7 @@ async def _generateTtsSampleTextForLocale(
logger.warning("Voice test AI sample empty or errorCount=%s", getattr(response, "errorCount", None))
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
- detail="Could not generate voice test sample text.",
+ detail=routeApiMsg("Could not generate voice test sample text."),
)
if len(content) > 500:
content = content[:500].rstrip()
diff --git a/modules/routes/routeWorkflowDashboard.py b/modules/routes/routeWorkflowDashboard.py
index 687f4206..c0c46a13 100644
--- a/modules/routes/routeWorkflowDashboard.py
+++ b/modules/routes/routeWorkflowDashboard.py
@@ -23,6 +23,9 @@ from modules.datamodels.datamodelPagination import PaginationParams
from modules.features.graphicalEditor.datamodelFeatureGraphicalEditor import (
AutoRun, AutoStepLog, AutoWorkflow, AutoTask,
)
+from modules.shared.i18nRegistry import apiRouteContext
+
+routeApiMsg = apiRouteContext("routeWorkflowDashboard")
logger = logging.getLogger(__name__)
limiter = Limiter(key_func=get_remote_address)
@@ -239,11 +242,11 @@ def get_run_steps(
"""Get step logs for a specific run (with access check)."""
db = _getDb()
if not db._ensureTableExists(AutoRun):
- raise HTTPException(status_code=404, detail="Run not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
runs = db.getRecordset(AutoRun, recordFilter={"id": runId})
if not runs:
- raise HTTPException(status_code=404, detail="Run not found")
+ raise HTTPException(status_code=404, detail=routeApiMsg("Run not found"))
run = dict(runs[0])
if not context.hasSysAdminRole:
@@ -256,7 +259,7 @@ def get_run_steps(
elif runMandate and userId and _isUserMandateAdmin(userId, runMandate):
pass
else:
- raise HTTPException(status_code=403, detail="Access denied")
+ raise HTTPException(status_code=403, detail=routeApiMsg("Access denied"))
if not db._ensureTableExists(AutoStepLog):
return {"steps": []}
diff --git a/modules/security/rbacCatalog.py b/modules/security/rbacCatalog.py
index 14b87534..587f6fbd 100644
--- a/modules/security/rbacCatalog.py
+++ b/modules/security/rbacCatalog.py
@@ -8,7 +8,7 @@ Feature-Container register their RBAC objects via mainXxx.py at startup.
"""
import logging
-from typing import Dict, List, Any, Optional
+from typing import Dict, List, Any, Optional, Union
from threading import Lock
logger = logging.getLogger(__name__)
@@ -43,7 +43,7 @@ class RbacCatalogService:
self._initialized = True
logger.info("RBAC Catalog Service initialized")
- def registerUiObject(self, featureCode: str, objectKey: str, label: Dict[str, str], meta: Optional[Dict[str, Any]] = None) -> bool:
+ def registerUiObject(self, featureCode: str, objectKey: str, label: Union[str, Dict[str, str]], meta: Optional[Dict[str, Any]] = None) -> bool:
"""Register a UI object for a feature."""
try:
self._uiObjects[objectKey] = {"objectKey": objectKey, "featureCode": featureCode, "label": label, "meta": meta or {}, "type": "UI"}
@@ -84,7 +84,7 @@ class RbacCatalogService:
logger.error(f"Failed to register DATA object {objectKey}: {e}")
return False
- def registerFeatureDefinition(self, featureCode: str, label: Dict[str, str], icon: str) -> bool:
+ def registerFeatureDefinition(self, featureCode: str, label: Union[str, Dict[str, str]], icon: str) -> bool:
"""Register a feature definition."""
try:
self._featureDefinitions[featureCode] = {"code": featureCode, "label": label, "icon": icon}
diff --git a/modules/serviceCenter/registry.py b/modules/serviceCenter/registry.py
index 851e4894..64003d29 100644
--- a/modules/serviceCenter/registry.py
+++ b/modules/serviceCenter/registry.py
@@ -33,98 +33,98 @@ IMPORTABLE_SERVICES: Dict[str, Dict[str, Any]] = {
"class": "TicketService",
"dependencies": [],
"objectKey": "service.ticket",
- "label": {"en": "Ticket System", "de": "Ticket-System", "fr": "Système de tickets"},
+ "label": "Ticket-System",
},
"messaging": {
"module": "modules.serviceCenter.services.serviceMessaging.mainServiceMessaging",
"class": "MessagingService",
"dependencies": [],
"objectKey": "service.messaging",
- "label": {"en": "Messaging", "de": "Nachrichten", "fr": "Messagerie"},
+ "label": "Nachrichten",
},
"billing": {
"module": "modules.serviceCenter.services.serviceBilling.mainServiceBilling",
"class": "BillingService",
"dependencies": ["subscription"],
"objectKey": "service.billing",
- "label": {"en": "Billing", "de": "Abrechnung", "fr": "Facturation"},
+ "label": "Abrechnung",
},
"subscription": {
"module": "modules.serviceCenter.services.serviceSubscription.mainServiceSubscription",
"class": "SubscriptionService",
"dependencies": [],
"objectKey": "service.subscription",
- "label": {"en": "Subscription", "de": "Abonnement", "fr": "Abonnement"},
+ "label": "Abonnement",
},
"sharepoint": {
"module": "modules.serviceCenter.services.serviceSharepoint.mainServiceSharepoint",
"class": "SharepointService",
"dependencies": ["security"],
"objectKey": "service.sharepoint",
- "label": {"en": "SharePoint", "de": "SharePoint", "fr": "SharePoint"},
+ "label": "SharePoint",
},
"clickup": {
"module": "modules.serviceCenter.services.serviceClickup.mainServiceClickup",
"class": "ClickupService",
"dependencies": ["security"],
"objectKey": "service.clickup",
- "label": {"en": "ClickUp", "de": "ClickUp", "fr": "ClickUp"},
+ "label": "ClickUp",
},
"chat": {
"module": "modules.serviceCenter.services.serviceChat.mainServiceChat",
"class": "ChatService",
"dependencies": ["utils"],
"objectKey": "service.chat",
- "label": {"en": "Chat", "de": "Chat", "fr": "Chat"},
+ "label": "Chat",
},
"extraction": {
"module": "modules.serviceCenter.services.serviceExtraction.mainServiceExtraction",
"class": "ExtractionService",
"dependencies": ["chat", "utils"],
"objectKey": "service.extraction",
- "label": {"en": "Extraction", "de": "Extraktion", "fr": "Extraction"},
+ "label": "Extraktion",
},
"generation": {
"module": "modules.serviceCenter.services.serviceGeneration.mainServiceGeneration",
"class": "GenerationService",
"dependencies": ["utils", "chat"],
"objectKey": "service.generation",
- "label": {"en": "Generation", "de": "Generierung", "fr": "Génération"},
+ "label": "Generierung",
},
"ai": {
"module": "modules.serviceCenter.services.serviceAi.mainServiceAi",
"class": "AiService",
"dependencies": ["chat", "utils", "extraction", "billing"],
"objectKey": "service.ai",
- "label": {"en": "AI", "de": "KI", "fr": "IA"},
+ "label": "KI",
},
"web": {
"module": "modules.serviceCenter.services.serviceWeb.mainServiceWeb",
"class": "WebService",
"dependencies": ["ai", "chat", "utils"],
"objectKey": "service.web",
- "label": {"en": "Web Research", "de": "Web-Recherche", "fr": "Recherche Web"},
+ "label": "Web-Recherche",
},
"neutralization": {
"module": "modules.features.neutralization.serviceNeutralization.mainServiceNeutralization",
"class": "NeutralizationService",
"dependencies": ["extraction", "generation"],
"objectKey": "service.neutralization",
- "label": {"en": "Neutralization", "de": "Neutralisierung", "fr": "Neutralisation"},
+ "label": "Neutralisierung",
},
"agent": {
"module": "modules.serviceCenter.services.serviceAgent.mainServiceAgent",
"class": "AgentService",
"dependencies": ["ai", "chat", "utils", "extraction", "billing", "streaming", "knowledge"],
"objectKey": "service.agent",
- "label": {"en": "Agent", "de": "Agent", "fr": "Agent"},
+ "label": "Agent",
},
"knowledge": {
"module": "modules.serviceCenter.services.serviceKnowledge.mainServiceKnowledge",
"class": "KnowledgeService",
"dependencies": ["ai"],
"objectKey": "service.knowledge",
- "label": {"en": "Knowledge Store", "de": "Wissensspeicher", "fr": "Base de connaissances"},
+ "label": "Wissensspeicher",
},
}
diff --git a/modules/serviceCenter/services/serviceAi/subStructureFilling.py b/modules/serviceCenter/services/serviceAi/subStructureFilling.py
index 6ba32dfd..3795f44d 100644
--- a/modules/serviceCenter/services/serviceAi/subStructureFilling.py
+++ b/modules/serviceCenter/services/serviceAi/subStructureFilling.py
@@ -18,6 +18,12 @@ from modules.datamodels.datamodelExtraction import ContentPart
from modules.datamodels.datamodelAi import AiCallRequest, AiCallOptions, OperationTypeEnum, PriorityEnum, ProcessingModeEnum
from modules.workflows.processing.shared.stateTools import checkWorkflowStopped
+
+class _AiResponseFallback:
+ """Lightweight wrapper used when AI JSON parsing fails but raw content must be preserved."""
+ def __init__(self, content):
+ self.content = content
+
logger = logging.getLogger(__name__)
@@ -719,12 +725,8 @@ class StructureFiller:
self.services.chat.progressLogUpdate(sectionOperationId, 0.8, "Validating generated content")
- class _AiResponse:
- def __init__(self, content):
- self.content = content
-
responseElements = await self._processAiResponseForSection(
- aiResponse=_AiResponse(aiResponseJson),
+ aiResponse=_AiResponseFallback(aiResponseJson),
contentType=contentType,
operationType=operationType,
sectionId=sectionId,
@@ -1032,17 +1034,10 @@ class StructureFiller:
else:
generatedElements = []
- class AiResponse:
- def __init__(self, content):
- self.content = content
-
- aiResponse = AiResponse(aiResponseJson)
+ aiResponse = _AiResponseFallback(aiResponseJson)
except Exception as parseError:
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
- class AiResponse:
- def __init__(self, content):
- self.content = content
- aiResponse = AiResponse(aiResponseJson)
+ aiResponse = _AiResponseFallback(aiResponseJson)
generatedElements = []
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
@@ -1200,17 +1195,10 @@ class StructureFiller:
else:
generatedElements = []
- class AiResponse:
- def __init__(self, content):
- self.content = content
-
- aiResponse = AiResponse(aiResponseJson)
+ aiResponse = _AiResponseFallback(aiResponseJson)
except Exception as parseError:
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
- class AiResponse:
- def __init__(self, content):
- self.content = content
- aiResponse = AiResponse(aiResponseJson)
+ aiResponse = _AiResponseFallback(aiResponseJson)
generatedElements = []
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
@@ -1467,17 +1455,10 @@ class StructureFiller:
else:
generatedElements = []
- class AiResponse:
- def __init__(self, content):
- self.content = content
-
- aiResponse = AiResponse(aiResponseJson)
+ aiResponse = _AiResponseFallback(aiResponseJson)
except Exception as parseError:
logger.error(f"Error parsing response from _callAiWithLooping for section {sectionId}: {str(parseError)}")
- class AiResponse:
- def __init__(self, content):
- self.content = content
- aiResponse = AiResponse(aiResponseJson)
+ aiResponse = _AiResponseFallback(aiResponseJson)
generatedElements = []
self.services.chat.progressLogUpdate(sectionOperationId, 0.6, "Processing AI response")
diff --git a/modules/shared/attributeUtils.py b/modules/shared/attributeUtils.py
index 239e214d..ea92c0b8 100644
--- a/modules/shared/attributeUtils.py
+++ b/modules/shared/attributeUtils.py
@@ -36,42 +36,44 @@ class AttributeDefinition(BaseModel):
placeholder: Optional[str] = None
-# Global registry for model labels
-MODEL_LABELS: Dict[str, Dict[str, Dict[str, str]]] = {}
-
-
-def registerModelLabels(modelName: str, modelLabel: Dict[str, str], labels: Dict[str, Dict[str, str]]):
- """
- Register labels for a model's attributes and the model itself.
-
- Args:
- modelName: Name of the model class
- modelLabel: Dictionary mapping language codes to model labels
- e.g. {"en": "Prompt", "fr": "Invite"}
- labels: Dictionary mapping attribute names to their translations
- e.g. {"name": {"en": "Name", "fr": "Nom"}}
- """
- MODEL_LABELS[modelName] = {"model": modelLabel, "attributes": labels}
+def _getModelLabelEntry(modelName: str) -> Dict[str, Any]:
+ """Resolve label data produced by @i18nModel (see modules.shared.i18nRegistry.MODEL_LABELS)."""
+ try:
+ from modules.shared.i18nRegistry import MODEL_LABELS as i18nModelLabels
+ except ImportError:
+ return {}
+ return i18nModelLabels.get(modelName) or {}
def getModelLabels(modelName: str, language: str = "en") -> Dict[str, str]:
- """
- Get labels for a model's attributes in the specified language.
+ """Get labels for a model's attributes in the specified language.
- Args:
- modelName: Name of the model class
- language: Language code (default: "en")
-
- Returns:
- Dictionary mapping attribute names to their labels in the specified language
+ Reads @i18nModel registration (German base strings); non-German languages use the i18n cache.
+ Attribute values are strings; dict-shaped entries are still accepted for unusual callers.
"""
- modelData = MODEL_LABELS.get(modelName, {})
+ modelData = _getModelLabelEntry(modelName)
attributeLabels = modelData.get("attributes", {})
- return {
- attr: translations.get(language, translations.get("en", attr))
- for attr, translations in attributeLabels.items()
- }
+ result: Dict[str, str] = {}
+ for attr, translations in attributeLabels.items():
+ if isinstance(translations, dict):
+ result[attr] = translations.get(language, translations.get("en", attr))
+ elif isinstance(translations, str):
+ result[attr] = _resolveLabel(translations, language)
+ else:
+ result[attr] = attr
+ return result
+
+
+def _resolveLabel(germanText: str, language: str) -> str:
+ """Resolve a German base label to the requested language via i18n cache."""
+ if language == "de":
+ return germanText
+ try:
+ from modules.shared.i18nRegistry import _CACHE
+ return _CACHE.get(language, {}).get(germanText, germanText)
+ except ImportError:
+ return germanText
def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Dict[str, str]:
@@ -87,19 +89,14 @@ def _mergedAttributeLabels(modelClass: Type[BaseModel], userLanguage: str) -> Di
def getModelLabel(modelName: str, language: str = "en") -> str:
- """
- Get the label for a model in the specified language.
-
- Args:
- modelName: Name of the model class
- language: Language code (default: "en")
-
- Returns:
- Model label in the specified language, or model name if no label exists
- """
- modelData = MODEL_LABELS.get(modelName, {})
+ """Get the label for a model in the specified language (see getModelLabels)."""
+ modelData = _getModelLabelEntry(modelName)
modelLabel = modelData.get("model", {})
- return modelLabel.get(language, modelLabel.get("en", modelName))
+ if isinstance(modelLabel, dict):
+ return modelLabel.get(language, modelLabel.get("en", modelName))
+ elif isinstance(modelLabel, str):
+ return _resolveLabel(modelLabel, language)
+ return modelName
def getModelAttributeDefinitions(modelClass: Type[BaseModel] = None, userLanguage: str = "en") -> Dict[str, Any]:
diff --git a/modules/shared/frontendTypes.py b/modules/shared/frontendTypes.py
index ab3e6939..1a80aa40 100644
--- a/modules/shared/frontendTypes.py
+++ b/modules/shared/frontendTypes.py
@@ -137,6 +137,41 @@ CUSTOM_TYPE_DESCRIPTIONS: Dict[FrontendType, Dict[str, str]] = {
"fr": "Tâche ClickUp",
"de": "ClickUp-Aufgabe"
},
+ FrontendType.CASE_LIST: {
+ "en": "Case List",
+ "fr": "Liste de cas",
+ "de": "Fallunterscheidung"
+ },
+ FrontendType.FIELD_BUILDER: {
+ "en": "Field Builder",
+ "fr": "Constructeur de champs",
+ "de": "Feld-Editor"
+ },
+ FrontendType.KEY_VALUE_ROWS: {
+ "en": "Key-Value Rows",
+ "fr": "Lignes clé-valeur",
+ "de": "Schlüssel-Wert-Zeilen"
+ },
+ FrontendType.CRON: {
+ "en": "Cron Expression",
+ "fr": "Expression cron",
+ "de": "Cron-Ausdruck"
+ },
+ FrontendType.CONDITION: {
+ "en": "Condition",
+ "fr": "Condition",
+ "de": "Bedingung"
+ },
+ FrontendType.MAPPING_TABLE: {
+ "en": "Mapping Table",
+ "fr": "Table de correspondance",
+ "de": "Zuordnungstabelle"
+ },
+ FrontendType.FILTER_EXPRESSION: {
+ "en": "Filter Expression",
+ "fr": "Expression de filtre",
+ "de": "Filterausdruck"
+ },
}
diff --git a/modules/shared/i18nRegistry.py b/modules/shared/i18nRegistry.py
new file mode 100644
index 00000000..c44a65b1
--- /dev/null
+++ b/modules/shared/i18nRegistry.py
@@ -0,0 +1,666 @@
+# Copyright (c) 2025 Patrick Motsch
+# All rights reserved.
+"""
+Gateway i18n registry: t(), @i18nModel, boot-sync, in-memory cache.
+
+All UI-visible texts in the gateway (HTTPException details, model labels,
+API messages) are tagged with t() and registered at import time.
+At boot, the registry is synced to the xx base set in the DB.
+At runtime, t() returns the cached translation for the current request language.
+"""
+
+from __future__ import annotations
+
+import logging
+from contextvars import ContextVar
+from dataclasses import dataclass, field as dataclass_field
+from typing import Any, Dict, List, Optional, Type
+
+from pydantic import BaseModel
+
+logger = logging.getLogger(__name__)
+
+# ---------------------------------------------------------------------------
+# Registry (populated at import time by t() and @i18nModel)
+# ---------------------------------------------------------------------------
+
+@dataclass
+class _I18nRegistryEntry:
+ context: str
+ value: str
+
+
+_REGISTRY: Dict[str, _I18nRegistryEntry] = {}
+
+# ---------------------------------------------------------------------------
+# Translation cache (populated at boot by _loadCache)
+# ---------------------------------------------------------------------------
+
+_CACHE: Dict[str, Dict[str, str]] = {}
+
+# ---------------------------------------------------------------------------
+# Per-request language (set by middleware)
+# ---------------------------------------------------------------------------
+
+_CURRENT_LANGUAGE: ContextVar[str] = ContextVar("i18n_lang", default="de")
+
+# ---------------------------------------------------------------------------
+# Model labels (backwards-compatible with getModelLabels / getModelLabel)
+# ---------------------------------------------------------------------------
+
+MODEL_LABELS: Dict[str, Dict[str, Any]] = {}
+
+
+# ---------------------------------------------------------------------------
+# t() -- tag and translate
+# ---------------------------------------------------------------------------
+
+def t(key: str, context: str = "api", value: str = "") -> str:
+ """Tag a UI-visible text for i18n and return the translation.
+
+ At import time: registers the key with context and AI description.
+ At runtime: returns the cached translation for _CURRENT_LANGUAGE.
+ Falls back to the key itself (German base text) if no translation found.
+ """
+ if key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context=context, value=value)
+ lang = _CURRENT_LANGUAGE.get()
+ if lang == "de":
+ return key
+ return _CACHE.get(lang, {}).get(key, key)
+
+
+def apiRouteContext(routeModuleName: str):
+ """Return a callable that registers + translates HTTPException details.
+
+ The key is registered eagerly in ``_REGISTRY`` the moment ``_apiMsg(key)``
+ is evaluated (module-level ``detail=routeApiMsg("…")`` runs at import time).
+ At runtime ``t()`` returns the cached translation for the current language.
+ """
+ _ctx = f"api.{routeModuleName}"
+
+ def _apiMsg(key: str, value: str = "") -> str:
+ if key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context=_ctx, value=value)
+ return t(key, _ctx, value)
+ return _apiMsg
+
+
+# ---------------------------------------------------------------------------
+# @i18nModel -- class decorator for Pydantic models
+# ---------------------------------------------------------------------------
+
+def i18nModel(modelLabel: str, aiContext: str = ""):
+ """Class decorator: registers model and field labels for i18n.
+
+ 1. Registers t(modelLabel, "table.", aiContext or docstring)
+ 2. For each Field with json_schema_extra["label"]:
+ Registers t(label, "table..", field.description)
+ 3. Populates MODEL_LABELS for getModelLabels()/getModelLabel() in attributeUtils
+ """
+ def _decorator(cls: Type[BaseModel]) -> Type[BaseModel]:
+ className = cls.__name__
+ ctx = aiContext or _extractDocstringFirstLine(cls)
+ t(modelLabel, f"table.{className}", ctx)
+
+ attributes: Dict[str, str] = {}
+ for fieldName, fieldInfo in cls.model_fields.items():
+ extra = fieldInfo.json_schema_extra
+ if not isinstance(extra, dict):
+ continue
+ label = extra.get("label")
+ if label:
+ desc = fieldInfo.description or ""
+ t(label, f"table.{className}.{fieldName}", desc)
+ attributes[fieldName] = label
+ else:
+ attributes[fieldName] = fieldName
+
+ MODEL_LABELS[className] = {
+ "model": modelLabel,
+ "attributes": attributes,
+ }
+ return cls
+ return _decorator
+
+
+def _extractDocstringFirstLine(cls: type) -> str:
+ doc = cls.__doc__
+ if not doc:
+ return ""
+ return doc.strip().split("\n")[0].strip()
+
+
+# ---------------------------------------------------------------------------
+# Language setter (called by middleware)
+# ---------------------------------------------------------------------------
+
+def _setLanguage(lang: str):
+ """Set the language for the current request context."""
+ _CURRENT_LANGUAGE.set(lang)
+
+
+def _getLanguage() -> str:
+ """Get the language for the current request context."""
+ return _CURRENT_LANGUAGE.get()
+
+
+# ---------------------------------------------------------------------------
+# Boot: scan route files for routeApiMsg("…") calls → register eagerly
+# ---------------------------------------------------------------------------
+
+_ROUTE_API_MSG_RE = None # compiled lazily
+
+def _scanRouteApiMsgKeys():
+ """Scan all gateway route/feature Python files for routeApiMsg("…") calls
+ and register the keys in _REGISTRY so they appear in the boot DB sync.
+ """
+ import re
+ from pathlib import Path
+
+ global _ROUTE_API_MSG_RE
+ if _ROUTE_API_MSG_RE is None:
+ _ROUTE_API_MSG_RE = re.compile(
+ r"""routeApiMsg\(\s*(['"])((?:\\.|(?!\1).)+)\1""",
+ )
+
+ gatewayRoot = Path(__file__).resolve().parents[1]
+ scanDirs = [gatewayRoot / "routes", gatewayRoot / "features"]
+
+ _ctxRe = re.compile(r'''apiRouteContext\(\s*['"]([^'"]+)['"]\s*\)''')
+
+ for scanDir in scanDirs:
+ if not scanDir.is_dir():
+ continue
+ for pyFile in scanDir.rglob("*.py"):
+ try:
+ src = pyFile.read_text(encoding="utf-8", errors="replace")
+ except OSError:
+ continue
+ ctxMatch = _ctxRe.search(src)
+ if not ctxMatch:
+ continue
+ ctx = f"api.{ctxMatch.group(1)}"
+ for m in _ROUTE_API_MSG_RE.finditer(src):
+ key = m.group(2).replace("\\'", "'").replace('\\"', '"')
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context=ctx, value="")
+
+ logger.info("i18n route scan: %d api.* keys in registry after scan",
+ sum(1 for e in _REGISTRY.values() if e.context.startswith("api.")))
+
+
+def _registerNavLabels():
+ """Register all navigation labels from NAVIGATION_SECTIONS as i18n keys.
+
+ Called at boot before DB sync so that nav labels appear in the xx base set
+ and can be translated via the Admin UI.
+ """
+ try:
+ from modules.system.mainSystem import NAVIGATION_SECTIONS
+ except ImportError:
+ logger.warning("i18n: could not import NAVIGATION_SECTIONS for nav label registration")
+ return
+
+ count = 0
+ for section in NAVIGATION_SECTIONS:
+ title = section.get("title", "")
+ if title and title not in _REGISTRY:
+ _REGISTRY[title] = _I18nRegistryEntry(context="nav", value="")
+ count += 1
+
+ for item in section.get("items", []):
+ label = item.get("label", "")
+ if label and label not in _REGISTRY:
+ _REGISTRY[label] = _I18nRegistryEntry(context="nav", value="")
+ count += 1
+
+ for subgroup in section.get("subgroups", []):
+ sgTitle = subgroup.get("title", "")
+ if sgTitle and sgTitle not in _REGISTRY:
+ _REGISTRY[sgTitle] = _I18nRegistryEntry(context="nav", value="")
+ count += 1
+ for item in subgroup.get("items", []):
+ label = item.get("label", "")
+ if label and label not in _REGISTRY:
+ _REGISTRY[label] = _I18nRegistryEntry(context="nav", value="")
+ count += 1
+
+ logger.info("i18n nav labels: registered %d nav keys", count)
+
+
+def _registerFeatureUiLabels():
+ """Register FEATURE_LABEL and UI_OBJECTS labels from all feature modules (German i18n keys)."""
+ try:
+ from modules.system import mainSystem as _mainSystem
+ _fl = getattr(_mainSystem, "FEATURE_LABEL", None)
+ if isinstance(_fl, str) and _fl and _fl not in _REGISTRY:
+ _REGISTRY[_fl] = _I18nRegistryEntry(context="nav", value="")
+ except ImportError:
+ pass
+
+ _featureModulePaths = (
+ "modules.features.trustee.mainTrustee",
+ "modules.features.graphicalEditor.mainGraphicalEditor",
+ "modules.features.commcoach.mainCommcoach",
+ "modules.features.teamsbot.mainTeamsbot",
+ "modules.features.workspace.mainWorkspace",
+ "modules.features.realEstate.mainRealEstate",
+ "modules.features.neutralization.mainNeutralization",
+ "modules.features.chatbot.mainChatbot",
+ )
+ added = 0
+ for modPath in _featureModulePaths:
+ try:
+ mod = __import__(modPath, fromlist=["FEATURE_LABEL", "UI_OBJECTS"])
+ except ImportError:
+ continue
+ fl = getattr(mod, "FEATURE_LABEL", None)
+ if isinstance(fl, str) and fl and fl not in _REGISTRY:
+ _REGISTRY[fl] = _I18nRegistryEntry(context="nav", value="")
+ added += 1
+ for uiObj in getattr(mod, "UI_OBJECTS", []) or []:
+ lab = uiObj.get("label")
+ if isinstance(lab, str) and lab and lab not in _REGISTRY:
+ _REGISTRY[lab] = _I18nRegistryEntry(context="nav", value="")
+ added += 1
+ elif isinstance(lab, dict):
+ base = lab.get("de") or lab.get("en")
+ if base and base not in _REGISTRY:
+ _REGISTRY[base] = _I18nRegistryEntry(context="nav", value="")
+ added += 1
+ logger.info("i18n feature UI labels: %d new keys (nav context)", added)
+
+
+def _registerRbacLabels():
+ """Register DATA_OBJECTS, RESOURCE_OBJECTS labels and TEMPLATE_ROLES descriptions
+ from all feature modules and system module as i18n keys.
+
+ context mapping:
+ - DATA_OBJECTS → rbac.data
+ - RESOURCE_OBJECTS → rbac.resource
+ - TEMPLATE_ROLES[].description (de) → rbac.role
+ - QUICK_ACTIONS[].label/description (de) → rbac.quickaction
+ - QUICK_ACTION_CATEGORIES[].label (de) → rbac.quickaction
+ """
+ _systemModule = "modules.system.mainSystem"
+ _featureModulePaths = (
+ _systemModule,
+ "modules.features.trustee.mainTrustee",
+ "modules.features.graphicalEditor.mainGraphicalEditor",
+ "modules.features.commcoach.mainCommcoach",
+ "modules.features.teamsbot.mainTeamsbot",
+ "modules.features.workspace.mainWorkspace",
+ "modules.features.realEstate.mainRealEstate",
+ "modules.features.neutralization.mainNeutralization",
+ "modules.features.chatbot.mainChatbot",
+ )
+
+ def _extractDe(obj) -> str:
+ if isinstance(obj, str):
+ return obj
+ if isinstance(obj, dict):
+ return obj.get("de") or obj.get("en") or ""
+ return ""
+
+ added = 0
+ for modPath in _featureModulePaths:
+ try:
+ mod = __import__(modPath, fromlist=[
+ "DATA_OBJECTS", "RESOURCE_OBJECTS", "TEMPLATE_ROLES",
+ "QUICK_ACTIONS", "QUICK_ACTION_CATEGORIES",
+ ])
+ except ImportError:
+ continue
+
+ for dataObj in getattr(mod, "DATA_OBJECTS", []) or []:
+ key = _extractDe(dataObj.get("label"))
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context="rbac.data", value="")
+ added += 1
+
+ for resObj in getattr(mod, "RESOURCE_OBJECTS", []) or []:
+ key = _extractDe(resObj.get("label"))
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context="rbac.resource", value="")
+ added += 1
+
+ for role in getattr(mod, "TEMPLATE_ROLES", []) or []:
+ key = _extractDe(role.get("description"))
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context="rbac.role", value="")
+ added += 1
+
+ for qa in getattr(mod, "QUICK_ACTIONS", []) or []:
+ for field in ("label", "description"):
+ key = _extractDe(qa.get(field))
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context="rbac.quickaction", value="")
+ added += 1
+
+ for cat in getattr(mod, "QUICK_ACTION_CATEGORIES", []) or []:
+ key = _extractDe(cat.get("label"))
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context="rbac.quickaction", value="")
+ added += 1
+
+ logger.info("i18n rbac labels: %d new keys (rbac.* context)", added)
+
+
+def _registerServiceCenterLabels():
+ """Register service-center category labels and bootstrap role descriptions."""
+ added = 0
+
+ def _extractDe(obj) -> str:
+ if isinstance(obj, str):
+ return obj
+ if isinstance(obj, dict):
+ return obj.get("de") or obj.get("en") or ""
+ return ""
+
+ try:
+ from modules.serviceCenter.registry import IMPORTABLE_SERVICES
+ for svc in IMPORTABLE_SERVICES.values():
+ key = _extractDe(svc.get("label"))
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context="service", value="")
+ added += 1
+ except ImportError:
+ pass
+
+ _bootstrapRoleDescriptions = [
+ "Administrator - Benutzer und Ressourcen im Mandanten verwalten",
+ "Benutzer - Standard-Benutzer mit Zugriff auf eigene Datensätze",
+ "Betrachter - Nur-Lese-Zugriff auf Gruppen-Datensätze",
+ "System-Administrator - Vollständiger administrativer Zugriff über alle Mandanten",
+ ]
+ for desc in _bootstrapRoleDescriptions:
+ if desc not in _REGISTRY:
+ _REGISTRY[desc] = _I18nRegistryEntry(context="rbac.role", value="")
+ added += 1
+
+ logger.info("i18n service/bootstrap labels: %d new keys", added)
+
+
+def _registerNodeLabels():
+ """Register all graph-editor node labels, descriptions, parameter descriptions,
+ output labels, port descriptions, category labels, and entry-point titles."""
+ added = 0
+
+ def _extractDe(obj) -> str:
+ if isinstance(obj, str):
+ return obj
+ if isinstance(obj, dict):
+ return obj.get("de") or obj.get("en") or ""
+ return ""
+
+ def _reg(key: str, ctx: str):
+ nonlocal added
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context=ctx, value="")
+ added += 1
+
+ try:
+ from modules.features.graphicalEditor.nodeDefinitions import STATIC_NODE_TYPES
+ for nd in STATIC_NODE_TYPES:
+ _reg(_extractDe(nd.get("label")), "node.label")
+ _reg(_extractDe(nd.get("description")), "node.desc")
+
+ for param in nd.get("parameters", []) or []:
+ _reg(_extractDe(param.get("description")), "node.param")
+ _reg(_extractDe(param.get("label")), "node.param")
+
+ outLabels = nd.get("outputLabels")
+ if isinstance(outLabels, dict):
+ deList = outLabels.get("de") or outLabels.get("en") or []
+ for lbl in deList:
+ _reg(lbl, "node.output")
+ elif isinstance(outLabels, list):
+ for lbl in outLabels:
+ _reg(lbl, "node.output")
+ except ImportError:
+ pass
+
+ try:
+ from modules.features.graphicalEditor.portTypes import PORT_TYPE_CATALOG
+ for schema in PORT_TYPE_CATALOG.values():
+ for field in getattr(schema, "fields", []) or []:
+ desc = getattr(field, "description", None)
+ if desc:
+ _reg(_extractDe(desc if isinstance(desc, (str, dict)) else None), "port.desc")
+ except ImportError:
+ pass
+
+ _nodeCategoryLabels = [
+ "Trigger", "Eingabe/Mensch", "Ablauf", "Daten", "KI",
+ "Datei", "E-Mail", "SharePoint", "ClickUp", "Treuhand",
+ ]
+ for lbl in _nodeCategoryLabels:
+ _reg(lbl, "node.category")
+
+ _entryPointTitles = ["Jetzt ausführen", "Start"]
+ for lbl in _entryPointTitles:
+ _reg(lbl, "node.entry")
+
+ logger.info("i18n node labels: %d new keys (node.*/port.* context)", added)
+
+
+def _registerDatamodelOptionLabels():
+ """Register all frontend_options labels from Pydantic datamodels and subscription plans."""
+ added = 0
+
+ def _extractDe(obj) -> str:
+ if isinstance(obj, str):
+ return obj
+ if isinstance(obj, dict):
+ return obj.get("de") or obj.get("en") or ""
+ return ""
+
+ def _reg(key: str, ctx: str):
+ nonlocal added
+ if key and key not in _REGISTRY:
+ _REGISTRY[key] = _I18nRegistryEntry(context=ctx, value="")
+ added += 1
+
+ _datamodelModules = (
+ "modules.datamodels.datamodelRbac",
+ "modules.datamodels.datamodelChat",
+ "modules.datamodels.datamodelMessaging",
+ "modules.datamodels.datamodelNotification",
+ "modules.datamodels.datamodelUam",
+ "modules.datamodels.datamodelFiles",
+ "modules.datamodels.datamodelDataSource",
+ "modules.datamodels.datamodelFeatureDataSource",
+ "modules.datamodels.datamodelUiLanguage",
+ "modules.features.trustee.datamodelFeatureTrustee",
+ "modules.features.neutralization.datamodelFeatureNeutralizer",
+ )
+
+ for modPath in _datamodelModules:
+ try:
+ mod = __import__(modPath, fromlist=["__all__"])
+ except ImportError:
+ continue
+ for attrName in dir(mod):
+ cls = getattr(mod, attrName, None)
+ if not isinstance(cls, type) or not issubclass(cls, BaseModel):
+ continue
+ for fieldName, fieldInfo in cls.model_fields.items():
+ extra = (fieldInfo.json_schema_extra or {}) if hasattr(fieldInfo, "json_schema_extra") else {}
+ if not isinstance(extra, dict):
+ continue
+ options = extra.get("frontend_options")
+ if not isinstance(options, list):
+ continue
+ ctx = f"option.{cls.__name__}.{fieldName}"
+ for opt in options:
+ if isinstance(opt, dict):
+ _reg(_extractDe(opt.get("label")), ctx)
+
+ try:
+ from modules.datamodels.datamodelSubscription import BUILTIN_PLANS
+ for plan in BUILTIN_PLANS.values():
+ _reg(_extractDe(getattr(plan, "title", None)), "subscription.title")
+ _reg(_extractDe(getattr(plan, "description", None)), "subscription.desc")
+ except (ImportError, AttributeError):
+ pass
+
+ logger.info("i18n datamodel option labels: %d new keys", added)
+
+
+# ---------------------------------------------------------------------------
+# Boot: sync registry to DB
+# ---------------------------------------------------------------------------
+
+async def _syncRegistryToDb():
+ """Boot hook: write all registered keys into UiLanguageSet(xx).
+
+ 1. Scans route files for routeApiMsg("…") to eagerly register api.* keys.
+ 2. Registers navigation labels as nav.* keys.
+ 3. Registers feature UI labels (FEATURE_LABEL, UI_OBJECTS).
+ 4. Registers RBAC labels (DATA/RESOURCE/ROLE/QuickAction).
+ 5. Merges with existing UI keys (context="ui"), only touches gateway keys.
+ """
+ _scanRouteApiMsgKeys()
+ _registerNavLabels()
+ _registerFeatureUiLabels()
+ _registerRbacLabels()
+ _registerServiceCenterLabels()
+ _registerNodeLabels()
+ _registerDatamodelOptionLabels()
+
+ if not _REGISTRY:
+ logger.info("i18n registry: no keys to sync (empty registry)")
+ return
+
+ from modules.datamodels.datamodelUiLanguage import UiLanguageSet
+ from modules.shared.configuration import APP_CONFIG
+ from modules.connectors.connectorDbPostgre import _get_cached_connector
+ from modules.shared.timeUtils import getUtcTimestamp
+
+ db = _get_cached_connector(
+ dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
+ dbDatabase="poweron_management",
+ dbUser=APP_CONFIG.get("DB_USER"),
+ dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET"),
+ dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
+ userId="__i18n_boot__",
+ )
+
+ rows = db.getRecordset(UiLanguageSet, recordFilter={"id": "xx"})
+
+ gatewayEntries = [
+ {"context": entry.context, "key": key, "value": entry.value}
+ for key, entry in _REGISTRY.items()
+ ]
+ gatewayKeys = set(_REGISTRY.keys())
+
+ if not rows:
+ now = getUtcTimestamp()
+ rec = {
+ "id": "xx",
+ "label": "Basisset (Meta)",
+ "entries": gatewayEntries,
+ "status": "complete",
+ "isDefault": True,
+ "sysCreatedAt": now,
+ "sysCreatedBy": "__i18n_boot__",
+ "sysModifiedAt": now,
+ "sysModifiedBy": "__i18n_boot__",
+ }
+ db.recordCreate(UiLanguageSet, rec)
+ logger.info("i18n boot-sync: created xx set with %d gateway keys", len(gatewayEntries))
+ return
+
+ row = dict(rows[0])
+ existingEntries: List[dict] = row.get("entries") or []
+ if not isinstance(existingEntries, list):
+ existingEntries = []
+
+ uiEntries = [e for e in existingEntries if e.get("context", "") == "ui"]
+
+ oldGatewayEntries = [
+ e for e in existingEntries
+ if e.get("context", "") != "ui"
+ ]
+ oldGatewayByKey = {e["key"]: e for e in oldGatewayEntries}
+
+ added = 0
+ updated = 0
+ removed = 0
+
+ newGatewayEntries: List[dict] = []
+ for key, entry in _REGISTRY.items():
+ newEntry = {"context": entry.context, "key": key, "value": entry.value}
+ old = oldGatewayByKey.get(key)
+ if old is None:
+ added += 1
+ elif old.get("context") != entry.context or old.get("value") != entry.value:
+ updated += 1
+ newGatewayEntries.append(newEntry)
+
+ removed = len(set(oldGatewayByKey.keys()) - gatewayKeys)
+
+ mergedEntries = uiEntries + newGatewayEntries
+
+ if added == 0 and updated == 0 and removed == 0:
+ logger.info("i18n boot-sync: xx set up-to-date (%d gateway + %d ui keys)", len(newGatewayEntries), len(uiEntries))
+ return
+
+ now = getUtcTimestamp()
+ row["entries"] = mergedEntries
+ if "keys" in row:
+ del row["keys"]
+ row["sysModifiedAt"] = now
+ row["sysModifiedBy"] = "__i18n_boot__"
+ db.recordModify(UiLanguageSet, "xx", row)
+
+ logger.info(
+ "i18n boot-sync: xx updated (+%d added, ~%d updated, -%d removed, total=%d gateway + %d ui)",
+ added, updated, removed, len(newGatewayEntries), len(uiEntries),
+ )
+
+
+# ---------------------------------------------------------------------------
+# Boot: load translation cache
+# ---------------------------------------------------------------------------
+
+async def _loadCache():
+ """Boot hook: load all UiLanguageSets into the in-memory cache.
+
+ After this, t() lookups are O(1) dict access with no DB calls.
+ """
+ from modules.datamodels.datamodelUiLanguage import UiLanguageSet
+ from modules.shared.configuration import APP_CONFIG
+ from modules.connectors.connectorDbPostgre import _get_cached_connector
+
+ db = _get_cached_connector(
+ dbHost=APP_CONFIG.get("DB_HOST", "localhost"),
+ dbDatabase="poweron_management",
+ dbUser=APP_CONFIG.get("DB_USER"),
+ dbPassword=APP_CONFIG.get("DB_PASSWORD_SECRET"),
+ dbPort=int(APP_CONFIG.get("DB_PORT", 5432)),
+ userId="__i18n_cache__",
+ )
+
+ rows = db.getRecordset(UiLanguageSet)
+ _CACHE.clear()
+
+ for row in rows:
+ code = row.get("id", "")
+ if code == "xx":
+ continue
+ entries = row.get("entries")
+ if not isinstance(entries, list):
+ continue
+ langDict: Dict[str, str] = {}
+ for e in entries:
+ key = e.get("key", "")
+ val = e.get("value", "")
+ if key and val:
+ langDict[key] = val
+ if langDict:
+ _CACHE[code] = langDict
+
+ logger.info("i18n cache loaded: %d languages, %d total keys",
+ len(_CACHE), sum(len(v) for v in _CACHE.values()))
diff --git a/modules/shared/jsonContinuation.py b/modules/shared/jsonContinuation.py
index dd71986e..22180b41 100644
--- a/modules/shared/jsonContinuation.py
+++ b/modules/shared/jsonContinuation.py
@@ -59,7 +59,7 @@ OVERLAP_MAX_CHARS: int = 1000
# =============================================================================
-class TokenType(Enum):
+class JsonTokenType(Enum):
"""JSON Token Types"""
OBJECT_START = "{"
OBJECT_END = "}"
@@ -77,9 +77,9 @@ class TokenType(Enum):
@dataclass
-class Token:
+class JsonToken:
"""Represents a JSON token with position info"""
- type: TokenType
+ type: JsonTokenType
value: Any
start_pos: int
end_pos: int
@@ -120,7 +120,7 @@ class JsonTokenizer:
return self.jsonStr[self.pos]
return None
- def readString(self) -> Token:
+ def readString(self) -> JsonToken:
"""Read a JSON string token"""
start_pos = self.pos
self.pos += 1 # Skip opening quote
@@ -142,15 +142,15 @@ class JsonTokenizer:
value = raw[1:-1] # Remove quotes for value
except:
value = raw
- return Token(TokenType.STRING, value, start_pos, self.pos, raw)
+ return JsonToken(JsonTokenType.STRING, value, start_pos, self.pos, raw)
else:
self.pos += 1
# String was truncated
raw = self.jsonStr[start_pos:self.pos]
- return Token(TokenType.TRUNCATED, raw[1:] if len(raw) > 1 else "", start_pos, self.pos, raw)
+ return JsonToken(JsonTokenType.TRUNCATED, raw[1:] if len(raw) > 1 else "", start_pos, self.pos, raw)
- def readNumber(self) -> Token:
+ def readNumber(self) -> JsonToken:
"""Read a JSON number token"""
start_pos = self.pos
@@ -182,54 +182,54 @@ class JsonTokenizer:
except ValueError:
value = raw
- return Token(TokenType.NUMBER, value, start_pos, self.pos, raw)
+ return JsonToken(JsonTokenType.NUMBER, value, start_pos, self.pos, raw)
- def readKeyword(self) -> Token:
+ def readKeyword(self) -> JsonToken:
"""Read true, false, or null"""
start_pos = self.pos
- for keyword, token_type in [('true', TokenType.BOOLEAN),
- ('false', TokenType.BOOLEAN),
- ('null', TokenType.NULL)]:
+ for keyword, token_type in [('true', JsonTokenType.BOOLEAN),
+ ('false', JsonTokenType.BOOLEAN),
+ ('null', JsonTokenType.NULL)]:
if self.jsonStr[self.pos:].startswith(keyword):
self.pos += len(keyword)
value = True if keyword == 'true' else (False if keyword == 'false' else None)
- return Token(token_type, value, start_pos, self.pos, keyword)
+ return JsonToken(token_type, value, start_pos, self.pos, keyword)
# Partial keyword (truncated)
while self.pos < self.length and self.jsonStr[self.pos].isalpha():
self.pos += 1
raw = self.jsonStr[start_pos:self.pos]
- return Token(TokenType.TRUNCATED, raw, start_pos, self.pos, raw)
+ return JsonToken(JsonTokenType.TRUNCATED, raw, start_pos, self.pos, raw)
- def nextToken(self) -> Token:
+ def nextJsonToken(self) -> JsonToken:
"""Get the next token"""
self.skipWhitespace()
if self.pos >= self.length:
- return Token(TokenType.EOF, None, self.pos, self.pos, "")
+ return JsonToken(JsonTokenType.EOF, None, self.pos, self.pos, "")
char = self.jsonStr[self.pos]
startPos = self.pos
if char == '{':
self.pos += 1
- return Token(TokenType.OBJECT_START, '{', startPos, self.pos, '{')
+ return JsonToken(JsonTokenType.OBJECT_START, '{', startPos, self.pos, '{')
elif char == '}':
self.pos += 1
- return Token(TokenType.OBJECT_END, '}', startPos, self.pos, '}')
+ return JsonToken(JsonTokenType.OBJECT_END, '}', startPos, self.pos, '}')
elif char == '[':
self.pos += 1
- return Token(TokenType.ARRAY_START, '[', startPos, self.pos, '[')
+ return JsonToken(JsonTokenType.ARRAY_START, '[', startPos, self.pos, '[')
elif char == ']':
self.pos += 1
- return Token(TokenType.ARRAY_END, ']', startPos, self.pos, ']')
+ return JsonToken(JsonTokenType.ARRAY_END, ']', startPos, self.pos, ']')
elif char == ':':
self.pos += 1
- return Token(TokenType.COLON, ':', startPos, self.pos, ':')
+ return JsonToken(JsonTokenType.COLON, ':', startPos, self.pos, ':')
elif char == ',':
self.pos += 1
- return Token(TokenType.COMMA, ',', startPos, self.pos, ',')
+ return JsonToken(JsonTokenType.COMMA, ',', startPos, self.pos, ',')
elif char == '"':
return self.readString()
elif char == '-' or char.isdigit():
@@ -239,7 +239,7 @@ class JsonTokenizer:
else:
# Unknown character, treat as truncated
self.pos += 1
- return Token(TokenType.TRUNCATED, char, startPos, self.pos, char)
+ return JsonToken(JsonTokenType.TRUNCATED, char, startPos, self.pos, char)
@dataclass
@@ -632,25 +632,25 @@ class JsonAnalyzer:
in_value = False
while True:
- token = tokenizer.nextToken()
+ token = tokenizer.nextJsonToken()
- if token.type == TokenType.EOF:
+ if token.type == JsonTokenType.EOF:
break
- if token.type == TokenType.TRUNCATED:
+ if token.type == JsonTokenType.TRUNCATED:
# Return position before the truncated part
break
- if token.type in (TokenType.OBJECT_START, TokenType.ARRAY_START):
+ if token.type in (JsonTokenType.OBJECT_START, JsonTokenType.ARRAY_START):
stack_depth += 1
in_value = True
- elif token.type in (TokenType.OBJECT_END, TokenType.ARRAY_END):
+ elif token.type in (JsonTokenType.OBJECT_END, JsonTokenType.ARRAY_END):
stack_depth -= 1
last_value_end = token.end_pos
in_value = False
- elif token.type == TokenType.STRING:
+ elif token.type == JsonTokenType.STRING:
# Check if this is a key or a value
saved_pos = tokenizer.pos
tokenizer.skipWhitespace()
@@ -662,11 +662,11 @@ class JsonAnalyzer:
last_value_end = token.end_pos
in_value = False
- elif token.type in (TokenType.NUMBER, TokenType.BOOLEAN, TokenType.NULL):
+ elif token.type in (JsonTokenType.NUMBER, JsonTokenType.BOOLEAN, JsonTokenType.NULL):
last_value_end = token.end_pos
in_value = False
- elif token.type == TokenType.COMMA:
+ elif token.type == JsonTokenType.COMMA:
# After a comma, we've completed a value
last_complete_pos = last_value_end
@@ -714,12 +714,12 @@ class JsonAnalyzer:
tokenizer = JsonTokenizer(self.jsonStr)
while True:
- token = tokenizer.nextToken()
+ token = tokenizer.nextJsonToken()
- if token.type == TokenType.EOF or token.type == TokenType.TRUNCATED:
+ if token.type == JsonTokenType.EOF or token.type == JsonTokenType.TRUNCATED:
break
- if token.type == TokenType.OBJECT_START:
+ if token.type == JsonTokenType.OBJECT_START:
frame = StackFrame(
type="object",
start_pos=token.start_pos,
@@ -727,7 +727,7 @@ class JsonAnalyzer:
)
self.stack.append(frame)
- elif token.type == TokenType.ARRAY_START:
+ elif token.type == JsonTokenType.ARRAY_START:
frame = StackFrame(
type="array",
start_pos=token.start_pos,
@@ -735,24 +735,24 @@ class JsonAnalyzer:
)
self.stack.append(frame)
- elif token.type == TokenType.OBJECT_END:
+ elif token.type == JsonTokenType.OBJECT_END:
if self.stack and self.stack[-1].type == "object":
self.stack.pop()
- elif token.type == TokenType.ARRAY_END:
+ elif token.type == JsonTokenType.ARRAY_END:
if self.stack and self.stack[-1].type == "array":
self.stack.pop()
- elif token.type == TokenType.STRING:
+ elif token.type == JsonTokenType.STRING:
# Could be a key or a value
- self._handleStringToken(token, tokenizer)
+ self._handleStringJsonToken(token, tokenizer)
- elif token.type == TokenType.COMMA:
+ elif token.type == JsonTokenType.COMMA:
# Increment array index
if self.stack and self.stack[-1].type == "array":
self.stack[-1].index += 1
- def _handleStringToken(self, token: Token, tokenizer: JsonTokenizer):
+ def _handleStringJsonToken(self, token: JsonToken, tokenizer: JsonTokenizer):
"""Handle a string token (could be key or value)"""
if self.stack and self.stack[-1].type == "object":
# Check if this is a key (followed by colon)
@@ -995,12 +995,12 @@ class JsonAnalyzer:
current_key = None
while True:
- token = tokenizer.nextToken()
+ token = tokenizer.nextJsonToken()
- if token.type == TokenType.EOF:
+ if token.type == JsonTokenType.EOF:
break
- if token.type == TokenType.TRUNCATED:
+ if token.type == JsonTokenType.TRUNCATED:
# Mark the truncation point
if stack:
current = stack[-1]
@@ -1020,7 +1020,7 @@ class JsonAnalyzer:
})
break
- if token.type == TokenType.OBJECT_START:
+ if token.type == JsonTokenType.OBJECT_START:
obj = {
'type': 'object',
'key': current_key,
@@ -1032,7 +1032,7 @@ class JsonAnalyzer:
stack.append(obj)
current_key = None
- elif token.type == TokenType.ARRAY_START:
+ elif token.type == JsonTokenType.ARRAY_START:
arr = {
'type': 'array',
'key': current_key,
@@ -1044,19 +1044,19 @@ class JsonAnalyzer:
stack.append(arr)
current_key = None
- elif token.type == TokenType.OBJECT_END:
+ elif token.type == JsonTokenType.OBJECT_END:
if len(stack) > 1 and stack[-1].get('type') == 'object':
stack[-1]['end_pos'] = token.end_pos
stack[-1]['complete'] = True
stack.pop()
- elif token.type == TokenType.ARRAY_END:
+ elif token.type == JsonTokenType.ARRAY_END:
if len(stack) > 1 and stack[-1].get('type') == 'array':
stack[-1]['end_pos'] = token.end_pos
stack[-1]['complete'] = True
stack.pop()
- elif token.type == TokenType.STRING:
+ elif token.type == JsonTokenType.STRING:
# Check if it's a key
saved_pos = tokenizer.pos
tokenizer.skipWhitespace()
@@ -1081,7 +1081,7 @@ class JsonAnalyzer:
tokenizer.pos = saved_pos
- elif token.type in (TokenType.NUMBER, TokenType.BOOLEAN, TokenType.NULL):
+ elif token.type in (JsonTokenType.NUMBER, JsonTokenType.BOOLEAN, JsonTokenType.NULL):
value_node = {
'type': 'value',
'key': current_key,
diff --git a/modules/system/mainSystem.py b/modules/system/mainSystem.py
index 9d65ef5b..e3cfe2b0 100644
--- a/modules/system/mainSystem.py
+++ b/modules/system/mainSystem.py
@@ -15,7 +15,7 @@ logger = logging.getLogger(__name__)
# System metadata
FEATURE_CODE = "system"
-FEATURE_LABEL = {"en": "System", "de": "System", "fr": "Système"}
+FEATURE_LABEL = "System"
FEATURE_ICON = "mdi-cog"
# =============================================================================
@@ -38,13 +38,13 @@ NAVIGATION_SECTIONS = [
# ─── Meine Sicht (with top-level item + subgroups) ───
{
"id": "system",
- "title": {"en": "MY VIEW", "de": "MEINE SICHT", "fr": "MA VUE"},
+ "title": "Meine Sicht",
"order": 10,
"items": [
{
"id": "home",
"objectKey": "ui.system.home",
- "label": {"en": "Home", "de": "Übersicht", "fr": "Accueil"},
+ "label": "Übersicht",
"icon": "FaHome",
"path": "/",
"order": 10,
@@ -55,13 +55,13 @@ NAVIGATION_SECTIONS = [
# ── Basisdaten ──
{
"id": "system-basedata",
- "title": {"en": "Base Data", "de": "Basisdaten", "fr": "Données de base"},
+ "title": "Basisdaten",
"order": 20,
"items": [
{
"id": "connections",
"objectKey": "ui.system.connections",
- "label": {"en": "Connections", "de": "Verbindungen", "fr": "Connexions"},
+ "label": "Verbindungen",
"icon": "FaLink",
"path": "/basedata/connections",
"order": 10,
@@ -69,7 +69,7 @@ NAVIGATION_SECTIONS = [
{
"id": "files",
"objectKey": "ui.system.files",
- "label": {"en": "Files", "de": "Dateien", "fr": "Fichiers"},
+ "label": "Dateien",
"icon": "FaRegFileAlt",
"path": "/basedata/files",
"order": 20,
@@ -77,7 +77,7 @@ NAVIGATION_SECTIONS = [
{
"id": "prompts",
"objectKey": "ui.system.prompts",
- "label": {"en": "Prompts", "de": "Prompts", "fr": "Prompts"},
+ "label": "Prompts",
"icon": "FaLightbulb",
"path": "/basedata/prompts",
"order": 30,
@@ -87,13 +87,13 @@ NAVIGATION_SECTIONS = [
# ── Nutzung ──
{
"id": "system-usage",
- "title": {"en": "Usage", "de": "Nutzung", "fr": "Utilisation"},
+ "title": "Nutzung",
"order": 30,
"items": [
{
"id": "billing-admin",
"objectKey": "ui.system.billingAdmin",
- "label": {"en": "Billing", "de": "Abrechnung", "fr": "Facturation"},
+ "label": "Abrechnung",
"icon": "FaMoneyBillAlt",
"path": "/billing/admin",
"order": 10,
@@ -101,7 +101,7 @@ NAVIGATION_SECTIONS = [
{
"id": "statistics",
"objectKey": "ui.system.statistics",
- "label": {"en": "Statistics", "de": "Statistiken", "fr": "Statistiques"},
+ "label": "Statistiken",
"icon": "FaChartBar",
"path": "/billing/transactions",
"order": 20,
@@ -109,7 +109,7 @@ NAVIGATION_SECTIONS = [
{
"id": "automations",
"objectKey": "ui.system.automations",
- "label": {"en": "Automations", "de": "Automations", "fr": "Automations"},
+ "label": "Automations",
"icon": "FaRobot",
"path": "/automations",
"order": 30,
@@ -117,7 +117,7 @@ NAVIGATION_SECTIONS = [
{
"id": "store",
"objectKey": "ui.system.store",
- "label": {"en": "Store", "de": "Store", "fr": "Store"},
+ "label": "Store",
"icon": "FaStore",
"path": "/store",
"order": 40,
@@ -126,7 +126,7 @@ NAVIGATION_SECTIONS = [
{
"id": "settings",
"objectKey": "ui.system.settings",
- "label": {"en": "Settings", "de": "Einstellungen", "fr": "Paramètres"},
+ "label": "Einstellungen",
"icon": "FaCog",
"path": "/settings",
"order": 50,
@@ -139,19 +139,19 @@ NAVIGATION_SECTIONS = [
# ─── Administration (with subgroups) ───
{
"id": "admin",
- "title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"},
+ "title": "Administration",
"order": 200,
"subgroups": [
# ── Wizards ──
{
"id": "admin-wizards",
- "title": {"en": "Wizards", "de": "Wizards", "fr": "Assistants"},
+ "title": "Wizards",
"order": 10,
"items": [
{
"id": "admin-mandate-wizard",
"objectKey": "ui.admin.mandateWizard",
- "label": {"en": "Mandate Wizard", "de": "Mandanten-Wizard", "fr": "Assistant mandat"},
+ "label": "Mandanten-Wizard",
"icon": "FaMagic",
"path": "/admin/mandate-wizard",
"order": 10,
@@ -160,7 +160,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-invitation-wizard",
"objectKey": "ui.admin.invitationWizard",
- "label": {"en": "Invitation Wizard", "de": "Einladungs-Wizard", "fr": "Assistant d'invitation"},
+ "label": "Einladungs-Wizard",
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitation-wizard",
"order": 20,
@@ -171,13 +171,13 @@ NAVIGATION_SECTIONS = [
# ── Users ──
{
"id": "admin-users-group",
- "title": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
+ "title": "Benutzer",
"order": 20,
"items": [
{
"id": "admin-users",
"objectKey": "ui.admin.users",
- "label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"},
+ "label": "Benutzer",
"icon": "FaUsers",
"path": "/admin/users",
"order": 10,
@@ -186,7 +186,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-invitations",
"objectKey": "ui.admin.invitations",
- "label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"},
+ "label": "Benutzer-Einladungen",
"icon": "FaEnvelopeOpenText",
"path": "/admin/invitations",
"order": 20,
@@ -195,7 +195,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-user-access-overview",
"objectKey": "ui.admin.userAccessOverview",
- "label": {"en": "User Access Overview", "de": "Benutzer-Zugriffsübersicht", "fr": "Aperçu des accès utilisateur"},
+ "label": "Benutzer-Zugriffsübersicht",
"icon": "FaClipboardList",
"path": "/admin/user-access-overview",
"order": 30,
@@ -204,7 +204,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-subscriptions",
"objectKey": "ui.admin.subscriptions",
- "label": {"en": "Subscriptions", "de": "Abonnements", "fr": "Abonnements"},
+ "label": "Abonnements",
"icon": "FaFileContract",
"path": "/admin/subscriptions",
"order": 40,
@@ -215,13 +215,13 @@ NAVIGATION_SECTIONS = [
# ── System ──
{
"id": "admin-system-group",
- "title": {"en": "System", "de": "System", "fr": "Système"},
+ "title": "System",
"order": 30,
"items": [
{
"id": "admin-roles",
"objectKey": "ui.admin.roles",
- "label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"},
+ "label": "Rollen",
"icon": "FaUserTag",
"path": "/admin/mandate-roles",
"order": 10,
@@ -230,7 +230,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-mandate-role-permissions",
"objectKey": "ui.admin.mandateRolePermissions",
- "label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"},
+ "label": "Rollen-Berechtigungen",
"icon": "FaKey",
"path": "/admin/mandate-role-permissions",
"order": 20,
@@ -239,7 +239,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-mandates",
"objectKey": "ui.admin.mandates",
- "label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"},
+ "label": "Mandanten",
"icon": "FaBuilding",
"path": "/admin/mandates",
"order": 30,
@@ -248,7 +248,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-user-mandates",
"objectKey": "ui.admin.userMandates",
- "label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"},
+ "label": "Mandanten-Mitglieder",
"icon": "FaUserFriends",
"path": "/admin/user-mandates",
"order": 40,
@@ -257,7 +257,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-access",
"objectKey": "ui.admin.access",
- "label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"},
+ "label": "Zugriffsverwaltung",
"icon": "FaBuilding",
"path": "/admin/access",
"order": 50,
@@ -266,7 +266,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-feature-instances",
"objectKey": "ui.admin.featureInstances",
- "label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"},
+ "label": "Feature-Instanzen",
"icon": "FaCubes",
"path": "/admin/feature-instances",
"order": 60,
@@ -275,7 +275,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-feature-roles",
"objectKey": "ui.admin.featureRoles",
- "label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"},
+ "label": "Features Rollen-Vorlagen",
"icon": "FaShieldAlt",
"path": "/admin/feature-roles",
"order": 70,
@@ -285,7 +285,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-logs",
"objectKey": "ui.admin.logs",
- "label": {"en": "Logs", "de": "Logs", "fr": "Logs"},
+ "label": "Logs",
"icon": "FaFileAlt",
"path": "/admin/logs",
"order": 90,
@@ -295,7 +295,7 @@ NAVIGATION_SECTIONS = [
{
"id": "admin-languages",
"objectKey": "ui.admin.languages",
- "label": {"en": "UI Languages", "de": "UI-Sprachen", "fr": "Langues UI"},
+ "label": "UI-Sprachen",
"icon": "FaGlobe",
"path": "/admin/languages",
"order": 95,
@@ -376,64 +376,64 @@ DATA_OBJECTS = [
# UAM (User Access Management) - mandantenübergreifend
{
"objectKey": "data.uam.UserInDB",
- "label": {"en": "User", "de": "Benutzer", "fr": "Utilisateur"},
+ "label": "Benutzer",
"meta": {"table": "UserInDB", "namespace": "uam"}
},
{
"objectKey": "data.uam.AuthEvent",
- "label": {"en": "Auth Event", "de": "Auth-Ereignis", "fr": "Événement d'auth"},
+ "label": "Auth-Ereignis",
"meta": {"table": "AuthEvent", "namespace": "uam"}
},
{
"objectKey": "data.uam.UserConnection",
- "label": {"en": "Connection", "de": "Verbindung", "fr": "Connexion"},
+ "label": "Verbindung",
"meta": {"table": "UserConnection", "namespace": "uam"}
},
{
"objectKey": "data.uam.Mandate",
- "label": {"en": "Mandate", "de": "Mandant", "fr": "Mandat"},
+ "label": "Mandant",
"meta": {"table": "Mandate", "namespace": "uam"}
},
{
"objectKey": "data.uam.UserMandate",
- "label": {"en": "User Mandate", "de": "Benutzer-Mandant", "fr": "Mandat utilisateur"},
+ "label": "Benutzer-Mandant",
"meta": {"table": "UserMandate", "namespace": "uam"}
},
{
"objectKey": "data.uam.Invitation",
- "label": {"en": "Invitation", "de": "Einladung", "fr": "Invitation"},
+ "label": "Einladung",
"meta": {"table": "Invitation", "namespace": "uam"}
},
{
"objectKey": "data.uam.Role",
- "label": {"en": "Role", "de": "Rolle", "fr": "Rôle"},
+ "label": "Rolle",
"meta": {"table": "Role", "namespace": "uam"}
},
{
"objectKey": "data.uam.AccessRule",
- "label": {"en": "Access Rule", "de": "Zugriffsregel", "fr": "Règle d'accès"},
+ "label": "Zugriffsregel",
"meta": {"table": "AccessRule", "namespace": "uam"}
},
{
"objectKey": "data.uam.FeatureInstance",
- "label": {"en": "Feature Instance", "de": "Feature-Instanz", "fr": "Instance de feature"},
+ "label": "Feature-Instanz",
"meta": {"table": "FeatureInstance", "namespace": "uam"}
},
# Chat - benutzer-eigen, kein Mandantenkontext
{
"objectKey": "data.chat.Prompt",
- "label": {"en": "Prompt", "de": "Prompt", "fr": "Prompt"},
+ "label": "Prompt",
"meta": {"table": "Prompt", "namespace": "chat", "groupDisabled": True}
},
{
"objectKey": "data.chat.ChatWorkflow",
- "label": {"en": "Chat Workflow", "de": "Chat-Workflow", "fr": "Workflow de chat"},
+ "label": "Chat-Workflow",
"meta": {"table": "ChatWorkflow", "namespace": "chat", "groupDisabled": True}
},
# Files - benutzer-eigen
{
"objectKey": "data.files.FileItem",
- "label": {"en": "File", "de": "Datei", "fr": "Fichier"},
+ "label": "Datei",
"meta": {"table": "FileItem", "namespace": "files", "groupDisabled": True}
},
]
@@ -445,37 +445,37 @@ DATA_OBJECTS = [
RESOURCE_OBJECTS = [
{
"objectKey": "resource.store.teamsbot",
- "label": {"en": "Store: Teams Bot", "de": "Store: Teams Bot", "fr": "Store: Teams Bot"},
+ "label": "Store: Teams Bot",
"meta": {"category": "store", "featureCode": "teamsbot"}
},
{
"objectKey": "resource.store.workspace",
- "label": {"en": "Store: AI Workspace", "de": "Store: AI Workspace", "fr": "Store: AI Workspace"},
+ "label": "Store: AI Workspace",
"meta": {"category": "store", "featureCode": "workspace"}
},
{
"objectKey": "resource.store.commcoach",
- "label": {"en": "Store: CommCoach", "de": "Store: CommCoach", "fr": "Store: CommCoach"},
+ "label": "Store: CommCoach",
"meta": {"category": "store", "featureCode": "commcoach"}
},
{
"objectKey": "resource.system.api.auth",
- "label": {"en": "Authentication API", "de": "Authentifizierungs-API", "fr": "API d'authentification"},
+ "label": "Authentifizierungs-API",
"meta": {"endpoint": "/api/auth/*"}
},
{
"objectKey": "resource.system.api.users",
- "label": {"en": "Users API", "de": "Benutzer-API", "fr": "API des utilisateurs"},
+ "label": "Benutzer-API",
"meta": {"endpoint": "/api/users/*"}
},
{
"objectKey": "resource.system.api.mandates",
- "label": {"en": "Mandates API", "de": "Mandanten-API", "fr": "API des mandats"},
+ "label": "Mandanten-API",
"meta": {"endpoint": "/api/mandates/*"}
},
{
"objectKey": "resource.system.api.rbac",
- "label": {"en": "RBAC API", "de": "RBAC-API", "fr": "API RBAC"},
+ "label": "RBAC-API",
"meta": {"endpoint": "/api/rbac/*"}
},
]
@@ -487,13 +487,13 @@ def _discoverAicoreProviderObjects() -> List[Dict[str, Any]]:
Providers are discovered from the model registry at startup.
"""
providerLabels = {
- "anthropic": {"en": "Anthropic (Claude)", "de": "Anthropic (Claude)", "fr": "Anthropic (Claude)"},
- "openai": {"en": "OpenAI (GPT)", "de": "OpenAI (GPT)", "fr": "OpenAI (GPT)"},
- "mistral": {"en": "Mistral (Le Chat)", "de": "Mistral (Le Chat)", "fr": "Mistral (Le Chat)"},
- "perplexity": {"en": "Perplexity", "de": "Perplexity", "fr": "Perplexity"},
- "tavily": {"en": "Tavily (Web Search)", "de": "Tavily (Websuche)", "fr": "Tavily (Recherche Web)"},
- "privatellm": {"en": "Private LLM", "de": "Private LLM", "fr": "LLM Privé"},
- "internal": {"en": "Internal", "de": "Intern", "fr": "Interne"},
+ "anthropic": "Anthropic (Claude)",
+ "openai": "OpenAI (GPT)",
+ "mistral": "Mistral (Le Chat)",
+ "perplexity": "Perplexity",
+ "tavily": "Tavily (Websuche)",
+ "privatellm": "Private LLM",
+ "internal": "Intern",
}
try:
@@ -503,7 +503,7 @@ def _discoverAicoreProviderObjects() -> List[Dict[str, Any]]:
objects = []
for provider in providers:
- label = providerLabels.get(provider, {"en": provider, "de": provider, "fr": provider})
+ label = providerLabels.get(provider, provider)
objects.append({
"objectKey": f"resource.aicore.{provider}",
"label": label,
diff --git a/modules/workflows/automation2/executionEngine.py b/modules/workflows/automation2/executionEngine.py
index e7a6645f..97ac1918 100644
--- a/modules/workflows/automation2/executionEngine.py
+++ b/modules/workflows/automation2/executionEngine.py
@@ -32,6 +32,56 @@ from modules.workflows.automation2.runEnvelope import normalize_run_envelope
logger = logging.getLogger(__name__)
+_NODE_DEF_BY_ID: Dict[str, dict] = {}
+
+
+def _getNodeDef(nodeType: str) -> Optional[dict]:
+ """Lookup static node definition by type id (cached)."""
+ if not _NODE_DEF_BY_ID:
+ for nd in STATIC_NODE_TYPES:
+ _NODE_DEF_BY_ID[nd["id"]] = nd
+ return _NODE_DEF_BY_ID.get(nodeType)
+
+
+def _outputSchemaForNode(nodeType: str) -> Optional[str]:
+ """Return the output port schema name for a node type (port 0), or None."""
+ nd = _getNodeDef(nodeType)
+ if not nd:
+ return None
+ ports = nd.get("outputPorts")
+ if isinstance(ports, dict):
+ p0 = ports.get(0) or ports.get("0")
+ if isinstance(p0, dict):
+ return p0.get("schema")
+ return None
+
+
+def _isMergeNode(nodeType: str) -> bool:
+ return nodeType == "flow.merge"
+
+
+def _allMergePredecessorsReady(
+ nodeId: str,
+ connectionMap: Dict[str, List],
+ nodeOutputs: Dict[str, Any],
+) -> bool:
+ """For flow.merge: check that every connected predecessor has produced output or was skipped."""
+ for src, _, _ in connectionMap.get(nodeId, []):
+ if src not in nodeOutputs:
+ return False
+ return True
+
+
+def _normalizeResult(result: Any, nodeType: str) -> Any:
+ """Apply _normalizeToSchema if the node has a declared output schema."""
+ schema = _outputSchemaForNode(nodeType)
+ if schema and schema != "Transit" and isinstance(result, dict):
+ try:
+ return _normalizeToSchema(result, schema)
+ except Exception:
+ pass
+ return result
+
def _getNodeTypeIds(services: Any = None) -> Set[str]:
"""Collect all known node type IDs from static definitions."""
@@ -261,6 +311,19 @@ async def executeGraph(
nodeOutputs: Dict[str, Any] = dict(initialNodeOutputs or {})
is_resume = startAfterNodeId is not None
+
+ if is_resume and initialNodeOutputs and startAfterNodeId:
+ resumedNode = next((n for n in nodes if n.get("id") == startAfterNodeId), None)
+ if resumedNode:
+ resumedType = resumedNode.get("type", "")
+ resumedOutput = initialNodeOutputs.get(startAfterNodeId)
+ if isinstance(resumedOutput, dict):
+ schema = _outputSchemaForNode(resumedType)
+ if schema and schema != "Transit":
+ try:
+ initialNodeOutputs[startAfterNodeId] = _normalizeToSchema(resumedOutput, schema)
+ except Exception as valErr:
+ logger.warning("executeGraph resume: schema validation failed for %s: %s", startAfterNodeId, valErr)
if not runId and automation2_interface and workflowId and not is_resume:
run_context = {
"connectionMap": connectionMap,
@@ -491,6 +554,20 @@ async def executeGraph(
output={"iterationCount": len(items), "items": len(items)},
durationMs=int((time.time() - _stepStartMs) * 1000))
logger.info("executeGraph flow.loop done: %d iterations", len(items))
+ elif _isMergeNode(nodeType):
+ if not _allMergePredecessorsReady(nodeId, connectionMap, nodeOutputs):
+ logger.info("executeGraph node %s (flow.merge): waiting — not all predecessors ready, deferring", nodeId)
+ nodeOutputs[nodeId] = None
+ continue
+ _stepStartMs = time.time()
+ _inputSnap = {}
+ for src, _, _ in connectionMap.get(nodeId, []):
+ if src in nodeOutputs:
+ _inputSnap[src] = nodeOutputs[src]
+ _stepId = _createStepLog(automation2_interface, runId, nodeId, nodeType, "running", _inputSnap)
+ result, retryCount = await _executeWithRetry(executor, node, context)
+ result = _normalizeResult(result, nodeType)
+ nodeOutputs[nodeId] = result
else:
_stepStartMs = time.time()
_inputSnap = {}
@@ -499,6 +576,7 @@ async def executeGraph(
_inputSnap[src] = nodeOutputs[src]
_stepId = _createStepLog(automation2_interface, runId, nodeId, nodeType, "running", _inputSnap)
result, retryCount = await _executeWithRetry(executor, node, context)
+ result = _normalizeResult(result, nodeType)
nodeOutputs[nodeId] = result
_durMs = int((time.time() - _stepStartMs) * 1000)
_tokens = result.get("tokensUsed", 0) if isinstance(result, dict) else 0
diff --git a/modules/workflows/processing/modes/modeAutomation.py b/modules/workflows/processing/modes/modeAutomation.py
index 1d0121b9..f48d509e 100644
--- a/modules/workflows/processing/modes/modeAutomation.py
+++ b/modules/workflows/processing/modes/modeAutomation.py
@@ -8,7 +8,7 @@ import logging
import uuid
from typing import List, Dict, Any, Optional
from modules.datamodels.datamodelChat import (
- TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
+ TaskStep, TaskContext, ChatTaskResult, ActionItem, TaskStatus,
TaskPlan, ActionResult
)
from modules.datamodels.datamodelChat import ChatWorkflow
@@ -169,7 +169,7 @@ class AutomationMode(BaseMode):
return []
async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext,
- taskIndex: int = None, totalTasks: int = None) -> TaskResult:
+ taskIndex: int = None, totalTasks: int = None) -> ChatTaskResult:
"""
Execute task using Automation mode - executes predefined actions directly.
No AI planning or review phases - actions are executed sequentially as defined.
@@ -198,7 +198,7 @@ class AutomationMode(BaseMode):
if not actions:
logger.error(f"No actions found for task {taskIndex}, aborting")
- return TaskResult(
+ return ChatTaskResult(
taskId=taskStep.id,
status=TaskStatus.FAILED,
success=False,
@@ -266,7 +266,7 @@ class AutomationMode(BaseMode):
# Persist this action's result so next action can reference it via documentList
if getattr(self, "processor", None) and result.documents:
try:
- from modules.datamodels.datamodelWorkflow import TaskResult as WorkflowTaskResult
+ from modules.datamodels.datamodelWorkflow import WorkflowTaskResult
resultLabel = action.execResultLabel or f"action_{actionNumber}_result"
actionResultWithLabel = ActionResult(
success=result.success,
@@ -306,7 +306,7 @@ class AutomationMode(BaseMode):
taskStep, workflow, taskIndex, totalTasks, None
)
- return TaskResult(
+ return ChatTaskResult(
taskId=taskStep.id,
status=TaskStatus.COMPLETED,
success=True,
@@ -323,7 +323,7 @@ class AutomationMode(BaseMode):
taskStep, workflow, taskIndex, errorSummary
)
- return TaskResult(
+ return ChatTaskResult(
taskId=taskStep.id,
status=TaskStatus.FAILED,
success=False,
@@ -335,7 +335,7 @@ class AutomationMode(BaseMode):
logger.error(f"Error executing task {taskIndex}: {str(e)}")
await self.messageCreator.createErrorMessage(taskStep, workflow, taskIndex, str(e))
- return TaskResult(
+ return ChatTaskResult(
taskId=taskStep.id,
status=TaskStatus.FAILED,
success=False,
diff --git a/modules/workflows/processing/modes/modeBase.py b/modules/workflows/processing/modes/modeBase.py
index fe9a5da6..a8a3e048 100644
--- a/modules/workflows/processing/modes/modeBase.py
+++ b/modules/workflows/processing/modes/modeBase.py
@@ -7,7 +7,7 @@ from abc import ABC, abstractmethod
import uuid
import logging
from typing import List, Dict, Any, Optional
-from modules.datamodels.datamodelChat import TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus
+from modules.datamodels.datamodelChat import TaskStep, TaskContext, ChatTaskResult, ActionItem, TaskStatus
from modules.datamodels.datamodelChat import ChatWorkflow
from modules.workflows.processing.core.taskPlanner import TaskPlanner
from modules.workflows.processing.core.actionExecutor import ActionExecutor
@@ -29,7 +29,7 @@ class BaseMode(ABC):
@abstractmethod
- async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> TaskResult:
+ async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> ChatTaskResult:
"""Execute a task step - must be implemented by concrete modes"""
pass
diff --git a/modules/workflows/processing/modes/modeDynamic.py b/modules/workflows/processing/modes/modeDynamic.py
index ab992cd4..67a32a64 100644
--- a/modules/workflows/processing/modes/modeDynamic.py
+++ b/modules/workflows/processing/modes/modeDynamic.py
@@ -10,7 +10,7 @@ import time
from datetime import datetime, timezone
from typing import List, Dict, Any
from modules.datamodels.datamodelChat import (
- TaskStep, TaskContext, TaskResult, ActionItem, TaskStatus,
+ TaskStep, TaskContext, ChatTaskResult, ActionItem, TaskStatus,
ActionResult, Observation, ObservationPreview, ReviewResult, ReviewContext
)
from modules.datamodels.datamodelChat import ChatWorkflow
@@ -48,7 +48,7 @@ class DynamicMode(BaseMode):
# Dynamic mode generates actions one at a time in the execution loop
return []
- async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> TaskResult:
+ async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> ChatTaskResult:
"""Execute task using Dynamic mode - iterative plan-act-observe-refine loop"""
# Get task index from workflow state
@@ -335,7 +335,7 @@ class DynamicMode(BaseMode):
# Create task completion message (totalTasks not needed - removed from signature)
await self.messageCreator.createTaskCompletionMessage(taskStep, workflow, taskIndex, None, completionReviewResult)
- return TaskResult(
+ return ChatTaskResult(
taskId=taskStep.id,
status=status,
success=success,
diff --git a/modules/workflows/processing/workflowProcessor.py b/modules/workflows/processing/workflowProcessor.py
index 3f83379b..99d8fd63 100644
--- a/modules/workflows/processing/workflowProcessor.py
+++ b/modules/workflows/processing/workflowProcessor.py
@@ -18,7 +18,7 @@ from modules.shared.jsonUtils import extractJsonString, repairBrokenJson, parseJ
from modules.datamodels.datamodelWorkflow import UnderstandingResult
if TYPE_CHECKING:
- from modules.datamodels.datamodelWorkflow import TaskResult
+ from modules.datamodels.datamodelWorkflow import WorkflowTaskResult
logger = logging.getLogger(__name__)
@@ -109,7 +109,7 @@ class WorkflowProcessor:
self.services.chat.progressLogFinish(operationId, False)
raise
- async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> datamodelChat.TaskResult:
+ async def executeTask(self, taskStep: TaskStep, workflow: ChatWorkflow, context: TaskContext) -> datamodelChat.ChatTaskResult:
"""Execute a task step using the appropriate mode"""
import time
diff --git a/modules/workflows/workflowManager.py b/modules/workflows/workflowManager.py
index 3fa6a373..379283b8 100644
--- a/modules/workflows/workflowManager.py
+++ b/modules/workflows/workflowManager.py
@@ -942,7 +942,7 @@ The following is the user's original input message. Analyze intent, normalize th
# Persist task result for cross-task/round document references
# Convert ChatTaskResult to WorkflowTaskResult for persistence
- from modules.datamodels.datamodelWorkflow import TaskResult as WorkflowTaskResult
+ from modules.datamodels.datamodelWorkflow import WorkflowTaskResult
# Get final ActionResult from task execution (last action result)
finalActionResult = None
@@ -952,7 +952,6 @@ The following is the user's original input message. Analyze intent, normalize th
# Use last action result from context
finalActionResult = taskContext.previousActionResults[-1]
- # Create WorkflowTaskResult for persistence
if finalActionResult:
workflowTaskResult = WorkflowTaskResult(
taskId=taskStep.id,
diff --git a/tests/unit/datamodels/test_workflow_models.py b/tests/unit/datamodels/test_workflow_models.py
index ab73f10f..59e3736d 100644
--- a/tests/unit/datamodels/test_workflow_models.py
+++ b/tests/unit/datamodels/test_workflow_models.py
@@ -19,7 +19,7 @@ from modules.datamodels.datamodelWorkflow import (
RequestContext,
UnderstandingResult,
TaskDefinition,
- TaskResult
+ WorkflowTaskResult
)
from modules.datamodels.datamodelDocref import DocumentReferenceList, DocumentListReference
from modules.datamodels.datamodelAi import OperationTypeEnum