Qué es TDPBX#
TDPBX es el panel de control de la flota Talkadillo. No es un PBX y no cursa llamadas: centraliza intención declarativa, telemetría y aprovisionamiento. Talkadillo —nodo SIP escrito en Rust que habla el protocolo backplane directamente— procesa las llamadas.
Reemplaza, no administra
Sustituye consolas aisladas de FreePBX/VitalPBX. El soporte Asterisk se eliminó del código (agente Go + modo observer retirados); los importadores en tools/ solo sirven para migrar configuración real al modelo declarativo.
Herramienta interna
No se vende ni se licencia a terceros. La jerarquía platform → resellers → customers modela la estructura real de clientes de Talkadillo, con scoping a nivel repositorio y aislamiento por cuenta.
Arquitectura y stack#
Monolito modular en Go con fronteras internas limpias (gateway, graph, config, provisioner, cdr, store). Separación estricta control/data: el backplane guarda intención, cada nodo la materializa.
Nodos, enrollment y protocolo#
Enrollment dirigido por operador: se crea el nodo en la consola, se genera un token de un solo uso y se ejecuta el install script en la máquina (descarga el agente de la ruta /downloads, se enrola en el primer arranque systemd y reconecta con su certificado). La emisión del certificado es la autorización.
- →rpc Connect(stream AgentEnvelope) returns (stream GatewayEnvelope) — un stream por nodo, envelopes con correlation_id.
- →Handshake declara node_software, capabilities[] y config_schema. Un nodo que declara nada nunca se policea contra baseline (los nodos existen antes de su primer handshake).
- →Heartbeat reporta llamadas activas, carga, memoria/disco, registros y active_config_version — lo que el nodo dice correr es autoritativo.
- →ConfigPush / ConfigAck con resultados APPLIED · REJECTED · DUPLICATE · SUPERSEDED · DEFERRED más findings[] (WARN = aplicó con matices, ERROR = rechazó).
- →CDRs por batch con ack (el agente solo avanza su cursor tras el ack), RegistrationSync por deltas, EventBatch idempotente por event_id, renovación de certs y rollouts de binario del agente.
Configuración y versiones#
La consola y la API operan sobre intención estructurada — jamás archivos crudos de PBX. El engine (config.Build) compila el NodeConfig desde un snapshot y omite bloques que Talkadillo no necesita (p. ej. todo SIPSettings).
Telefonía avanzada y E911#
Vive en nodes.settings.telephony (ahí lo lee el engine) con API tipada y validada encima: parqueo con órbitas y retorno al que parqueó, conferencias con PIN de moderador, paging unidireccional/intercom con política ante ocupado, grupos de pickup y set de prompts de buzón por nodo.
CDR y analytics#
CDRs en ClickHouse, no en Postgres: tabla ReplacingMergeTree, partición mensual, TTL 7 años. Entrega at-least-once con dedup determinista (UUID5 de la línea cruda) y lecturas con FINAL. Incluye topología de forking, causa SIP, app/route de dialplan y diagnóstico por pata.
- →cdrs (log paginado), callSummary (totales del mismo filtro), callBreakdown(dimension:) (12 dimensiones: caller, destino, nodo, cuenta, hora, día, outcome, cause, troncal, app, route) y callTrend.
- →Export GET /api/v1/cdrs.csv en streaming con el mismo filtro que la UI; un export truncado termina en línea # TRUNCATED, nunca pasa por respuesta completa.
- →Los agregados exigen ventana temporal: un GROUP BY sin filtro sería un scan de años de flota.
Archivo, compresión y transcripción#
Dos mundos con la misma palabra: recordings es la librería de prompts (panel → nodo, por gRPC) y media_assets es el archivo de llamadas y buzones (nodo → object storage, nunca dentro de una config).
Tatu, portal self-service y físicos#
Tres identidades separadas: User (staff, solo OIDC), PortalUser (empleado del cliente, login + TOTP) y Extension (registro SIP sin login). Un portal user puede tener varias extensiones y varios devices Tatu (móvil, escritorio y web).
- →Tatu: bootstrap REST (login/TOTP, rotación de refresh, QR de onboarding por staff, pairing entre devices), perfil myProvisioningProfile (líneas + policy efectiva + profileVersion para polling barato) y reporte de calidad (MOS/jitter) a ClickHouse.
- →Portal /portal: mis llamadas, buzones y ajustes propios. El sujeto se deriva, nunca se suministra. La reproducción de buzón otorga una sola URL por escucha (network-only) con check de pertenencia; los campos editables son un set fijo vía statement dedicado, no el update general.
- →Sesiones: staff cookie tdpbx_session (30 días idle, re-emitida al día, revocación inmediata por session_version); portal cookie tdpbx_portal verificada contra la fila del device en cada request; Tatu access token de 15 min.
- →Físicos: plantillas Yealink/Grandstream/Fanvil (librería global) y auto-provisión por /provisioning/{token}/{mac}.cfg — la URL es bearer y nunca se loguea.
Webhooks salientes#
La única forma en que el panel avisa hacia afuera. Una entrega es una deuda: la fila solo se retira con 2xx. Un hecho cubierto sin suscripción genera una fila undeliverable visible en vez de silencio verde.
- →La transición se decide por WRITE (UpdateNodeStatus retorna lo reemplazado): dos instancias jamás anuncian dos veces el mismo outage.
- →Idempotencia desde la transición: call.emergency usa el event_id del nodo; node.config_drift clavea (nodo, esperado, ejecutado, fecha UTC) para no notificar 2.880 veces al día.
- →Si la emergencia no se puede encolar, se acorta el ack del EventBatch para que el nodo reintente; si el payload no se puede construir, se salta para no atascar el stream. Firmado HMAC con TDPBX_WEBHOOK_ENCRYPTION_KEY dedicada; dispatcher con lease, batch = slots libres y drain en shutdown.
Seguridad y RBAC#
- →mTLS bilateral (RequireAndVerifyClientCert contra la CA propia). Sin puertos inbound abiertos en el borde; el audio RTP nunca sale de los nodos.
- →Roles + cuentas, independientes: viewer < accounting < engineer < admin < super_admin como piso por mutación; la cuenta filtra cada lectura/escritura por subárbol. El "manager de plataforma" y el "manager de cuenta" son el mismo rol sobre distintas cuentas.
- →User management anti-escalada: dentro del subárbol propio, sin actuar sobre superiores ni otorgar rol mayor al propio, nunca sobre uno mismo, y nunca dejando la plataforma sin super_admin activo. Entrar es OIDC: crear usuario no emite credencial, el primer login reclama la fila por email.
- →Auditoría: cada cambio de config deja snapshot + hash; secretos SIP visibles solo con rol engineer y lectura auditada.
- →Firewall observable, no gestionado: lo reportado (observed_sip) vs lo observado desde fuera (sonda sentinel TCP: RST = sin firewall, DROP = filtrado). Advisory siempre: nunca bloquea un push ni cambia estado. Rechaza loopback/RFC1918 y solo usa TCP.
Monitoreo y operación#
- →Tiempo real: subscriptions nodeStatus · nodeConfigStatus · nodeAgentUpdateStatus · nodeWssCertStatus · fleetStatus (vía Redis) + snapshot de operador (llamadas vivas, presencia) plegado desde eventos.
- →Liveness desconfiado: sweep a 3 intervalos de heartbeat que se niega a marcar offline si el nodo aún tiene stream local o si voltearía a la mayoría del fleet. Emite control_plane.degraded y métricas de supresión/fallos de escritura.
- →Rechazos visibles: node_connection_rejections agrega por (identidad, motivo, IP) con conteos — un nodo retirado que sigue llamando se ve en /connections en vez de perderse en logs.
- →Rollouts: updates de binario del agente por flota con concurrencia acotada y verificación SHA256; certs WSS vía ACME DNS-01; inventario cloud como snapshot con timestamp (nunca lectura viva).
API GraphQL#
Schema-first: api/graph/schema.graphql es la fuente de verdad y gqlgen genera los tipos Go. Convenciones: paginación cursor Relay (Connection/Edge), mutaciones con payload ({ recurso, errors }) y subscriptions para estado live.
Runbooks#
Procedimientos en docs/runbooks/ del repo:
Gaps conocidos#
Matriz verificada contra nodo lab (docs/PARITY.md). Talkadillo hoy no expresa: toggles DND *78/*79, desvío a móvil/externo, conditional forward ni miembros externos en ring groups. El aprovisionamiento cloud (crear instancias) y el relay de media por gRPC están especificados pero no implementados. La transcripción nunca hace fallback entre proveedores por diseño.
