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