From 7778325e5e8401e43da8f39ad14696caee9094d6 Mon Sep 17 00:00:00 2001 From: patrick-motsch Date: Wed, 18 Feb 2026 23:52:51 +0100 Subject: [PATCH] feat: add debugMode to config, filter bot own captions to prevent repeats Co-authored-by: Cursor --- .../features/teamsbot/browserBotConnector.py | 2 + .../features/teamsbot/datamodelTeamsbot.py | 2 + modules/features/teamsbot/service.py | 13 +- .../routes/routeAdminUserAccessOverview.py | 28 ++ modules/routes/routeSystem.py | 97 ++++-- modules/system/mainSystem.py | 289 +++++++++++------- 6 files changed, 283 insertions(+), 148 deletions(-) diff --git a/modules/features/teamsbot/browserBotConnector.py b/modules/features/teamsbot/browserBotConnector.py index 173d2e5e..2e76d039 100644 --- a/modules/features/teamsbot/browserBotConnector.py +++ b/modules/features/teamsbot/browserBotConnector.py @@ -39,6 +39,7 @@ class BrowserBotConnector: botAccountEmail: Optional[str] = None, botAccountPassword: Optional[str] = None, transferMode: str = "auto", + debugMode: bool = False, ) -> Dict[str, Any]: """ Send join command to the Browser Bot service. @@ -75,6 +76,7 @@ class BrowserBotConnector: "gatewayWsUrl": gatewayWsUrl, "language": language, "transferMode": transferMode, + "debugMode": debugMode, } # Add authenticated join credentials if configured diff --git a/modules/features/teamsbot/datamodelTeamsbot.py b/modules/features/teamsbot/datamodelTeamsbot.py index dd823c2e..0d058db3 100644 --- a/modules/features/teamsbot/datamodelTeamsbot.py +++ b/modules/features/teamsbot/datamodelTeamsbot.py @@ -186,6 +186,7 @@ class TeamsbotConfig(BaseModel): triggerIntervalSeconds: int = Field(default=10, ge=3, le=60, description="Seconds between periodic AI analysis triggers") triggerCooldownSeconds: int = Field(default=3, ge=1, le=30, description="Minimum seconds between AI calls") contextWindowSegments: int = Field(default=20, ge=5, le=100, description="Number of transcript segments to include in AI context") + debugMode: bool = Field(default=False, description="Enable debug mode: screenshots at every join step for diagnostics") def _getEffectiveBrowserBotUrl(self) -> Optional[str]: """Resolve the effective browser bot URL: per-instance config takes priority, then env variable.""" @@ -228,6 +229,7 @@ class TeamsbotConfigUpdateRequest(BaseModel): triggerIntervalSeconds: Optional[int] = None triggerCooldownSeconds: Optional[int] = None contextWindowSegments: Optional[int] = None + debugMode: Optional[bool] = None # ============================================================================ diff --git a/modules/features/teamsbot/service.py b/modules/features/teamsbot/service.py index 6482c073..b00b6ccb 100644 --- a/modules/features/teamsbot/service.py +++ b/modules/features/teamsbot/service.py @@ -146,6 +146,7 @@ class TeamsbotService: botAccountEmail=botAccountEmail, botAccountPassword=botAccountPassword, transferMode=self.config.transferMode if hasattr(self.config, 'transferMode') else "auto", + debugMode=self.config.debugMode if hasattr(self.config, 'debugMode') else False, ) if result.get("success"): @@ -458,8 +459,13 @@ class TeamsbotService: if not text: return - # Filter out the bot's own speech from AI triggering. + # Filter out the bot's own speech entirely — captions of the bot's + # own voice come back as garbled text (e.g. German TTS → English caption) + # which pollutes the context buffer and confuses AI analysis. isBotSpeaker = self._isBotSpeaker(speaker) + if isBotSpeaker: + logger.debug(f"Session {sessionId}: Ignoring own bot caption from: [{speaker}] {text[:80]}...") + return # Differential transcript writing: # If the same speaker is still talking and the new text is a @@ -538,11 +544,6 @@ class TeamsbotService: "isContinuation": isContinuation, }) - # Skip AI analysis for bot's own speech (prevents feedback loop) - if isBotSpeaker: - logger.debug(f"Session {sessionId}: Skipping AI trigger for bot's own speech: [{speaker}] {text[:60]}...") - return - # Check if AI analysis should be triggered (only for final transcripts) if not isFinal: return diff --git a/modules/routes/routeAdminUserAccessOverview.py b/modules/routes/routeAdminUserAccessOverview.py index 3dd47342..758eff65 100644 --- a/modules/routes/routeAdminUserAccessOverview.py +++ b/modules/routes/routeAdminUserAccessOverview.py @@ -531,6 +531,34 @@ def getEffectivePermissions( try: interface = getRootInterface() + # MandateAdmin: verify the requested user shares at least one admin mandate + if not context.hasSysAdminRole: + adminMandateIds = [] + adminUserMandates = interface.getUserMandates(str(context.user.id)) + for um in adminUserMandates: + umId = getattr(um, 'id', None) + mid = getattr(um, 'mandateId', None) + if not umId or not mid: + continue + roleIds = interface.getRoleIdsForUserMandate(str(umId)) + for roleId in roleIds: + role = interface.getRole(roleId) + if role and role.roleLabel == "admin" and not role.featureInstanceId: + adminMandateIds.append(str(mid)) + break + + if not adminMandateIds: + raise HTTPException(status_code=403, detail="Insufficient permissions") + + userInAdminMandate = False + for mid in adminMandateIds: + if _isUserInMandate(interface, userId, mid): + userInAdminMandate = True + break + + if not userInAdminMandate: + raise HTTPException(status_code=403, detail="Benutzer gehört nicht zu Ihrem Mandate") + # Get user user = interface.getUser(userId) if not user: diff --git a/modules/routes/routeSystem.py b/modules/routes/routeSystem.py index 1a2ad6bf..e896c4f3 100644 --- a/modules/routes/routeSystem.py +++ b/modules/routes/routeSystem.py @@ -336,6 +336,33 @@ def _getInstanceViewPermissions( return permissions # Fail-safe: no permissions on error +def _filterItems( + items: List[Dict[str, Any]], + language: str, + isSysAdmin: bool, + roleIds: List[str], + hasGlobalPermission: bool +) -> List[Dict[str, Any]]: + """Filter and format navigation items based on permissions.""" + filteredItems = [] + for item in items: + if item.get("adminOnly") and not isSysAdmin: + if not hasGlobalPermission and not _checkUiPermission(roleIds, item["objectKey"]): + continue + if item.get("sysAdminOnly") and not isSysAdmin: + continue + if item.get("public"): + filteredItems.append(_formatBlockItem(item, language)) + continue + if isSysAdmin: + filteredItems.append(_formatBlockItem(item, language)) + continue + if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]): + filteredItems.append(_formatBlockItem(item, language)) + filteredItems.sort(key=lambda i: i["order"]) + return filteredItems + + def _buildStaticBlocks( language: str, isSysAdmin: bool, @@ -346,40 +373,54 @@ def _buildStaticBlocks( Build static navigation blocks from NAVIGATION_SECTIONS. Returns list of blocks with items filtered by permissions. + Supports subgroups within sections. """ blocks = [] for section in NAVIGATION_SECTIONS: - # Filter items based on UI AccessRules (ui.admin.*, ui.billing.*, etc.) - filteredItems = [] - for item in section.get("items", []): - # Public items are always visible - if item.get("public"): - filteredItems.append(_formatBlockItem(item, language)) - continue - - # SysAdmin-only items (e.g. automation-events) require isSysAdmin - if item.get("sysAdminOnly") and not isSysAdmin: - continue - - # Check UI AccessRule for this objectKey - # Roles with item=None rule (e.g. sysadmin) get access to everything - # Roles with specific ui.admin.* rules get access to those items - if hasGlobalPermission or _checkUiPermission(roleIds, item["objectKey"]): - filteredItems.append(_formatBlockItem(item, language)) + if section.get("adminOnly") and not isSysAdmin: + continue - # Only include section if it has visible items - if filteredItems: - # Sort items by order - filteredItems.sort(key=lambda i: i["order"]) + # Handle sections with subgroups + if "subgroups" in section: + filteredSubgroups = [] + for subgroup in section["subgroups"]: + subItems = _filterItems( + subgroup.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission + ) + if subItems: + filteredSubgroups.append({ + "id": subgroup["id"], + "title": subgroup["title"].get(language, subgroup["title"].get("en", subgroup["id"])), + "order": subgroup.get("order", 50), + "items": subItems, + }) - blocks.append({ - "type": "static", - "id": section["id"], - "title": section["title"].get(language, section["title"].get("en", section["id"])), - "order": section.get("order", 50), - "items": filteredItems, - }) + filteredSubgroups.sort(key=lambda s: s["order"]) + + if filteredSubgroups: + blocks.append({ + "type": "static", + "id": section["id"], + "title": section["title"].get(language, section["title"].get("en", section["id"])), + "order": section.get("order", 50), + "items": [], + "subgroups": filteredSubgroups, + }) + else: + # Standard flat section + filteredItems = _filterItems( + section.get("items", []), language, isSysAdmin, roleIds, hasGlobalPermission + ) + + if filteredItems: + blocks.append({ + "type": "static", + "id": section["id"], + "title": section["title"].get(language, section["title"].get("en", section["id"])), + "order": section.get("order", 50), + "items": filteredItems, + }) return blocks diff --git a/modules/system/mainSystem.py b/modules/system/mainSystem.py index f8b6edf2..75a92401 100644 --- a/modules/system/mainSystem.py +++ b/modules/system/mainSystem.py @@ -106,132 +106,176 @@ NAVIGATION_SECTIONS = [ }, ], }, + # ─── Administration (with subgroups) ─── + # Access control is at item level, NOT section level. + # Groups auto-hide if 0 visible pages for the user. { "id": "admin", "title": {"en": "ADMINISTRATION", "de": "ADMINISTRATION", "fr": "ADMINISTRATION"}, "order": 200, - "adminOnly": True, - "items": [ + "subgroups": [ + # ── Wizards ── { - "id": "admin-users", - "objectKey": "ui.admin.users", - "label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"}, - "icon": "FaUsers", - "path": "/admin/users", + "id": "admin-wizards", + "title": {"en": "Wizards", "de": "Wizards", "fr": "Assistants"}, "order": 10, - "adminOnly": True, + "items": [ + { + "id": "admin-mandate-wizard", + "objectKey": "ui.admin.mandateWizard", + "label": {"en": "Mandate Wizard", "de": "Mandanten-Wizard", "fr": "Assistant mandat"}, + "icon": "FaMagic", + "path": "/admin/mandate-wizard", + "order": 10, + "adminOnly": True, + }, + { + "id": "admin-invitation-wizard", + "objectKey": "ui.admin.invitationWizard", + "label": {"en": "Invitation Wizard", "de": "Einladungs-Wizard", "fr": "Assistant d'invitation"}, + "icon": "FaEnvelopeOpenText", + "path": "/admin/invitation-wizard", + "order": 20, + "adminOnly": True, + }, + ], }, + # ── Users ── { - "id": "admin-invitations", - "objectKey": "ui.admin.invitations", - "label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"}, - "icon": "FaEnvelopeOpenText", - "path": "/admin/invitations", - "order": 12, - "adminOnly": True, - }, - { - "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"}, - "icon": "FaClipboardList", - "path": "/admin/user-access-overview", - "order": 14, - "adminOnly": True, - }, - { - "id": "admin-mandates", - "objectKey": "ui.admin.mandates", - "label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"}, - "icon": "FaBuilding", - "path": "/admin/mandates", + "id": "admin-users-group", + "title": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"}, "order": 20, - "adminOnly": True, + "items": [ + { + "id": "admin-users", + "objectKey": "ui.admin.users", + "label": {"en": "Users", "de": "Benutzer", "fr": "Utilisateurs"}, + "icon": "FaUsers", + "path": "/admin/users", + "order": 10, + "adminOnly": True, + }, + { + "id": "admin-invitations", + "objectKey": "ui.admin.invitations", + "label": {"en": "User Invitations", "de": "Benutzer-Einladungen", "fr": "Invitations utilisateurs"}, + "icon": "FaEnvelopeOpenText", + "path": "/admin/invitations", + "order": 20, + "adminOnly": True, + }, + { + "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"}, + "icon": "FaClipboardList", + "path": "/admin/user-access-overview", + "order": 30, + "adminOnly": True, + }, + { + "id": "admin-billing", + "objectKey": "ui.admin.billing", + "label": {"en": "Billing Administration", "de": "Billing-Verwaltung", "fr": "Administration de facturation"}, + "icon": "FaMoneyBillAlt", + "path": "/admin/billing", + "order": 40, + "adminOnly": True, + "sysAdminOnly": True, + }, + ], }, + # ── System ── { - "id": "admin-user-mandates", - "objectKey": "ui.admin.userMandates", - "label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"}, - "icon": "FaUserFriends", - "path": "/admin/user-mandates", - "order": 25, - "adminOnly": True, - }, - { - "id": "admin-access", - "objectKey": "ui.admin.access", - "label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"}, - "icon": "FaBuilding", - "path": "/admin/access", + "id": "admin-system-group", + "title": {"en": "System", "de": "System", "fr": "Système"}, "order": 30, - "adminOnly": True, - }, - { - "id": "admin-roles", - "objectKey": "ui.admin.roles", - "label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"}, - "icon": "FaUserTag", - "path": "/admin/mandate-roles", - "order": 40, - "adminOnly": True, - }, - { - "id": "admin-mandate-role-permissions", - "objectKey": "ui.admin.mandateRolePermissions", - "label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"}, - "icon": "FaKey", - "path": "/admin/mandate-role-permissions", - "order": 45, - "adminOnly": True, - }, - { - "id": "admin-feature-instances", - "objectKey": "ui.admin.featureInstances", - "label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"}, - "icon": "FaCubes", - "path": "/admin/feature-instances", - "order": 48, - "adminOnly": True, - }, - { - "id": "admin-feature-roles", - "objectKey": "ui.admin.featureRoles", - "label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"}, - "icon": "FaShieldAlt", - "path": "/admin/feature-roles", - "order": 50, - "adminOnly": True, - "sysAdminOnly": True, - }, - { - "id": "admin-billing", - "objectKey": "ui.admin.billing", - "label": {"en": "Billing Administration", "de": "Billing-Verwaltung", "fr": "Administration de facturation"}, - "icon": "FaMoneyBillAlt", - "path": "/admin/billing", - "order": 60, - "adminOnly": True, - "sysAdminOnly": True, - }, - { - "id": "admin-automation-events", - "objectKey": "ui.admin.automationEvents", - "label": {"en": "Automation Events", "de": "Automation Events", "fr": "Événements d'automatisation"}, - "icon": "FaClock", - "path": "/admin/automation-events", - "order": 65, - "adminOnly": True, - "sysAdminOnly": True, - }, - { - "id": "admin-logs", - "objectKey": "ui.admin.logs", - "label": {"en": "Logs", "de": "Logs", "fr": "Logs"}, - "icon": "FaFileAlt", - "path": "/admin/logs", - "order": 70, - "adminOnly": True, - "sysAdminOnly": True, + "items": [ + { + "id": "admin-roles", + "objectKey": "ui.admin.roles", + "label": {"en": "Roles", "de": "Rollen", "fr": "Rôles"}, + "icon": "FaUserTag", + "path": "/admin/mandate-roles", + "order": 10, + "adminOnly": True, + }, + { + "id": "admin-mandate-role-permissions", + "objectKey": "ui.admin.mandateRolePermissions", + "label": {"en": "Role Permissions", "de": "Rollen-Berechtigungen", "fr": "Permissions des rôles"}, + "icon": "FaKey", + "path": "/admin/mandate-role-permissions", + "order": 20, + "adminOnly": True, + }, + { + "id": "admin-mandates", + "objectKey": "ui.admin.mandates", + "label": {"en": "Mandates", "de": "Mandanten", "fr": "Mandats"}, + "icon": "FaBuilding", + "path": "/admin/mandates", + "order": 30, + "adminOnly": True, + }, + { + "id": "admin-user-mandates", + "objectKey": "ui.admin.userMandates", + "label": {"en": "Mandate Members", "de": "Mandanten-Mitglieder", "fr": "Membres du mandat"}, + "icon": "FaUserFriends", + "path": "/admin/user-mandates", + "order": 40, + "adminOnly": True, + }, + { + "id": "admin-access", + "objectKey": "ui.admin.access", + "label": {"en": "Access Management", "de": "Zugriffsverwaltung", "fr": "Gestion des accès"}, + "icon": "FaBuilding", + "path": "/admin/access", + "order": 50, + "adminOnly": True, + }, + { + "id": "admin-feature-instances", + "objectKey": "ui.admin.featureInstances", + "label": {"en": "Feature Instances", "de": "Feature-Instanzen", "fr": "Instances de features"}, + "icon": "FaCubes", + "path": "/admin/feature-instances", + "order": 60, + "adminOnly": True, + }, + { + "id": "admin-feature-roles", + "objectKey": "ui.admin.featureRoles", + "label": {"en": "Feature Role Templates", "de": "Features Rollen-Vorlagen", "fr": "Modèles de rôles features"}, + "icon": "FaShieldAlt", + "path": "/admin/feature-roles", + "order": 70, + "adminOnly": True, + "sysAdminOnly": True, + }, + { + "id": "admin-automation-events", + "objectKey": "ui.admin.automationEvents", + "label": {"en": "Automation Events", "de": "Automation Events", "fr": "Événements d'automatisation"}, + "icon": "FaClock", + "path": "/admin/automation-events", + "order": 80, + "adminOnly": True, + "sysAdminOnly": True, + }, + { + "id": "admin-logs", + "objectKey": "ui.admin.logs", + "label": {"en": "Logs", "de": "Logs", "fr": "Logs"}, + "icon": "FaFileAlt", + "path": "/admin/logs", + "order": 90, + "adminOnly": True, + "sysAdminOnly": True, + }, + ], }, ], }, @@ -255,6 +299,7 @@ def _buildUiObjectsFromNavigation() -> List[Dict[str, Any]]: """Build UI_OBJECTS list from NAVIGATION_SECTIONS for RBAC registration.""" uiObjects = [] for section in NAVIGATION_SECTIONS: + # Process direct items for item in section.get("items", []): uiObjects.append({ "objectKey": item["objectKey"], @@ -268,6 +313,22 @@ def _buildUiObjectsFromNavigation() -> List[Dict[str, Any]]: "icon": item["icon"], } }) + # Process subgroups (nested items within section) + for subgroup in section.get("subgroups", []): + for item in subgroup.get("items", []): + uiObjects.append({ + "objectKey": item["objectKey"], + "label": item["label"], + "meta": { + "area": section["id"], + "subgroup": subgroup["id"], + "public": item.get("public", False), + "adminOnly": item.get("adminOnly", False), + "deprecated": item.get("deprecated", False), + "path": item["path"], + "icon": item["icon"], + } + }) return uiObjects