Files
leocrm/docs/bauplan-unified-messaging.md
T
Agent Zero cc3ac9a43d feat: Unified Messaging System — kommunikation plugin, AI/Proactive/System participants, MessageSidebar, Rich Content Renderer
Phase 1: Backend plugin kommunikation (13 files, 10 tables, REST API, WebSocket, RBAC, DMS Bridge, Participant Registry, Mini-App Registry, Search Provider)
Phase 2: AI plugins as participants (ai_assistant + ai_proactive dock as participants, heartbeat job)
Phase 3: system_notif plugin (system events → chat messages, pinned System room)
Phase 4: Frontend MessageSidebar (replaces AISidebar, same design, comm API client, WebSocket hook, commStore)
Phase 5: Rich Content Block Renderer (11 components: Markdown, HTML, Image, Audio, Video, File, ActionCard, ContactCard, MiniApp, BlockRenderer)
BasePlugin: added services property + _container in on_activate
2026-07-22 01:22:15 +02:00

66 KiB

Bauplan: Unified Messaging System für LeoCRM

Vollständiger Bauplan unter Berücksichtigung aller bestehenden Systeme, Plugin-Architektur, RBAC, DMS, Suche und aller Nutzer-Anforderungen.


1. Bestandsaufnahme — Was existiert bereits

1.1 Plugin-System

  • BasePlugin mit Lifecycle Hooks: on_install, on_activate, on_deactivate, on_uninstall
  • PluginManifest mit: name, version, dependencies, routes, events, migrations, permissions, is_core
  • PluginRegistry mit: Discovery, Install, Activate, Deactivate, Uninstall, Topological Sort, Dependency Resolution
  • EventBus — in-process pub/sub, async handlers
  • MigrationRunner — SQL-Migrations pro Plugin
  • ServiceContainer — shared services

1.2 Bestehende Plugins

Plugin Status Abhängigkeiten is_core Eigene UI Eigene Tabellen
ai_assistant Aktiv Ja (ChatWindow, Sessions) ai_chat_sessions, ai_chat_messages, ai_chat_attachments, ai_chat_folders, ai_providers, ai_models, ai_presets, ai_agents
ai_proactive Aktiv ai_assistant, unified_search Ja (SuggestionList, SSE) ai_proactive_suggestions, ai_proactive_context_log, ai_proactive_settings
dms Aktiv permissions Ja (DMS Page) folders, files
mail Aktiv Ja (Mail Page) mails
calendar Aktiv Ja (Calendar Page) calendar_events
unified_search Aktiv Ja (Search Page) search_index
permissions Aktiv Ja (Settings) permissions, role_permissions
report_generator Aktiv Ja

1.3 Notification-System (Core, nicht Plugin)

  • Modelle: Notification, NotificationType, NotificationPreference
  • Core Service: app/core/notifications.pycreate_notification()
  • Routes: app/routes/notifications.py
  • Plugin Integration: Plugins deklarieren Notification-Types via get_notification_types()
  • Sync: PluginRegistry.sync_notification_types() → DB
  • Frontend: Aktuell im uiStore als string[] (Mock-Daten)

1.4 RBAC / Permission System

  • Permission Registry: app/core/permission_registry.py
  • Permission Check: app/core/permissions.pycheck_permission(), get_cached_permissions()
  • FastAPI Deps: require_permission(), require_admin(), require_write()
  • Modelle: Permission, Role, RolePermission (permissions plugin)
  • User Session: get_current_user → liefert permissions[], denied_permissions[], field_permissions{}, is_system_admin
  • Pattern: module:action (z.B. comm:read, comm:write, comm:manage)

1.5 DMS Plugin

  • Modelle: Folder (hierarchisch, tenant-scoped, soft-deletable), File (storage_path auf Disk, mime_type, size_bytes)
  • Storage: /tmp/dms (configurable via DMS_STORAGE_BASE)
  • Sharing: Internal sharing via permissions
  • OnlyOffice: Edit sessions für Office-Dateien
  • Upload: UploadFile → Disk + DB-Eintrag

1.6 AI Assistant Plugin (Detail)

  • Provider-Verwaltung: Multi-Provider (OpenAI, Ollama, etc.), API-Keys in DB
  • Modelle/Preset/Agents: Konfigurierbar pro Tenant
  • Chat Sessions: Eigene Tabellen, Streaming via SSE
  • Tool Registry: Plugins können AI-Tools registrieren (tool_registry.py)
  • Frontend: ChatWindow Komponente, AIAssistant Page, AISettings Page

1.7 AI Proactive Plugin (Detail)

  • Suggestion Engine: Kontext-Änderung → LLM → Suggestion
  • SSE Push: _sse_queues dict, push_suggestion()
  • Background Jobs: deep_analysis via ARQ
  • Settings: Pro-User (enabled, categories, confidence_threshold, rate_limit, model)
  • Frontend: SuggestionList Komponente, ProactiveAISettings Page
  • Events: context.view_changed, context.entity_selected

1.8 Frontend Sidebar Struktur

  • Linke Sidebar: Navigation (Dashboard, Kontakte, Kalender, Dateien, Mail)
  • Rechte Sidebar (AISidebar): 5 Tabs — KI Chat, Live KI, Benachrichtigungen, Team, Chat
  • uiStore: aiSidebarCollapsed, aiSidebarTab, notifications: string[]
  • Komponenten: ChatWindow, SuggestionList, TeamPanel, ChatPanel (placeholder)

1.9 AI Copilot System (Core, nicht Plugin)

  • Modelle: AIConversation, AIMessage (tenant-scoped)
  • Service: ai_copilot_service — NL → proposed actions → execute
  • Routes: /api/v1/ai/copilot/query, /api/v1/ai/copilot/execute
  • Frontend: AIAssistant Page nutzt Copilot

2. Architektur — Plugin-Ebenen

Übersicht

┌─────────────────────────────────────────────────────────────────┐
│                    Frontend                                      │
│  ┌──────────────┐  ┌───────────────────┐  ┌──────────────────┐  │
│  │ Linke Sidebar │  │ Hauptbereich       │  │ Rechte Sidebar    │  │
│  │ Navigation    │  │ (CRM Seiten)       │  │ = MessageSidebar   │  │
│  │              │  │                    │  │ Raum-Liste + Feed  │  │
│  │              │  │                    │  │ + Eingabefeld      │  │
│  └──────────────┘  └───────────────────┘  └──────────────────┘  │
└─────────────────────────────────────────────────────────────────┘
         │                    │                        │
         │                    │                        │ WebSocket
┌────────┴────────────────────┴────────────────────────┴──────────┐
│              Plugin: kommunikation (Core)                         │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐  │
│  │ Conversations│ │ Messages     │ │ Participant Registry     │  │
│  │ + Räume      │ │ + Blocks     │ │ (Andockpunkt)             │  │
│  │ + Locking    │ │ + Attachments│ │                          │  │
│  │ + RBAC       │ │ + Reactions  │ │ register(type, handler)  │  │
│  └──────────────┘ └──────────────┘ └──────────────────────────┘  │
│  ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────┐  │
│  │ WebSocket Mgr│ │ DMS Bridge   │ │ Mini-App Registry        │  │
│  │ (Real-time)  │ │ (File Store) │ │ (Plugin Blocks)          │  │
│  └──────────────┘ └──────────────┘ └──────────────────────────┘  │
└──────────────────────────┬──────────────────────────────────────┘
                           │ EventBus + Participant Registry
┌──────────────────────────┴──────────────────────────────────────┐
│  Plugin: ai_assistant  │  Plugin: ai_proactive  │  Plugin:       │
│  (Teilnehmer: ai)      │  (Teilnehmer: ai_p)   │  system_notif   │
│  @KI → Response        │  Heartbeat → Kanal    │  (Teilnehmer:   │
│  Keine eigene UI       │  Keine eigene UI      │  system)        │
│  Nutzt comm UI         │  Nutzt comm UI        │  Events→Msg    │
└─────────────────────────────────────────────────────────────────┘
                           │
┌──────────────────────────┴──────────────────────────────────────┐
│  Später: whatsapp_gateway │ telegram_gateway │ email_gateway    │
│  (Teilnehmer: whatsapp)  │ (Teilnehmer: telegram) │ (Teilnehmer: email)│
└─────────────────────────────────────────────────────────────────┘

3. Plugin kommunikation (Core-Plugin)

3.1 Manifest

PluginManifest(
    name="kommunikation",
    version="1.0.0",
    display_name="Kommunikation",
    description="Unified Messaging: Chat, KI, System, Messenger — alles ist ein Teilnehmer",
    dependencies=["permissions", "dms"],
    routes=[
        PluginRouteDef(path="/api/v1/comm", module="...routes", router_attr="router"),
    ],
    events=[
        "message.received",
        "message.sent",
        "conversation.created",
        "conversation.updated",
        "participant.joined",
        "participant.left",
        "reaction.added",
    ],
    migrations=["0001_initial.sql"],
    permissions=[
        "comm:read",        # Nachrichten/Konversationen lesen
        "comm:write",       # Nachrichten schreiben
        "comm:create",      # Konversationen erstellen
        "comm:manage",      # Teilnehmer verwalten, Räume locken
        "comm:admin",        # Konversation-Admin (Rollen vergeben)
        "comm:delete",       # Nachrichten/Konversationen löschen
    ],
    is_core=True,
)

3.2 Komponenten

app/plugins/builtins/kommunikation/
├── __init__.py
├── plugin.py                    # Plugin-Klasse, Lifecycle
├── manifest.py                  # (in plugin.py)
├── models.py                     # SQLAlchemy-Modelle
├── schemas.py                    # Pydantic-Schemas
├── routes.py                     # REST-API + WebSocket
├── services.py                   # Business Logic
├── participant_registry.py      # Teilnehmer-Interface + Registry
├── content_types.py              # Block-Typ-Definitionen
├── miniapp_registry.py           # Mini-App Registry
├── websocket_manager.py          # WebSocket-Verbindungs-Manager
├── dms_bridge.py                 # DMS-Integration für Attachments
├── rbac.py                       # Chat-interne RBAC-Logik
├── search_provider.py            # unified_search Provider
└── migrations/
    └── 0001_initial.sql

3.3 Datenmodell

Tabelle comm_conversations

CREATE TABLE comm_conversations (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    title           TEXT,                          -- Raum-Name (NULL = Direkt-Chat)
    title_set_by    UUID,                          -- user_id die den Titel gesetzt hat
    is_pinned       BOOLEAN DEFAULT FALSE,         -- von User angepinnt
    is_locked       BOOLEAN DEFAULT FALSE,         -- Plugin-Lock (User kann nicht ändern)
    locked_by       TEXT,                          -- Plugin-Name der gelockt hat
    is_direct       BOOLEAN DEFAULT FALSE,         -- 1:1 vs Gruppe/Raum
    is_archived     BOOLEAN DEFAULT FALSE,         -- archiviert
    created_by      UUID,                          -- user_id oder plugin_name
    created_by_type TEXT DEFAULT 'user',           -- 'user', 'plugin', 'system'
    last_msg_at     TIMESTAMPTZ,
    last_msg_preview    TEXT,                      -- für Listen-Anzeige
    last_msg_sender_type TEXT,                     -- für Icon in Liste
    metadata        JSONB DEFAULT '{}',            -- z.B. {"pinned_by": "user_id"}
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ix_comm_conversations_tenant ON comm_conversations(tenant_id);
CREATE INDEX ix_comm_conversations_tenant_pinned ON comm_conversations(tenant_id, is_pinned);
CREATE INDEX ix_comm_conversations_last_msg ON comm_conversations(tenant_id, last_msg_at DESC);

Tabelle comm_participants

CREATE TABLE comm_participants (
    id                UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id         UUID NOT NULL,
    conversation_id   UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
    participant_id    UUID,                          -- user_id (NULL für ai/system/gateways)
    participant_type   TEXT NOT NULL,                 -- 'user', 'ai', 'ai_proactive', 'system', 'whatsapp', ...
    display_name       TEXT,                          -- Override-Name
    role              TEXT DEFAULT 'member',         -- 'admin', 'member', 'reader'
    joined_at         TIMESTAMPTZ DEFAULT NOW(),
    left_at           TIMESTAMPTZ,
    UNIQUE(conversation_id, participant_id, participant_type)
);
CREATE INDEX ix_comm_participants_tenant_conv ON comm_participants(tenant_id, conversation_id);
CREATE INDEX ix_comm_participants_tenant_user ON comm_participants(tenant_id, participant_id);

Tabelle comm_messages

CREATE TABLE comm_messages (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
    sender_id       UUID,                            -- user_id (NULL für ai/system)
    sender_type     TEXT NOT NULL,                   -- 'user', 'ai', 'ai_proactive', 'system', ...
    content         TEXT NOT NULL DEFAULT '',        -- Text-Inhalt (Fallback)
    content_format  TEXT DEFAULT 'text',             -- 'text', 'markdown', 'html'
    metadata        JSONB DEFAULT '{}',              -- typ-spezifische Daten
    reply_to_id     UUID REFERENCES comm_messages(id) ON DELETE SET NULL,
    is_pinned       BOOLEAN DEFAULT FALSE,           -- Nachricht angepinnt im Feed
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    read_at         TIMESTAMPTZ,                     -- veraltet → comm_message_reads
    edited_at       TIMESTAMPTZ,
    deleted_at      TIMESTAMPTZ
);
CREATE INDEX ix_comm_messages_tenant_conv ON comm_messages(tenant_id, conversation_id, created_at);
CREATE INDEX ix_comm_messages_tenant_sender ON comm_messages(tenant_id, sender_id);

Tabelle comm_message_blocks (Rich Content)

CREATE TABLE comm_message_blocks (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id    UUID NOT NULL,
    message_id   UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
    block_type   TEXT NOT NULL,                     -- 'text', 'markdown', 'html', 'image', 'audio', 'video', 'file', 'action_card', 'contact_card', 'miniapp', ...
    block_data   JSONB NOT NULL,                    -- typ-spezifische strukturierte Daten
    sort_order   INT DEFAULT 0
);
CREATE INDEX ix_comm_blocks_tenant_msg ON comm_message_blocks(tenant_id, message_id);

Tabelle comm_message_attachments (DMS-Referenzen)

CREATE TABLE comm_message_attachments (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id    UUID NOT NULL,
    message_id   UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
    file_id      UUID,                               -- DMS file_id (Referenz)
    file_source  TEXT DEFAULT 'comm',                -- 'comm' (eigener DMS-Bereich) oder 'dms' (externer Verweis)
    file_name    TEXT NOT NULL,
    file_type    TEXT NOT NULL,                      -- MIME type
    file_size    BIGINT,
    thumbnail_path TEXT,                             -- für Bilder/Videos
    metadata     JSONB DEFAULT '{}',
    created_at   TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ix_comm_attachments_tenant_msg ON comm_message_attachments(tenant_id, message_id);

Tabelle comm_message_reactions

CREATE TABLE comm_message_reactions (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id    UUID NOT NULL,
    message_id   UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
    user_id      UUID NOT NULL,
    emoji        TEXT NOT NULL,
    created_at   TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(message_id, user_id, emoji)
);
CREATE INDEX ix_comm_reactions_tenant_msg ON comm_message_reactions(tenant_id, message_id);

Tabelle comm_message_reads (Lese-Status pro User pro Konversation)

CREATE TABLE comm_message_reads (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
    user_id         UUID NOT NULL,
    last_read_msg_id UUID REFERENCES comm_messages(id) ON DELETE SET NULL,
    last_read_at    TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(conversation_id, user_id)
);
CREATE INDEX ix_comm_reads_tenant_user ON comm_message_reads(tenant_id, user_id);

Tabelle comm_conversation_pins (User-spezifisches Pinning)

CREATE TABLE comm_conversation_pins (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
    user_id         UUID NOT NULL,
    pinned_at       TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(conversation_id, user_id)
);
CREATE INDEX ix_comm_pins_tenant_user ON comm_conversation_pins(tenant_id, user_id);

Tabelle comm_message_edits (Bearbeitung-Historie)

CREATE TABLE comm_message_edits (
    id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id    UUID NOT NULL,
    message_id   UUID NOT NULL REFERENCES comm_messages(id) ON DELETE CASCADE,
    old_content  TEXT NOT NULL,
    old_blocks   JSONB DEFAULT '[]',
    edited_by    UUID NOT NULL,
    edited_at    TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX ix_comm_edits_tenant_msg ON comm_message_edits(tenant_id, message_id);

Tabelle comm_conversation_mutes (Stummschaltung pro User)

CREATE TABLE comm_conversation_mutes (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    tenant_id       UUID NOT NULL,
    conversation_id UUID NOT NULL REFERENCES comm_conversations(id) ON DELETE CASCADE,
    user_id         UUID NOT NULL,
    muted_at        TIMESTAMPTZ DEFAULT NOW(),
    UNIQUE(conversation_id, user_id)
);
CREATE INDEX ix_comm_mutes_tenant_user ON comm_conversation_mutes(tenant_id, user_id);

3.4 Raum-Logik

Räume = Benannte Konversationen mit Pinning und Locking

Eigenschaft Wer setzt es Bedeutung
title User oder Plugin Raum-Name. NULL = Direkt-Chat (auto-Titel aus Teilnehmern)
is_pinned (Tabelle comm_conversation_pins) User User-spezifisches Anpinnen für Sortierung
is_locked Plugin Plugin-Lock: User kann Titel/Teilnehmer nicht ändern
locked_by Plugin Plugin-Name der gelockt hat
is_direct System TRUE = 1:1 Chat, FALSE = Gruppe/Raum
is_archived User Archiviert → erscheint nicht in Standard-Liste

Plugin-erstellte Räume:

  • Plugin erstellt Konversation via services.create_conversation()
  • Setzt is_locked=True, locked_by='plugin_name', created_by_type='plugin'
  • Fügt sich selbst als Participant hinzu (participant_type='system' etc.)
  • Fügt User als Participant hinzu (role='reader' oder role='member')
  • Pinnt den Raum für den User (comm_conversation_pins)

Beispiele für Plugin-Räume:

  • system_notif → Raum "System" (locked, gepinnt) — System-Notifications
  • ai_proactive → Raum "Live KI" (locked, gepinnt) — Proaktive Vorschläge + Heartbeat
  • ai_assistant → Raum "Assistent" (locked, gepinnt) — 1:1 KI-Chat

3.5 Participant Registry

class ParticipantHandler(ABC):
    """Interface das Plugins implementieren um als Teilnehmer zu fungieren."""
    
    @abstractmethod
    async def on_message_received(
        self, 
        conversation_id: uuid.UUID,
        message: dict,           # Die neue Nachricht
        conversation: dict,       # Die gesamte Konversation mit Teilnehmern
        mentions: list[str],     # Geparste @Mentions (Teilnehmer-Typen)
        context: dict            # Tenant, user, etc.
    ) -> list[dict] | None:
        """Wird aufgerufen wenn eine neue Nachricht in einer Konversation
        ankommt, an der dieser Teilnehmer beteiligt ist.
        
        Rückgabe: Optional Liste von neuen Nachrichten (z.B. AI-Response).
        Für reine Leser (system) → return None.
        Für reaktive Teilnehmer (ai) → return [message_dict, ...].
        """
        pass
    
    @abstractmethod
    def get_participant_info(self) -> dict:
        """Metadaten: display_name, avatar_url, capabilities, description."""
        pass


class ParticipantRegistry:
    """Global registry für Plugin-Teilnehmer."""
    
    def __init__(self):
        self._handlers: dict[str, ParticipantHandler] = {}
    
    def register(self, participant_type: str, handler: ParticipantHandler) -> None:
        self._handlers[participant_type] = handler
    
    def unregister(self, participant_type: str) -> None:
        self._handlers.pop(participant_type, None)
    
    def get_handler(self, participant_type: str) -> ParticipantHandler | None:
        return self._handlers.get(participant_type)
    
    def list_types(self) -> list[str]:
        return list(self._handlers.keys())


# Global instance
_registry = ParticipantRegistry()

def get_participant_registry() -> ParticipantRegistry:
    return _registry

3.6 DMS Bridge

Das kommunikation Plugin nutzt das DMS für Datei-Speicherung:

class DmsBridge:
    """Bridge zum DMS Plugin für Attachment-Speicherung."""
    
    COMM_FOLDER_NAME = "_kommunikation"  # Versteckter Root-Ordner
    
    async def ensure_comm_folder(self, db, tenant_id) -> Folder:
        """Stellt sicher dass der Kommunikation-Ordner im DMS existiert."""
        # Prüfe ob Folder existiert
        # Wenn nicht: Erstelle Root-Folder '_kommunikation'
        # Pro Konversation: Sub-Ordner mit Konversations-ID
    
    async def store_attachment(
        self, db, tenant_id, conversation_id, user_id, file: UploadFile
    ) -> dict:
        """Speichert eine Datei im DMS unter _kommunikation/{conversation_id}/."""
        # 1. Ensure conversation sub-folder (created_by = user_id)
        # 2. Upload file to DMS (uploaded_by = user_id)
        # 3. Return file_id + metadata
    
    async def reference_external_file(
        self, db, file_id: uuid.UUID
    ) -> dict:
        """Referenziert eine bereits im DMS existierende Datei."""
        # Prüfe ob file_id existiert
        # Return metadata ohne Kopie
    
    async def get_file(self, db, file_id: uuid.UUID) -> File:
        """Holt eine Datei aus dem DMS."""

Zwei Attachment-Modi:

  1. file_source='comm' — Datei wurde im Chat hochgeladen, liegt unter _kommunikation/{conversation_id}/ im DMS
  2. file_source='dms' — Datei ist eine Referenz auf ein bestehendes DMS-Dokument (keine Kopie)

3.7 Chat-interne RBAC

Konversations-Rollen (in comm_participants.role):

Rolle Rechte
admin Alles: Nachrichten löschen, Teilnehmer verwalten, Titel ändern, andere admin machen
member Nachrichten schreiben, lesen, reagieren, Dateien hochladen, @Mentions
reader Nur lesen, reagieren — kein Schreiben

RBAC-Integration mit bestehendem System:

class CommRBAC:
    """Chat-interne RBAC, angebunden an bestehendes Permission-System."""
    
    async def can_user_write(
        self, db, user_id, conversation_id, user_permissions: list[str]
    ) -> bool:
        """Prüft ob User schreiben darf.
        1. System-Permission 'comm:write' muss vorhanden sein
        2. User muss Teilnehmer der Konversation sein
        3. User-Rolle in Konversation muss 'admin' oder 'member' sein
        4. Konversation darf nicht gelockt sein (für nicht-Plugins)
        """
    
    async def can_user_manage(
        self, db, user_id, conversation_id, user_permissions: list[str]
    ) -> bool:
        """Prüft ob User Teilnehmer verwalten darf.
        1. System-Permission 'comm:manage' oder 'comm:admin'
        2. User muss Konversation-Admin sein
        """
    
    async def can_user_delete(
        self, db, user_id, conversation_id, user_permissions: list[str]
    ) -> bool:
        """Prüft ob User Nachrichten/Konversation löschen darf.
        1. System-Permission 'comm:delete' oder is_system_admin
        2. Für eigene Nachrichten: immer erlaubt
        3. Für fremde Nachrichten: Konversation-Admin
        4. Für Konversation: admin + comm:delete
        """
    
    async def invite_user(
        self, db, conversation_id, inviter_id, invitee_id, role='member'
    ) -> comm_participants:
        """Lädt User in Konversation ein.
        1. Inviter muss admin sein oder comm:manage haben
        2. Invitee muss existieren und im selben Tenant sein
        3. Füge als Participant hinzu
        4. Event: participant.joined
        """
    
    async def change_role(
        self, db, conversation_id, changer_id, target_id, new_role
    ) -> comm_participants:
        """Ändert Rolle eines Teilnehmers.
        1. Changer muss admin sein
        2. Target muss Teilnehmer sein
        3. Neue Rolle: 'admin', 'member', 'reader'
        """

Permission-Flow:

User Request → require_permission('comm:write') → System-Check
             → CommRBAC.can_user_write(current_user) → Konversations-Check
             → check_permission(current_user, 'comm:write') + is_system_admin
             → Participant role check → Erlaubt/Verweigert

3.8 WebSocket Manager

class WebSocketManager:
    """Verwaltet WebSocket-Verbindungen pro User."""
    
    def __init__(self):
        self._connections: dict[str, list[WebSocket]] = {}  # user_id → connections
    
    async def connect(self, websocket: WebSocket, user_id: str):
        """Neue WebSocket-Verbindung."""
    
    async def disconnect(self, websocket: WebSocket, user_id: str):
        """Verbindung geschlossen."""
    
    async def send_to_user(self, user_id: str, message: dict):
        """Sendet Nachricht an alle Verbindungen eines Users."""
    
    async def send_to_conversation(self, conversation_id: str, message: dict, exclude_user: str = None):
        """Sendet an alle Teilnehmer einer Konversation."""
    
    async def broadcast(self, message: dict):
        """Broadcast an alle verbundenen User."""

WebSocket Events:

# Client → Server
{"type": "subscribe", "conversation_id": "..."}
{"type": "unsubscribe", "conversation_id": "..."}
{"type": "typing", "conversation_id": "...", "is_typing": true}
{"type": "ping"}

# Server → Client
{"type": "message.new", "conversation_id": "...", "message": {...}}
{"type": "message.updated", "message": {...}}
{"type": "message.deleted", "id": "...", "conversation_id": "..."}
{"type": "message.reaction", "message_id": "...", "emoji": "👍", "user_id": "...", "action": "add|remove"}
{"type": "participant.joined", "conversation_id": "...", "participant": {...}}
{"type": "participant.left", "conversation_id": "...", "participant_id": "..."}
{"type": "participant.role_changed", "conversation_id": "...", "participant_id": "...", "role": "..."}
{"type": "typing", "conversation_id": "...", "user_id": "...", "is_typing": true}
{"type": "conversation.updated", "conversation": {...}}
{"type": "conversation.pinned", "conversation_id": "...", "pinned": true}
{"type": "read.update", "conversation_id": "...", "user_id": "...", "last_read_msg_id": "..."}
{"type": "pong"}

3.9 Mini-App Registry

class MiniAppRegistry:
    """Registry für Mini-Apps die von Plugins bereitgestellt werden."""
    
    def __init__(self):
        self._apps: dict[str, MiniAppDef] = {}
    
    def register(self, app_id: str, name: str, icon: str, 
                 render_schema: dict, handler: Callable) -> None:
        """Plugin registriert eine Mini-App."""
    
    def unregister(self, app_id: str) -> None:
    
    def list_apps(self) -> list[dict]:
        """Alle verfügbaren Mini-Apps für Frontend."""
    
    def get_app(self, app_id: str) -> MiniAppDef | None:


class MiniAppDef(BaseModel):
    app_id: str
    name: str
    icon: str
    description: str
    render_schema: dict    # JSON-Schema für Frontend-Rendering
    plugin_name: str      # welches Plugin hat es registriert

3.10 Search Provider (unified_search Integration)

class CommSearchProvider:
    """Provider für unified_search — durchsucht alle Konversationen und Nachrichten."""
    
    async def search(
        self, db, tenant_id, user_id, query: str, limit: int = 20
    ) -> list[dict]:
        """Sucht in Nachrichten und Konversationen.
        
        Returns:
        [
            {
                'type': 'message',
                'id': '...',
                'conversation_id': '...',
                'conversation_title': '...',
                'content': '...',
                'sender_type': 'user',
                'sender_name': 'Max',
                'created_at': '...',
                'snippet': '...matching text...'
            },
            {
                'type': 'conversation',
                'id': '...',
                'title': '...',
                'participant_count': 3,
                'last_msg_at': '...'
            }
        ]
        """
    
    async def index_message(self, message: dict) -> None:
        """Indexiert eine Nachricht für die Suche."""
    
    async def reindex_all(self, db, tenant_id) -> None:
        """Vollständige Neu-Indexierung."""

Registrierung: Das kommunikation Plugin registriert seinen Search Provider beim unified_search Plugin bei Aktivierung.


4. Plugin ai_assistant (Anpassung)

4.1 Was bleibt

  • Provider-Verwaltung, Modelle, Presets, Agents, Tools
  • Tool Registry (andere Plugins können Tools registrieren)
  • Streaming-Logik
  • AI Copilot System (AIConversation/AIMessage) — parallel laufen lassen
  • Eigene Settings-Pages (Provider, Modelle, Agents)

4.2 Was ändert sich

  • Keine eigene Chat-UIChatWindow wird durch kommunikation MessageSidebar ersetzt
  • Implementiert ParticipantHandler — registriert sich als ai Teilnehmer
  • Erstellt gepinnten Raum "Assistent" — 1:1 Konversation mit User + AI
  • Schreibt in comm_messages — nicht mehr nur in ai_chat_sessions
  • Auf message.received hören — wenn @KI erwähnt oder AI Teilnehmer → Response
  • Streaming über WebSocket — nicht mehr SSE, sondern WebSocket message.new Events
    • Token-basiertes Streaming: AI generiert Token für Token
    • Pro Token: {"type": "message.streaming", "conversation_id": "...", "message_id": "...", "token": "...", "chunk_index": N}
    • Am Ende: {"type": "message.streaming.done", "message_id": "...", "final_content": "..."}
    • Frontend zeigt Token live an, ersetzt am Ende durch finalen Content

4.2.1 BasePlugin services Property (System-Fix)

BasePlugin wird um eine services Property erweitert, damit Plugins auf den ServiceContainer zugreifen können:

# In app/plugins/base.py

class BasePlugin(ABC):
    def __init__(self) -> None:
        # ... bestehend ...
        self._container: ServiceContainer | None = None
    
    async def on_activate(self, db, service_container, event_bus) -> None:
        self._container = service_container  # ← NEU
        # ... bestehend ...
    
    @property
    def services(self) -> ServiceContainer:
        if self._container is None:
            raise RuntimeError("Services not available — plugin not activated")
        return self._container

Aufwand: 3-4 Zeilen in base.py. Keine bestehenden Plugins müssen geändert werden.

  • Token-basiertes Streaming: AI generiert Token für Token
  • Pro Token: {"type": "message.streaming", "conversation_id": "...", "message_id": "...", "token": "...", "chunk_index": N}
  • Am Ende: {"type": "message.streaming.done", "message_id": "...", "final_content": "..."}
  • Frontend zeigt Token live an, ersetzt am Ende durch finalen Content

4.3 ParticipantHandler Implementation

class AIParticipantHandler(ParticipantHandler):
    """AI Assistant als Teilnehmer im kommunikation Plugin."""
    
    async def on_message_received(
        self, conversation_id, message, conversation, mentions, context
    ) -> list[dict] | None:
        """Reagiert auf Nachrichten in Konversationen mit AI-Teilnehmer."""
        # 1. Prüfe ob AI Teilnehmer in dieser Konversation ist
        # 2. Prüfe ob @KI erwähnt wurde ODER Konversation ist 1:1 mit AI
        # 3. Wenn ja: generiere AI-Response via stream_chat()
        # 4. Schreibe Response als comm_message (sender_type='ai')
        # 5. Push via WebSocket
        # 6. Return [response_message_dict]
    
    def get_participant_info(self) -> dict:
        return {
            'display_name': 'KI Assistent',
            'avatar_url': None,  # Robot-Icon im Frontend
            'capabilities': ['chat', 'tools', 'streaming'],
            'description': 'KI Assistent mit LLM und Tools'
        }

4.4 Plugin on_activate

class AIAssistantPlugin(BasePlugin):
    async def on_activate(self, db, container, event_bus):
        await super().on_activate(db, container, event_bus)
        
        # 1. Bei kommunikation registrieren
        from app.plugins.builtins.kommunikation.participant_registry import get_participant_registry
        registry = get_participant_registry()
        registry.register('ai', AIParticipantHandler(self.services))
        
        # 2. Auf message.received hören (für @KI Detection)
        event_bus.subscribe('message.received', self.on_message_received)
        
        # 3. Tools registrieren (bestehend)
        # ...
    
    async def on_deactivate(self, db, container, event_bus):
        # Unregister participant
        get_participant_registry().unregister('ai')
        event_bus.unsubscribe('message.received', self.on_message_received)
        await super().on_deactivate(db, container, event_bus)

5. Plugin ai_proactive (Anpassung)

5.1 Was bleibt

  • Context-Engine (Kontext-Änderung → Suggestion)
  • Background Jobs (deep_analysis via ARQ)
  • Settings (Pro-User: enabled, categories, confidence, rate_limit, model)
  • Tool Registry Integration

5.2 Was ändert sich

  • Keine eigene UISuggestionList wird durch kommunikation MessageSidebar ersetzt
  • Implementiert ParticipantHandler — registriert sich als ai_proactive Teilnehmer
  • Erstellt gepinnten Raum "Live KI" — locked, gepinnt, für proaktive Vorschläge
  • Heartbeat — regelmäßige Background-Job postet Status/Updates in den Raum
  • Schreibt in comm_messages — nicht mehr in ai_proactive_suggestions (parallel)
  • Push via WebSocket — nicht mehr SSE
  • Suggestion als Rich Contentaction_card Blocks mit Buttons

5.3 Heartbeat

class AIProactivePlugin(BasePlugin):
    manifest = PluginManifest(
        ...,
        events=["context.view_changed", "context.entity_selected"],
        ...,
    )
    
    async def on_activate(self, db, container, event_bus):
        await super().on_activate(db, container, event_bus)
        
        # 1. Bei kommunikation registrieren
        registry = get_participant_registry()
        registry.register('ai_proactive', AIProactiveParticipantHandler(self.services))
        
        # 2. Heartbeat Job registrieren (ARQ)
        # Läuft alle X Minuten, postet Status in "Live KI" Raum
        # z.B. "System aktiv — überwacht 3 Kontakte, 2 Vorschläge generiert"
    
    async def on_deactivate(self, db, container, event_bus):
        registry.unregister('ai_proactive')
        # Heartbeat Job abmelden
        await super().on_deactivate(db, container, event_bus)

Heartbeat Job:

async def heartbeat_job(ctx):
    """Läuft alle 5 Minuten.
    
    - Prüft ob Proactive AI für User aktiviert ist
    - Postet Status-Message in "Live KI" Raum:
      - Anzahl überwachter Entities
      - Anzahl generierter Vorschläge (heute)
      - System-Status (aktiv, pausiert, Fehler)
    - Wenn neue Vorschläge vorhanden: pusht diese als action_card
    """

5.4 Proactive ParticipantHandler

class AIProactiveParticipantHandler(ParticipantHandler):
    async def on_message_received(
        self, conversation_id, message, conversation, mentions, context
    ) -> list[dict] | None:
        """Reagiert auf Nachrichten im 'Live KI' Raum.
        
        - User kann Fragen stellen ("Was schlägst du vor?")
        - AI Proactive generiert Vorschlag basierend auf aktuellem Kontext
        - Antwort als action_card mit Buttons
        """
    
    def get_participant_info(self) -> dict:
        return {
            'display_name': 'Live KI',
            'avatar_url': None,  # Bulb-Icon im Frontend
            'capabilities': ['proactive', 'context_aware', 'heartbeat'],
            'description': 'Proaktive KI mit Kontext-Überwachung'
        }

6. Plugin system_notif (Neu)

6.1 Verantwortung

Wandelt System-Events in Nachrichten um. Ersetzt langfristig das bestehende Notification-System.

6.2 Manifest

PluginManifest(
    name="system_notif",
    version="1.0.0",
    display_name="System Benachrichtigungen",
    description="Wandelt System-Events in Chat-Nachrichten um",
    dependencies=["kommunikation"],
    routes=[],  # Keine eigenen Routes — nur Participant
    events=[
        "lead.created", "contact.created", "contact.updated",
        "task.overdue", "task.created", "mail.received",
        "user.created", "workflow.completed",
    ],
    migrations=["0001_initial.sql"],
    permissions=["system_notif:read"],
    is_core=False,
)

6.3 Funktionsweise

class SystemNotificationPlugin(BasePlugin):
    async def on_activate(self, db, container, event_bus):
        await super().on_activate(db, container, event_bus)
        
        # 1. Bei kommunikation registrieren
        registry = get_participant_registry()
        registry.register('system', SystemParticipantHandler())
        
        # 2. Auf System-Events hören (bereits via manifest.events)
        # 3. Für jeden User: Erstelle gepinnten Raum "System" (locked)
    
    async def on_lead_created(self, payload):
        """lead.created → System-Nachricht"""
        # 1. Finde alle User die benachrichtigt werden sollen
        # 2. Für jeden User: schreibe Nachricht in System-Raum
        # 3. Nachricht als action_card: "Neuer Lead: Acme Corp" + [Öffnen] Button
    
    async def on_contact_created(self, payload):
        """contact.created → System-Nachricht"""
    
    async def on_task_overdue(self, payload):
        """task.overdue → System-Nachricht (warning)"""

6.4 System ParticipantHandler

class SystemParticipantHandler(ParticipantHandler):
    async def on_message_received(
        self, conversation_id, message, conversation, mentions, context
    ) -> list[dict] | None:
        """System ist passiv — liest nur, antwortet nicht.
        
        Ausnahme: User kann auf Action-Buttons reagieren (archivieren, öffnen).
        Diese Reaktionen werden als metadata auf der Nachricht gesetzt, nicht als neue Nachricht.
        """
        return None
    
    def get_participant_info(self) -> dict:
        return {
            'display_name': 'System',
            'avatar_url': None,  # Bell-Icon im Frontend
            'capabilities': ['notifications', 'action_cards'],
            'description': 'System-Benachrichtigungen'
        }

6.5 Migration bestehender Notifications

  • Bestehende notifications Tabelle bleibt erhalten (Abwärtskompatibilität)
  • Neue System-Events schreiben in comm_messages (System-Raum)
  • Langfristig: Frontend zeigt nur noch comm_messages an, alte Tabelle wird deprecated
  • create_notification() bleibt unverändert (Core-Code kann Plugin-Code nicht importieren)
  • Stattdessen: create_notification() publiziert EventBus Event notification.created
  • system_notif Plugin hört auf notification.created und schreibt in comm_messages
  • Keine zirkuläre Abhängigkeit — Core → EventBus → Plugin

7. Frontend — MessageSidebar

7.1 Struktur

Die rechte Sidebar wird zur MessageSidebar und ersetzt die aktuelle AISidebar:

┌──────────────────────────────────────────┐
│  Kommunikation                    [×]    │
├──────────────────────────────────────────┤
│  🔍 Suche...                             │
├──────────────────────────────────────────┤
│  RAUM-LISTE                              │
│  📌 🔒 System                           │  ← gepinnt + locked (Plugin)
│     🔔 3 neue Leads importiert           │
│  📌 🔒 Live KI                          │  ← gepinnt + locked (Plugin)
│     🤖 System aktiv — 2 Vorschläge...   │
│  📌 🔒 Assistent                        │  ← gepinnt + locked (Plugin)
│     🤖 47 Kontakte ohne Email...        │
│  ────────────────────────────           │
│  👥 Projekt Alpha                       │  ← User-Raum (nicht gepinnt)
│     Max: @KI fasse die Leads zusammen    │
│  👥 Sales Team                          │
│     Lisa: Q3-Zahlen sind da              │
│  👤 Max Müller                          │  ← Direkt-Chat
│     Du: Hast du kurz Zeit?               │
├──────────────────────────────────────────┤
│  FEED (aktive Konversation)             │
│  ┌────────────────────────────────────┐ │
│  │ Max: @KI fasse die Leads zusammen   │ │
│  │ 🤖 KI: 3 neue Leads, 2 aus...       │ │
│  │ Lisa: Super, danke!                 │ │
│  │ ┌──────────────────────────────┐   │ │
│  │ │ 📎 lead_report.pdf           │   │ │
│  │ │ ──────────────────────────── │   │ │
│  │ │ ## 3 neue Leads              │   │ │
│  │ │ - **Acme Corp** — €50k       │   │ │
│  │ │ ──────────────────────────── │   │ │
│  │ │ [Öffnen]  [Archivieren]      │   │ │
│  │ └──────────────────────────────┘   │ │
│  └────────────────────────────────────┘ │
├──────────────────────────────────────────┤
│  [📎] [Eingabefeld...]        [Senden]  │
└──────────────────────────────────────────┘

7.2 Komponenten

frontend/src/components/comm/
├── MessageSidebar.tsx          # Haupt-Komponente (ersetzt AISidebar)
├── ConversationList.tsx        # Raum-Liste (links im Panel)
├── ConversationItem.tsx        # Einzelne Raum-Zeile
├── MessageFeed.tsx             # Nachrichten-Feed (Mitte)
├── MessageBubble.tsx           # Einzelne Nachricht
├── MessageInput.tsx            # Eingabefeld + Upload + @Mention
├── BlockRenderer.tsx          # Rich Content Block Renderer
├── blocks/
│   ├── MarkdownBlock.tsx
│   ├── HtmlBlock.tsx
│   ├── ImageBlock.tsx
│   ├── AudioBlock.tsx
│   ├── VideoBlock.tsx
│   ├── FileBlock.tsx
│   ├── ActionCardBlock.tsx
│   ├── ContactCardBlock.tsx
│   └── MiniAppBlock.tsx
├── ParticipantAvatars.tsx     # Teilnehmer-Avatare im Header
├── ReactionBar.tsx            # Emoji-Reaktionen
├── TypingIndicator.tsx         # "X tippt..."
└── CreateConversationDialog.tsx  # Neue Konversation erstellen

7.3 commStore (neu)

// store/commStore.ts — neuer Store für Communication State
import { create } from 'zustand';

export interface CommState {
  conversations: Conversation[];
  activeConversationId: string | null;
  messages: Record<string, Message[]>;  // conversation_id → messages
  typingUsers: Record<string, string[]>;  // conversation_id → user_ids
  unreadCounts: Record<string, number>;  // conversation_id → count

  setConversations: (convs: Conversation[]) => void;
  setActiveConversation: (id: string | null) => void;
  addMessage: (convId: string, msg: Message) => void;
  updateConversation: (conv: Conversation) => void;
  setTyping: (convId: string, userIds: string[]) => void;
  setUnread: (convId: string, count: number) => void;
}

export const useCommStore = create<CommState>((set) => ({
  conversations: [],
  activeConversationId: null,
  messages: {},
  typingUsers: {},
  unreadCounts: {},
  // ... implementations
}));

7.4 uiStore Anpassung

// uiStore.ts — neue State-Struktur
export type MessageSidebarView = 'rooms' | 'feed';

export interface UIState {
  // ... bestehende ...
  
  // Communication
  messageSidebarCollapsed: boolean;
  messageSidebarView: MessageSidebarView;  // 'rooms' = Liste, 'feed' = aktive Konversation
  activeConversationId: string | null;
  commWebSocket: WebSocket | null;
  
  toggleMessageSidebar: () => void;
  setMessageSidebarView: (view: MessageSidebarView) => void;
  setActiveConversation: (id: string | null) => void;
}

7.4 WebSocket Hook

// hooks/useCommWebSocket.ts
export function useCommWebSocket() {
    const { activeConversationId, addMessage, updateConversation } = useCommStore();
    
    useEffect(() => {
        const ws = new WebSocket(`${WS_BASE}/api/v1/comm/ws`);
        
        ws.onmessage = (event) => {
            const data = JSON.parse(event.data);
            switch (data.type) {
                case 'message.new':
                    addMessage(data.conversation_id, data.message);
                    break;
                case 'conversation.updated':
                    updateConversation(data.conversation);
                    break;
                case 'typing':
                    // Update typing indicator
                    break;
                // ...
            }
        };
        
        return () => ws.close();
    }, []);
}

7.5 API Client

// api/comm.ts
export const commApi = {
    // Konversationen
    listConversations: () => api.get('/comm/conversations'),
    createConversation: (data) => api.post('/comm/conversations', data),
    getConversation: (id) => api.get(`/comm/conversations/${id}`),
    updateConversation: (id, data) => api.patch(`/comm/conversations/${id}`, data),
    pinConversation: (id) => api.post(`/comm/conversations/${id}/pin`),
    unpinConversation: (id) => api.delete(`/comm/conversations/${id}/pin`),
    muteConversation: (id) => api.post(`/comm/conversations/${id}/mute`),
    
    // Teilnehmer
    addParticipant: (convId, data) => api.post(`/comm/conversations/${convId}/participants`, data),
    removeParticipant: (convId, pid) => api.delete(`/comm/conversations/${convId}/participants/${pid}`),
    changeRole: (convId, pid, role) => api.patch(`/comm/conversations/${convId}/participants/${pid}`, { role }),
    
    // Nachrichten
    getMessages: (convId, page) => api.get(`/comm/conversations/${convId}/messages`, { params: { page } }),
    sendMessage: (convId, data) => api.post(`/comm/conversations/${convId}/messages`, data),
    editMessage: (id, data) => api.patch(`/comm/messages/${id}`, data),
    deleteMessage: (id) => api.delete(`/comm/messages/${id}`),
    
    // Attachments
    uploadAttachment: (msgId, file) => {
        const formData = new FormData();
        formData.append('file', file);
        return api.post(`/comm/messages/${msgId}/attachments`, formData);
    },
    referenceDmsFile: (msgId, fileId) => api.post(`/comm/messages/${msgId}/attachments`, { file_id: fileId }),
    
    // Reaktionen
    addReaction: (msgId, emoji) => api.post(`/comm/messages/${msgId}/reactions`, { emoji }),
    removeReaction: (msgId, emoji) => api.delete(`/comm/messages/${msgId}/reactions/${emoji}`),
    
    // Read State
    markRead: (convId) => api.post(`/comm/conversations/${convId}/read`),
    
    // Mini-Apps
    listMiniApps: () => api.get('/comm/miniapps'),
    startMiniApp: (convId, appId, config) => api.post(`/comm/conversations/${convId}/miniapps`, { app_id: appId, config }),
};

8. REST API

8.1 Konversationen

GET    /api/v1/comm/conversations
  → Liste für aktuellen User (inkl. pinned, locked, unread_count, last_msg)
  Query: ?archived=false (default: nur nicht-archivierte)
  Response: [{ id, title, is_locked, locked_by, is_direct, is_pinned, 
               participants: [...], last_msg_preview, last_msg_sender_type, 
               last_msg_at, unread_count }]

POST   /api/v1/comm/conversations
  Body: { title?, participant_ids: [uuid], participant_types: ['user'], 
          initial_message?, is_direct? }
  → Neue Konversation erstellen (User wird automatisch admin)
  Permission: comm:create

GET    /api/v1/comm/conversations/{id}
  → Details + Teilnehmer-Liste + Rolle des aktuellen Users

PATCH  /api/v1/comm/conversations/{id}
  Body: { title?, is_archived? }
  → Titel ändern (nicht wenn locked), archivieren
  Permission: comm:write + admin role (für title)

DELETE /api/v1/comm/conversations/{id}
  → Konversation löschen (admin) oder verlassen (member)
  Permission: comm:delete (admin) oder comm:write (leave)

POST   /api/v1/comm/conversations/{id}/pin
  → Für aktuellen User anpinnen
  Permission: comm:read

DELETE /api/v1/comm/conversations/{id}/pin
  → Pinning entfernen

POST   /api/v1/comm/conversations/{id}/mute
  → Stummschalten für aktuellen User

DELETE /api/v1/comm/conversations/{id}/mute

8.2 Teilnehmer

POST   /api/v1/comm/conversations/{id}/participants
  Body: { participant_id, participant_type: 'user', role: 'member' }
  → Teilnehmer hinzufügen (einladen)
  Permission: comm:manage + admin role

DELETE /api/v1/comm/conversations/{id}/participants/{pid}
  → Teilnehmer entfernen
  Permission: comm:manage + admin role (oder self-leave)

PATCH  /api/v1/comm/conversations/{id}/participants/{pid}
  Body: { role: 'admin'|'member'|'reader' }
  → Rolle ändern
  Permission: comm:admin + admin role

8.3 Nachrichten

GET    /api/v1/comm/conversations/{id}/messages
  → Nachrichten (paginiert, 50 pro Seite)
  Query: ?page=1&before={msg_id}
  Response: { items: [...], total, page, has_more }
  Permission: comm:read + participant

POST   /api/v1/comm/conversations/{id}/messages
  Body: { content, content_format: 'markdown', 
         blocks?: [{ block_type, block_data }],
         reply_to_id?, attachments?: [{ file_id?, file_source? }] }
  → Nachricht senden
  Permission: comm:write + participant (admin/member role)
  → Triggert EventBus: message.received
  → Triggert ParticipantHandler für alle nicht-user Teilnehmer
  → Push via WebSocket an alle Teilnehmer

PATCH  /api/v1/comm/messages/{id}
  Body: { content?, read? }
  → Bearbeiten oder als gelesen markieren
  Permission: comm:write (eigene) oder comm:manage (fremde)

DELETE /api/v1/comm/messages/{id}
  → Nachricht löschen (soft delete)
  Permission: comm:delete (eigene) oder admin role

8.4 Attachments

POST   /api/v1/comm/messages/{id}/attachments
  Body: multipart/form-data (file) ODER JSON ({ file_id, file_source: 'dms' })
  → Datei hochladen (→ DMS Bridge) oder DMS-Datei referenzieren
  Permission: comm:write + participant

GET    /api/v1/comm/attachments/{id}
  → Datei herunterladen
  Permission: comm:read + participant

GET    /api/v1/comm/attachments/{id}/thumbnail
  → Thumbnail für Bild/Video

8.5 Reaktionen

POST   /api/v1/comm/messages/{id}/reactions
  Body: { emoji }
  Permission: comm:write + participant

DELETE /api/v1/comm/messages/{id}/reactions/{emoji}

8.6 Read State

POST   /api/v1/comm/conversations/{id}/read
  Body: { last_read_msg_id }
  → Lese-Status aktualisieren
  Permission: comm:read + participant

8.7 Mini-Apps

GET    /api/v1/comm/miniapps
  → Verfügbare Mini-Apps (von Plugins registriert)
  Response: [{ app_id, name, icon, description, plugin_name }]

POST   /api/v1/comm/conversations/{id}/miniapps
  Body: { app_id, config? }
  → Mini-App in Konversation starten → erzeugt miniapp Block
  Permission: comm:write + participant

8.8 WebSocket

WS     /api/v1/comm/ws
  → Authentifiziert via Session-Cookie (wie REST-API, Same-Origin)
  → Bei Connect: Session validieren, user_id extrahieren, Tenant-Kontext setzen
  → Bei ungültiger Session: WebSocket mit Code 4001 geschlossen
  → Siehe WebSocket Events oben

9. Plugin-übergreifende Integration

9.1 EventBus Flow bei neuer Nachricht

1. User schreibt Nachricht → POST /api/v1/comm/conversations/{id}/messages
2. kommunikation Service:
   a. Speichert Nachricht in comm_messages + comm_message_blocks
   b. Publiziert EventBus: message.received { conversation_id, message, mentions }
   c. Pusht via WebSocket an alle Teilnehmer
3. ParticipantHandler werden aufgerufen:
   a. AI ParticipantHandler: @KI erwähnt? → generiere Response → neue comm_message
   b. AI Proactive Handler: im Live KI Raum? → generiere Vorschlag → neue comm_message
   c. System Handler: passiv → return None
4. Wenn neue Nachricht von ParticipantHandler: → wieder Schritt 2
   WICHTIG: Infinite-Loop-Protection
   - Jede Nachricht bekommt metadata['triggered_by'] = original_message_id
   - ParticipantHandler prüft: wenn message.id == triggered_by → nicht erneut triggern
   - Max depth counter in metadata['trigger_depth'] (default 0, max 3)
   - Bei max depth: Nachricht wird gespeichert aber keine Handler mehr getriggert

9.2 Plugin-Raum-Erstellung bei Aktivierung

Plugin wird aktiviert → on_activate()
  → Prüfe ob Plugin-Raum für User existiert
  → Wenn nicht: Erstelle Konversation
    - title: "System" / "Live KI" / "Assistent"
    - is_locked: true
    - locked_by: plugin_name
    - created_by_type: 'plugin'
    - Füge Plugin als Participant hinzu (participant_type)
    - Füge User als Participant hinzu (role: 'reader' für system, 'member' für ai)
    - Pinne für User
  → Wenn ja: nichts tun

9.3 unified_search Integration

kommunikation Plugin aktiviert →
  → Registriert CommSearchProvider bei unified_search
  → unified_search indexiert comm_messages
  → Suche liefert Ergebnisse aus Konversationen

9.4 DMS Integration

User lädt Datei im Chat →
  → DmsBridge.store_attachment()
  → Erstellt/Findet DMS-Ordner _kommunikation/{conversation_id}/
  → Speichert Datei im DMS
  → Erstellt comm_message_attachment mit file_id + file_source='comm'

User referenziert DMS-Datei →
  → DmsBridge.reference_external_file(file_id)
  → Prüft DMS-Datei existiert
  → Erstellt comm_message_attachment mit file_id + file_source='dms'
  → Keine Kopie, nur Referenz

10. Vollständige Checkliste — Was berücksichtigt wurde

Core Features

  • Einheitliches Messaging-System (ein Plugin, ein Datenmodell)
  • KI als Teilnehmer (kein eigener Chat-Typ)
  • System als Teilnehmer (Notifications = Nachrichten)
  • Externe Messenger als Teilnehmer (erweiterbar)
  • Rich Content Transport (Blocks: text, markdown, html, image, audio, video, file, action_card, contact_card, miniapp)
  • Mini-Apps (Plugin-basiert, im Chat renderbar)
  • Datei-Upload (DMS-Integration mit eigener Datenquelle + externe Referenzen)
  • WebSocket (Real-time für alles)
  • EventBus Integration (Plugins reagieren auf Nachrichten)

Raum-Logik

  • Räume = benannte Konversationen (Titel editierbar)
  • Pinning (user-spezifisch, pro User)
  • Locking (Plugin-Lock: User kann nicht ändern)
  • Plugin-erstellte Räume (System, Live KI, Assistent)
  • Archivierung (ausgeblendete Konversationen)
  • Stummschaltung (mute pro User)

KI-Plugins

  • ai_assistant als Teilnehmer (ohne eigene Chat-UI)
  • ai_proactive als Teilnehmer (ohne eigene UI)
  • Heartbeat für ai_proactive (regelmäßiger Background-Job)
  • Gepinnter Kanal für proaktive KI
  • @KI Mention in jedem Chat
  • Streaming über WebSocket
  • AI Copilot bleibt parallel (nicht migriert)

RBAC

  • Anbindung an bestehendes Permission-System (comm:read, comm:write, etc.)
  • Konversations-Rollen (admin, member, reader)
  • User einladen (admin only)
  • Rollen vergeben (admin only)
  • System-Permission + Konversations-Permission (zwei-Level Check)
  • Locked Räume (User kann Titel/Teilnehmer nicht ändern)

DMS

  • Eigener DMS-Bereich (_kommunikation/{conversation_id}/)
  • Externe DMS-Referenzen (file_source='dms')
  • Thumbnail-Generierung für Bilder/Videos

Suche

  • unified_search Provider für comm_messages + comm_conversations
  • Volltext-Suche in Nachrichten
  • Konversations-Suche

Frontend

  • MessageSidebar ersetzt AISidebar
  • Raum-Liste (pinned/locked oben, dann normale)
  • Unified Feed mit Rich Content Rendering
  • Eingabefeld mit Datei-Upload + @Mention
  • WebSocket-Verbindung
  • Block-Renderer (Markdown, HTML, Image, Audio, Video, File, ActionCard, MiniApp)
  • Reaktionen (Emoji-Bar)
  • Typing-Indikator
  • Lese-Status (gelesen-Häkchen)

Migration

  • Alte Tabellen bleiben (ai_chat_sessions, ai_conversations, notifications)
  • Neue Tabellen parallel (comm_*)
  • Schrittweise Migration
  • Abwärtskompatibilität

Zukünftig

  • Messenger-Gateway Plugins (WhatsApp, Telegram, Email)
  • Push-Notifications für Mobile (später)
  • E2E-Verschlüsselung (später evaluieren)

11. Was könnte noch fehlen? — Ergänzungen

11.1 Message Threading

  • reply_to_id ist vorhanden für direkte Antworten
  • Echte Thread-Ansicht (Sub-Threads) wäre möglich, aber nicht in Phase 1
  • Empfehlung: Reply-To für Phase 1, Sub-Threads später

11.2 Message Drafts

  • Drafts pro Konversation speichern (lokal im Frontend oder serverseitig)
  • Empfehlung: Lokal im Frontend (localStorage) für Phase 1

11.3 Message Forwarding

  • Nachricht an andere Konversation weiterleiten
  • Empfehlung: Später, einfache Implementierung (kopiere message + blocks)

11.4 Conversation Export

  • Konversation als PDF/CSV exportieren
  • Empfehlung: Später, über Report Generator Plugin

11.5 User Presence / Online-Status

  • Real-time Online-Status über WebSocket
  • Empfehlung: Phase 2, über WebSocket presence channel

11.6 Voice Messages

  • Audio-Block ist vorhanden, Aufnahme im Frontend
  • Empfehlung: Phase 2 (Audio-Block + Frontend Recorder)

11.7 Message Pinning (innerhalb Konversation)

  • is_pinned auf comm_messages vorhanden
  • Wichtige Nachrichten im Feed anpinnen
  • Empfehlung: Phase 1 (Feld vorhanden, UI später)

11.8 Conversation Description / Topic

  • Raum-Beschreibung / Thema
  • Empfehlung: In metadata speichern, UI später

11.9 Read Receipts (Detail)

  • Wer hat die Nachricht gelesen?
  • Empfehlung: Phase 2 (comm_message_reads reicht für Phase 1)

11.10 Message Search (innerhalb Konversation)

  • Suche innerhalb einer Konversation
  • Empfehlung: Frontend-Filter auf geladene Nachrichten + Server-Suche für ältere

11.11 Notification Preferences pro Konversation

  • Stummschaltung ist vorhanden (mute)
  • Feinere Einstellungen (nur @Mentions, etc.)
  • Empfehlung: Phase 2

11.12 Group Avatar / Icon

  • Konversations-Icon setzen
  • Empfehlung: In metadata.icon speichern, Frontend später

11.13 Nachrichten-Bearbeitung-Historie

  • Neue Tabelle comm_message_edits speichert alte Versionen bei Bearbeitung
  • Felder: id, message_id, old_content, old_blocks (JSONB), edited_by, edited_at
  • Audit-Trail: jede Bearbeitung wird nachvollziehbar

11.14 Rate-Limiting (optionale Konfiguration)

  • Konfigurierbares Rate-Limit pro Tenant (z.B. max N Nachrichten pro Minute pro User)
  • In den Einstellungen aktivierbar/deaktivierbar
  • Default: deaktiviert, kann bei Bedarf eingeschaltet werden

11.15 Datei-Größen-Limit und Referenz-Modus

  • Dateien < 100MB: direkter Upload im Chat → DMS unter _kommunikation/{conversation_id}/
  • Dateien >= 100MB: nur DMS-Referenz (file_source='dms') — kein Upload im Chat
  • Frontend zeigt bei großen Dateien einen "Im DMS öffnen" Link statt Download
  • comm_message_attachments.file_source unterscheidet die Modi

11.16 Konversations-Cover-Bild

  • Gruppen-Chats / Räume können ein Cover-Bild haben
  • Gespeichert in metadata.cover_url
  • Frontend zeigt Cover-Bild in Raum-Liste und Konversations-Header

11.17 WebSocket Reconnection

  • Frontend auto-reconnect bei Verbindungsabbruch
  • Exponential backoff (1s, 2s, 4s, 8s, max 30s)
  • Bei Reconnect: Lese-Status synchronisieren, verpasste Nachrichten nachladen

11.18 Offline-Queue (später für Mobile)

  • Nachrichten lokal speichern bei Offline-Status
  • Bei Reconnect automatisch senden
  • Nur für Mobile App relevant, nicht für Desktop

11.19 Datenretention (Einstellungen)

  • Keine automatische Löschung in Phase 1
  • Einstellungen pro Tenant: Aufbewahrungszeit konfigurierbar (z.B. 90 Tage, 1 Jahr, unbegrenzt)
  • Bereits in Phase 1 als Setting-Feld vorsehen, Funktionalität später

11.20 Konversations-Limit (Einstellungen)

  • Kein Limit in Phase 1
  • Einstellungen pro Tenant: maximale Anzahl Konversationen konfigurierbar
  • Bereits in Phase 1 als Setting-Feld vorsehen, Funktionalität später

12. Implementierungs-Phasen

Phase 1: Backend — Plugin kommunikation (Woche 1-2)

  1. Plugin-Gerüst (plugin.py, manifest, __init__.py)
  2. Datenmodell (models.py) — alle Tabellen
  3. Migration (0001_initial.sql)
  4. Participant Registry (participant_registry.py)
  5. Services (services.py) — CRUD, Nachrichten senden, Raum-Erstellung
  6. RBAC (rbac.py) — Konversations-Rollen, Permission-Checks
  7. DMS Bridge (dms_bridge.py) — Attachment-Speicherung
  8. WebSocket Manager (websocket_manager.py)
  9. REST API (routes.py) — alle Endpoints
  10. Mini-App Registry (miniapp_registry.py)
  11. Content Types (content_types.py) — Block-Definitionen
  12. Search Provider (search_provider.py)

Phase 2: Backend — AI Plugins anpassen (Woche 2-3)

  1. ai_assistant: ParticipantHandler implementieren
  2. ai_assistant: Bei kommunikation registrieren
  3. ai_assistant: message.received → AI-Response
  4. ai_assistant: Streaming über WebSocket
  5. ai_assistant: Gepinnten Raum "Assistent" erstellen
  6. ai_proactive: ParticipantHandler implementieren
  7. ai_proactive: Bei kommunikation registrieren
  8. ai_proactive: Heartbeat Job
  9. ai_proactive: Gepinnten Raum "Live KI" erstellen
  10. ai_proactive: Suggestion als action_card

Phase 3: Backend — Plugin system_notif (Woche 3)

  1. Plugin-Gerüst
  2. ParticipantHandler (passiv)
  3. Event-Handler (lead.created, contact.created, etc.)
  4. Gepinnten Raum "System" erstellen
  5. Bestehende Notifications migrieren (parallel)

Phase 4: Frontend — MessageSidebar (Woche 3-4)

  1. MessageSidebar-Komponente (ersetzt AISidebar)
  2. ConversationList (Raum-Liste mit Pinning/Locking)
  3. MessageFeed (Nachrichten-Feed)
  4. MessageInput (Eingabefeld + Upload + @Mention)
  5. WebSocket Hook
  6. API Client (comm.ts)
  7. uiStore Anpassung
  8. Alte AISidebar entfernen

Phase 5: Frontend — Rich Content Renderer (Woche 4-5)

  1. BlockRenderer Haupt-Komponente
  2. MarkdownBlock, HtmlBlock
  3. ImageBlock, AudioBlock, VideoBlock, FileBlock
  4. ActionCardBlock (mit Button-Handler)
  5. ContactCardBlock
  6. MiniAppBlock (Plugin-basiert)
  7. ReactionBar
  8. TypingIndicator
  9. ParticipantAvatars

Phase 5.5: Daten-Migration bestehender Chats (Woche 5)

  1. Migration-Script: ai_chat_sessions → comm_conversations (1:1 mit AI)
  2. Migration-Script: ai_chat_messages → comm_messages (sender_type='user'/'ai')
  3. Migration-Script: ai_chat_attachments → comm_message_attachments
  4. Migration-Script: notifications → comm_messages (System-Raum, sender_type='system')
  5. Migration-Script: ai_proactive_suggestions → comm_messages (Live KI Raum, als action_card)
  6. Flag in metadata: {"migrated_from": "ai_chat_sessions"} für Nachverfolgung
  7. Alte Tabellen bleiben erhalten (kein Datenverlust)

Phase 6: Testing & Deployment (Woche 5)

  1. Backend Tests (API, WebSocket, RBAC, Participant Registry)
  2. Frontend Tests (MessageSidebar, Block Renderer)
  3. Integration Tests (AI Response, System Notifications)
  4. Deployment (Coolify)

Phase 7: Messenger-Gateway Plugins (später)

  1. whatsapp_gateway Plugin
  2. telegram_gateway Plugin
  3. email_gateway Plugin

Phase 8: Mobile & Push (später)

  1. Push-Notification Service
  2. Mobile App API
  3. E2E-Verschlüsselung (evaluieren)

13. Zusammenfassung

Ein Plugin (kommunikation)         → Chat-Infrastruktur + Rich Content + WebSocket + RBAC + DMS
Ein Interface (ParticipantHandler)  → Plugins docken als Teilnehmer an
Ein Datenmodell (10 Tabellen)       → Conversations, Participants, Messages, Blocks, Attachments, Reactions, Reads, Pins
Eine UI (MessageSidebar)           → Raum-Liste + Feed + Eingabefeld
Eine WebSocket                      → Real-time für alles
Ein EventBus                        → Plugins reagieren auf Nachrichten

KI = Teilnehmer                    → @KI in jedem Chat, keine eigene UI
Proactive KI = Teilnehmer           → Heartbeat + gepinnter Kanal, keine eigene UI
System = Teilnehmer                → Notifications als Nachrichten, locked Raum
WhatsApp = Teilnehmer              → Externe Messenger andocken (später)
Mini-Apps = Plugin-Blocks           → Erweiterbar im Chat
Räume = Benannte Chats              → Titel + Pinning + Locking
RBAC = Zwei-Level                   → System-Permission + Konversations-Rolle
DMS = Bridge                        → Eigener Bereich + externe Referenzen
Suche = Provider                    → unified_search Integration