# Copyright (c) 2025 Patrick Motsch # All rights reserved. """ CommCoach Feature Container - Main Module. Handles feature initialization and RBAC catalog registration. """ import logging 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_ICON = "mdi-account-voice" UI_OBJECTS = [ { "objectKey": "ui.feature.commcoach.dashboard", "label": {"en": "Dashboard", "de": "Dashboard", "fr": "Tableau de bord"}, "meta": {"area": "dashboard"} }, { "objectKey": "ui.feature.commcoach.coaching", "label": {"en": "Coaching & Dossier", "de": "Coaching & Dossier", "fr": "Coaching & Dossier"}, "meta": {"area": "coaching"} }, { "objectKey": "ui.feature.commcoach.settings", "label": {"en": "Settings", "de": "Einstellungen", "fr": "Parametres"}, "meta": {"area": "settings"} }, ] DATA_OBJECTS = [ { "objectKey": "data.feature.commcoach.CoachingContext", "label": {"en": "Coaching Context", "de": "Coaching-Kontext", "fr": "Contexte coaching"}, "meta": {"table": "CoachingContext", "fields": ["id", "title", "category", "status"]} }, { "objectKey": "data.feature.commcoach.CoachingSession", "label": {"en": "Coaching Session", "de": "Coaching-Session", "fr": "Session coaching"}, "meta": {"table": "CoachingSession", "fields": ["id", "contextId", "status", "summary"]} }, { "objectKey": "data.feature.commcoach.CoachingMessage", "label": {"en": "Coaching Message", "de": "Coaching-Nachricht", "fr": "Message coaching"}, "meta": {"table": "CoachingMessage", "fields": ["id", "sessionId", "role", "content"]} }, { "objectKey": "data.feature.commcoach.CoachingTask", "label": {"en": "Coaching Task", "de": "Coaching-Aufgabe", "fr": "Tache coaching"}, "meta": {"table": "CoachingTask", "fields": ["id", "contextId", "title", "status"]} }, { "objectKey": "data.feature.commcoach.CoachingScore", "label": {"en": "Coaching Score", "de": "Coaching-Score", "fr": "Score coaching"}, "meta": {"table": "CoachingScore", "fields": ["id", "dimension", "score", "trend"]} }, { "objectKey": "data.feature.commcoach.CoachingUserProfile", "label": {"en": "User Profile", "de": "Benutzerprofil", "fr": "Profil utilisateur"}, "meta": {"table": "CoachingUserProfile", "fields": ["id", "userId", "preferredLanguage"]} }, { "objectKey": "data.feature.commcoach.CoachingPersona", "label": {"en": "Coaching Persona", "de": "Coaching-Persona", "fr": "Persona coaching"}, "meta": {"table": "CoachingPersona", "fields": ["id", "key", "label", "gender"]} }, { "objectKey": "data.feature.commcoach.CoachingDocument", "label": {"en": "Coaching Document", "de": "Coaching-Dokument", "fr": "Document coaching"}, "meta": {"table": "CoachingDocument", "fields": ["id", "contextId", "fileName"]} }, { "objectKey": "data.feature.commcoach.CoachingBadge", "label": {"en": "Coaching Badge", "de": "Coaching-Auszeichnung", "fr": "Badge coaching"}, "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"}, "meta": {"wildcard": True} }, ] RESOURCE_OBJECTS = [ { "objectKey": "resource.feature.commcoach.context.create", "label": {"en": "Create Context", "de": "Kontext erstellen", "fr": "Creer contexte"}, "meta": {"endpoint": "/api/commcoach/{instanceId}/contexts", "method": "POST"} }, { "objectKey": "resource.feature.commcoach.context.archive", "label": {"en": "Archive Context", "de": "Kontext archivieren", "fr": "Archiver contexte"}, "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"}, "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"}, "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"}, "meta": {"endpoint": "/api/commcoach/{instanceId}/contexts/{contextId}/tasks", "method": "POST"} }, ] TEMPLATE_ROLES = [ { "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" }, "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"}, {"context": "DATA", "item": "data.feature.commcoach.CoachingMessage", "view": True, "read": "m", "create": "m", "update": "n", "delete": "n"}, {"context": "DATA", "item": "data.feature.commcoach.CoachingTask", "view": True, "read": "m", "create": "m", "update": "m", "delete": "m"}, {"context": "DATA", "item": "data.feature.commcoach.CoachingScore", "view": True, "read": "m", "create": "n", "update": "n", "delete": "n"}, {"context": "DATA", "item": "data.feature.commcoach.CoachingUserProfile", "view": True, "read": "m", "create": "m", "update": "m", "delete": "n"}, {"context": "RESOURCE", "item": "resource.feature.commcoach.context.create", "view": True}, {"context": "RESOURCE", "item": "resource.feature.commcoach.context.archive", "view": True}, {"context": "RESOURCE", "item": "resource.feature.commcoach.session.start", "view": True}, {"context": "RESOURCE", "item": "resource.feature.commcoach.session.complete", "view": True}, {"context": "RESOURCE", "item": "resource.feature.commcoach.task.manage", "view": True}, ] }, ] def getFeatureDefinition() -> Dict[str, Any]: return { "code": FEATURE_CODE, "label": FEATURE_LABEL, "icon": FEATURE_ICON, "autoCreateInstance": True, } def getUiObjects() -> List[Dict[str, Any]]: return UI_OBJECTS def getResourceObjects() -> List[Dict[str, Any]]: return RESOURCE_OBJECTS def getTemplateRoles() -> List[Dict[str, Any]]: return TEMPLATE_ROLES def getDataObjects() -> List[Dict[str, Any]]: return DATA_OBJECTS def registerFeature(catalogService) -> bool: try: for uiObj in UI_OBJECTS: catalogService.registerUiObject( featureCode=FEATURE_CODE, objectKey=uiObj["objectKey"], label=uiObj["label"], meta=uiObj.get("meta") ) for resObj in RESOURCE_OBJECTS: catalogService.registerResourceObject( featureCode=FEATURE_CODE, objectKey=resObj["objectKey"], label=resObj["label"], meta=resObj.get("meta") ) for dataObj in DATA_OBJECTS: catalogService.registerDataObject( featureCode=FEATURE_CODE, objectKey=dataObj["objectKey"], label=dataObj["label"], meta=dataObj.get("meta") ) _syncTemplateRolesToDb() _seedBuiltinPersonas() _registerScheduler() logger.info(f"Feature '{FEATURE_CODE}' registered {len(UI_OBJECTS)} UI, {len(RESOURCE_OBJECTS)} resource, {len(DATA_OBJECTS)} data objects") return True except Exception as e: logger.error(f"Failed to register feature '{FEATURE_CODE}': {e}") return False def _seedBuiltinPersonas(): """Seed builtin roleplay personas into the database.""" try: from .serviceCommcoachPersonas import seedBuiltinPersonas from .interfaceFeatureCommcoach import CommcoachInterface from modules.interfaces.interfaceDbManagement import getInterface as getDbInterface db = getDbInterface() interface = CommcoachInterface(db) seedBuiltinPersonas(interface) except Exception as e: logger.warning(f"CommCoach persona seeding failed (non-fatal): {e}") def _registerScheduler(): """Register CommCoach scheduled jobs (daily reminders).""" try: from modules.shared.eventManagement import eventManager from .serviceCommcoachScheduler import registerScheduledJobs registerScheduledJobs(eventManager) except Exception as e: logger.warning(f"CommCoach scheduler registration failed (non-fatal): {e}") def _syncTemplateRolesToDb() -> int: try: from modules.interfaces.interfaceDbApp import getRootInterface from modules.datamodels.datamodelRbac import Role, AccessRule, AccessRuleContext rootInterface = getRootInterface() existingRoles = rootInterface.getRolesByFeatureCode(FEATURE_CODE) templateRoles = [r for r in existingRoles if r.mandateId is None] existingRoleLabels = {r.roleLabel: str(r.id) for r in templateRoles} createdCount = 0 for roleTemplate in TEMPLATE_ROLES: roleLabel = roleTemplate["roleLabel"] if roleLabel in existingRoleLabels: roleId = existingRoleLabels[roleLabel] _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) else: newRole = Role( roleLabel=roleLabel, description=roleTemplate.get("description", {}), featureCode=FEATURE_CODE, mandateId=None, featureInstanceId=None, isSystemRole=False ) createdRole = rootInterface.db.recordCreate(Role, newRole.model_dump()) roleId = createdRole.get("id") _ensureAccessRulesForRole(rootInterface, roleId, roleTemplate.get("accessRules", [])) logger.info(f"Created template role '{roleLabel}' with ID {roleId}") createdCount += 1 if createdCount > 0: logger.info(f"Feature '{FEATURE_CODE}': Created {createdCount} template roles") return createdCount except Exception as e: logger.error(f"Error syncing template roles for feature '{FEATURE_CODE}': {e}") return 0 def _ensureAccessRulesForRole(rootInterface, roleId: str, ruleTemplates: List[Dict[str, Any]]) -> int: from modules.datamodels.datamodelRbac import AccessRule, AccessRuleContext existingRules = rootInterface.getAccessRulesByRole(roleId) existingSignatures = set() for rule in existingRules: sig = (rule.context.value if rule.context else None, rule.item) existingSignatures.add(sig) createdCount = 0 for template in ruleTemplates: context = template.get("context", "UI") item = template.get("item") sig = (context, item) if sig in existingSignatures: continue if context == "UI": contextEnum = AccessRuleContext.UI elif context == "DATA": contextEnum = AccessRuleContext.DATA elif context == "RESOURCE": contextEnum = AccessRuleContext.RESOURCE else: contextEnum = context newRule = AccessRule( roleId=roleId, context=contextEnum, item=item, view=template.get("view", False), read=template.get("read"), create=template.get("create"), update=template.get("update"), delete=template.get("delete"), ) rootInterface.db.recordCreate(AccessRule, newRule.model_dump()) createdCount += 1 if createdCount > 0: logger.debug(f"Created {createdCount} AccessRules for role {roleId}") return createdCount