Ir al contenido

2026 04 19 communications hub design

Esta página aún no está disponible en tu idioma.

Status: Approved
Date: 2026-04-19
Module: nkz-module-zulip
Replaces: iframe wrapper (src/App.tsx)


The NKZ Communications Hub is a contextual dashboard integrated into the Nekazari platform at /communications. It surfaces Zulip data (streams, messages, alerts, DMs) within the NKZ UI, with inline quick-reply capability. It is NOT a chat client — complex interactions deep-link to the full Zulip web UI.

SurfaceURLUse CaseTarget User
NKZ Communications Hubnekazari.robotika.cloud/communicationsContextual dashboard: alerts, streams, DMs, quick-replyDay-to-day platform use
Zulip Webmessaging.robotika.cloudFull chat client: search, admin, long threadsPower users, admin
Zulip Mobile App (official)App Store / Play StorePush notifications, quick reply in the fieldField technicians
DecisionChoiceRationale
Auth strategyPer-user API key via OIDCReal user traceability in Zulip, no bot attribution
API routingProxy via api-gateway (/api/zulip/*)Consistent with platform architecture, centralized auth/audit
Interaction modelQuick-reply inline (option B)80% of replies are short; avoids context-switching
Real-timeLong-polling via Zulip events APIOnly option Zulip exposes; works through api-gateway
Push notificationsZulip official mobile appNo duplication; FCM/APNs already solved
Bot managementPlatform Admin panelAdmin controls bot health, templates, stream provisioning

User (NKZ) → Keycloak OIDC → Zulip (auto-signup, GenericOpenIdConnectBackend)
|
JWT in cookie nkz_token
|
api-gateway extracts user_email from JWT
|
Looks up / caches Zulip API key (Redis, TTL 24h)
|
Proxies to zulip-service with Basic auth (email:api_key)
|
Response → frontend
  • First request: api-gateway calls Zulip Admin API using bot credentials:
    • GET /api/v1/users/{email} → get user_id
    • POST /api/v1/users/{user_id}/api_key/ → get API key
  • Cached: Redis key zulip:apikey:{email}, TTL 24h
  • User not in Zulip: returns 404 with clear message (OIDC not yet active or first login pending)
  • OIDC activated (ZULIP-4): Keycloak client zulip, secret in zulip-secret.oidc-client-secret
  • ZULIP_AUTH_BACKENDS=EmailAuthBackend,GenericOpenIdConnectBackend
  • Bot user nkz-platform-bot@robotika.cloud created in Zulip with admin role

Platform-wide (public, admin-only posting):
#platform-announcements
Per-tenant (private, invite-only):
#tenant-{id}-general — free team chat
#tenant-{id}-alerts — automated notifications destination
#tenant-{id}-* — additional streams from templates
Platform Admin ──────────→ #platform-announcements ──→ All users
(manual or via admin endpoint)
N8N workflows ───┐
risk-engine ─────┤
weather-worker ──┼───────→ #tenant-{id}-alerts ──→ Tenant users only
telemetry ───────┤ topic: iot-alerts
services ────────┘ topic: risk-warnings
topic: system-events
  • Extract tenant_id from JWT on every request
  • GET /streams: filter response to only return tenant-{tenant_id}-* + platform-announcements
  • GET/POST /messages: validate target stream belongs to user’s tenant
  • A user can NEVER see or write to another tenant’s streams

NKZ RouteZulip TargetPurpose
GET /api/zulip/streamsGET /api/v1/streamsList streams (tenant-filtered)
GET /api/zulip/streams/{id}/topicsGET /api/v1/users/me/{id}/topicsTopics in a stream
GET /api/zulip/messagesGET /api/v1/messagesMessages with narrow (stream/topic/DM)
POST /api/zulip/messagesPOST /api/v1/messagesSend message (quick-reply)
GET /api/zulip/users/meGET /api/v1/users/meProfile and unread counts
POST /api/zulip/messages/{id}/reactionsPOST /api/v1/messages/{id}/reactionsEmoji reactions
NKZ RouteZulip TargetPurpose
POST /api/zulip/events/registerPOST /api/v1/registerCreate event queue
GET /api/zulip/eventsGET /api/v1/eventsLong-poll for events
DELETE /api/zulip/eventsDELETE /api/v1/eventsCleanup queue

Provisioning Routes (bot credentials, internal)

Section titled “Provisioning Routes (bot credentials, internal)”
NKZ RoutePurpose
POST /api/zulip/provisioning/tenantCreate tenant space (streams, group, bot subscription)
DELETE /api/zulip/provisioning/tenant/{id}Archive tenant streams
POST /api/zulip/provisioning/tenant/{id}/userSubscribe user to tenant streams
DELETE /api/zulip/provisioning/tenant/{id}/user/{email}Unsubscribe user
POST /api/zulip/provisioning/syncReconcile state (idempotent)
  • api-gateway proxy timeout: >= 120s (long-poll connections)
  • Bot API key in zulip-secret.bot-api-key
  • Zulip internal URL: http://zulip-service:80

  • React IIFE module via @nekazari/module-builder
  • Registers as window.__NKZ__.register({ id: 'zulip', ... })
  • Externals: react, react-dom, react-router-dom, @nekazari/sdk, @nekazari/ui-kit
  • i18n: es + en minimum
  • Mobile-first: min viewport 350px
┌─────────────────────────────────┐
│ Header: "Comunicaciones" │
│ [Connection status] [Open Zulip]│
├─────────────────────────────────┤
│ ALERTAS IoT (expanded) │
│ - Alert cards with severity │
│ - Entity deep links to viewer │
│ - Unread badge │
├─────────────────────────────────┤
│ STREAMS DEL TENANT │
│ - Stream list with unread badge│
│ - Click → expand messages │
│ - Topic groups with messages │
│ - Quick-reply input per topic │
├─────────────────────────────────┤
│ MENSAJES DIRECTOS │
│ - DM list with avatars │
│ - Click → expand conversation │
│ - Quick-reply input │
├─────────────────────────────────┤
│ ANUNCIOS PLATAFORMA │
│ - Announcement cards │
│ - Read-only for normal users │
└─────────────────────────────────┘
CommunicationsHub (page root)
├── ConnectionStatus
├── AlertsPanel
│ ├── AlertCard (severity, entity link, timestamp)
│ └── AlertsBadge
├── StreamsPanel
│ ├── StreamListItem (name, unread badge)
│ └── StreamDetail (expanded)
│ ├── TopicGroup
│ │ ├── MessageBubble (author, avatar, content, time)
│ │ └── QuickReply (text input + send button)
│ └── DeepLink ("Open in Zulip")
├── DirectMessagesPanel
│ ├── DMListItem (avatar, name, unread badge)
│ └── DMDetail (expanded)
│ ├── MessageBubble
│ └── QuickReply
└── AnnouncementsPanel
└── AnnouncementCard (title, date, content preview)
  • Simple text input (no formatting toolbar)
  • Enter to send, Shift+Enter for newline
  • Sends via POST /api/zulip/messages (attributed to real user)
  • Emoji support via :emoji_name: (Zulip renders)
  • Sticky input at bottom when stream/DM expanded (mobile: above virtual keyboard)
  • Use Zulip’s pre-rendered content field (HTML, sanitized before injection)
  • Relative timestamps (“5 min ago”, “yesterday 14:32”)
  • Author avatar from Zulip API

  1. Frontend mounts CommunicationsHub
  2. Calls POST /api/zulip/events/register → receives queue_id + initial state (unreads, subscriptions)
  3. Loop: GET /api/zulip/events?queue_id=X&last_event_id=Y
    • Blocks until events arrive (long-poll, ~90s timeout)
    • Event types: message, update_message, subscription, reaction, typing
  4. Frontend updates local state with each batch of events
  5. On unmount: DELETE /api/zulip/events (cleanup queue)
  • Poll failure (network timeout, pod restart): exponential backoff (1s, 2s, 4s, max 30s)
  • Queue expired (Zulip cleans after ~10 min without poll): re-register automatically
  • Visual connection status indicator in hub header (connected / reconnecting / error)
  • One long-poll connection per active user viewing the hub
  • api-gateway proxy timeout must be >= 120s
  • Queue cleaned up when user navigates away from /communications

Automates Zulip tenant lifecycle: creates streams, manages user subscriptions, and maintains tenant isolation.

TriggerProvisioner Action
Tenant created (via tenant-webhook)Create private streams from templates, create user group, subscribe bot
User added to tenantCreate Zulip user (if needed), subscribe to tenant streams
User removed from tenantUnsubscribe from tenant streams
Tenant deactivatedArchive streams (preserve history, don’t delete)
Admin changes stream configCreate/archive streams per new template config
  • Stack: Python Flask, Docker image on GHCR (ghcr.io/nkz-os/nkz-module-zulip/provisioner:latest)
  • Deployment: k8s/provisioner-deployment.yaml (already exists, replicas=0 until image built)
  • Auth: Uses bot API key from zulip-secret.bot-api-key
  • Reads config from: admin_platform.communications_config (PostgreSQL)
  • Idempotent: POST /api/provisioning/sync reconciles desired vs. actual state
tenant-webhook (Keycloak event)
→ POST /api/zulip/provisioning/tenant
→ provisioner creates streams + subscribes bot
tenant-user-api (user added/removed)
→ POST /api/zulip/provisioning/tenant/{id}/user
→ provisioner manages subscriptions

New section in Platform Admin UI: Comunicaciones (alongside existing tenant/user management).

  1. Bot Status: real-time health check (GET /api/v1/users/me with bot key). Visual indicator: connected/error. Regenerate API key button (calls Zulip Admin API to regenerate, then displays the new key with instructions to update the K8s Secret manually — direct K8s Secret mutation from UI is out of scope).

  2. Send Announcement: inline form to publish to #platform-announcements. Choose topic (maintenance, update, incident). Preview Markdown. Sends via bot.

  3. Notification Templates: editable Markdown templates used by N8N and platform services for alert formatting. Stored in admin_platform.communications_config (JSONB). Template variables: {sensor_name}, {value}, {threshold}, {timestamp}, {entity_link}.

  4. Stream Templates: configurable list of streams auto-created per tenant. Default: {tenant}-general, {tenant}-alerts. Admin can add/remove templates (e.g., {tenant}-field-ops). Provisioner reads this on tenant creation.

  • admin_platform.communications_config table (new migration):
    • key (text, PK): config identifier
    • value (JSONB): configuration data
    • updated_at (timestamptz)
  • Keys: bot_config, notification_templates, stream_templates
  • NOT in Orion-LD (platform config, not digital twins)

nkz-mobile (React Native)
→ WebView src="https://nekazari.robotika.cloud/communications"
→ postMessage({ type: 'NKZ_AUTH_INJECTION', token: '<jwt>' })
→ Hub authenticates, loads streams and alerts
  • 350px min width: all panels stack vertically, each collapsible
  • Quick-reply input: position: sticky at bottom when stream/DM expanded; virtual keyboard must not cover it
  • Deep links: “Open in full Zulip” uses window.open('', '_blank') to open system browser (not navigate within WebView)
  • Background/foreground: long-poll queue may expire when app is backgrounded; hub auto-re-registers on foreground
  • Push notifications: handled by Zulip’s official mobile app (separate install); NKZ does NOT duplicate push

Namespaces: zulip.json per language (es, en minimum).

Key groups:

  • hub.* — hub UI labels (header, panel titles, connection status)
  • alerts.* — alert severity labels, empty states
  • streams.* — stream list, topic labels
  • dm.* — direct messages labels
  • announcements.* — announcements panel
  • quickReply.* — input placeholder, send button, error states
  • admin.* — platform admin panel labels

Phase 0: Prerequisites (OPS)
├─ Activate OIDC (ZULIP-4): run keycloak-create-zulip-client.sh, patch secret, redeploy
└─ Create bot user in Zulip, store API key in zulip-secret
Phase 1: Backend (api-gateway)
├─ JWT → Zulip API key middleware
├─ Proxy routes /api/zulip/*
└─ Tenant filtering middleware
Phase 2: Provisioner service (parallel with Phase 3)
├─ Flask app with provisioning endpoints
├─ Docker image → GHCR
├─ Integration with tenant-webhook
└─ DB migration for communications_config
Phase 3: Communications Hub frontend (needs Phase 1)
├─ Component tree implementation
├─ Long-polling event system
├─ Quick-reply functionality
├─ i18n (es + en)
└─ IIFE build + MinIO deploy
Phase 4: Platform Admin panel (needs Phase 1 + 2)
├─ Bot management UI
├─ Announcement sender
├─ Template editor
└─ Stream template configuration

  • Full chat client (Zulip web already exists at messaging.robotika.cloud)
  • Push notifications in nkz-mobile (Zulip official app handles this)
  • Attachments or rich formatting in quick-reply
  • Message search (deep link to Zulip)
  • Video/voice calls
  • Custom Zulip themes or branding