# 11 · Administración (equipo interno)

La administración es para **vuestro equipo**, no para las aplicaciones ni los clientes. Corre en un proceso y un dominio **separados** (`admin.<dominio>`, puerto 3002 en local) y se autentica con `ADMIN_TOKEN`. Nunca la expongáis a Internet sin SSO, segundo factor y lista de IPs.

Tiene dos formas de uso: la **consola web** y la **API** (`/admin/v1`), que hacen exactamente lo mismo.

## Consola web

Abrid la raíz del servicio de administración (`http://localhost:3002/` en local), pegad el `ADMIN_TOKEN` y entrad. El token se guarda solo en esa pestaña (`sessionStorage`) y se borra al cerrarla o pulsar *Salir*.

| Pantalla | Qué hacéis |
| --- | --- |
| **Panel** | Salud (colas, eventos de Meta fallidos, entregas abandonadas), clientes con pago bloqueado, números con problemas y qué parámetros de Meta de la plataforma están configurados |
| **Clientes** | Buscar, crear, suspender o reactivar el servicio. Cada ficha tiene cinco pestañas: *Resumen*, *Licencia* (estado, fecha de fin, límites, funciones y consumo del mes), **WhatsApp** (WABA, estado del token y de pago, números, conexión manual), *Apps y claves* y *Actividad* |
| **Aplicaciones** | Alta, orígenes permitidos, claves de API (crear y revocar, también las multi-tenant) y suscripciones a eventos |
| **Eventos de Meta** | Reprocesar los eventos fallidos de un rango |
| **Auditoría** | Quién cambió qué, filtrable por cliente |

### Dónde se indican los parámetros de Meta

| Qué | Dónde |
| --- | --- |
| App de Meta de la plataforma (`META_APP_ID`, `META_APP_SECRET`, `META_CONFIG_ID`, `META_VERIFY_TOKEN`) | Variables de entorno del servidor. El Panel solo comprueba que existen; nunca las muestra |
| **WABA, número y token de cada cliente** | Normalmente **los conecta el propio cliente** desde la pantalla *Configuración* de la aplicación con *Conectar un número* ([09](09-alta-de-numeros-y-onboarding.md)). Desde la consola, pestaña **WhatsApp** del cliente, se ven todos sus parámetros y se puede **releer de Meta**, cambiar el predeterminado y desconectar |
| Conexión a mano (WABA ID, Phone number ID, token y PIN opcional) | Pestaña **WhatsApp** del cliente, solo si `ALLOW_MANUAL_CONNECT=true`. Sirve para pilotos o cuando el cliente os ha dado acceso con un usuario del sistema. El token se guarda cifrado y no se vuelve a mostrar |

La consola no modifica el WABA en Meta: solo gestiona lo que la plataforma guarda y puede releer su estado.

### Seguridad de la consola

- Es estática y sin datos: toda la información se pide a `/admin/v1` con vuestro token. Su CSP no admite scripts ni estilos en línea y no puede ser incrustada.
- Pensada para una sola credencial compartida. **Antes de producción** ponedla detrás de SSO con segundo factor y lista de IPs; la auditoría registra `admin` como actor, no una persona concreta.

## API

Referencia completa: [endpoints](referencia/endpoints.md#administración) · la API es la misma que usa la consola.

```bash
export ADMIN=http://localhost:3002
export ADMIN_TOKEN=$(jq -r .adminToken .dev/playground.json)    # en local; en producción, vuestro secreto
alias adm='curl -sS -H "Authorization: Bearer $ADMIN_TOKEN" -H "Content-Type: application/json"'
```

## Receta: dar de alta un cliente nuevo, de cero a enviando

### 1. Registrar la app (una vez por aplicación, no por cliente)

```bash
adm -X POST $ADMIN/admin/v1/apps -d '{
  "code": "agenda",
  "name": "Agenda de citas",
  "allowed_origins": ["https://agenda.ejemplo.com"]
}'
```

`allowed_origins` son los orígenes **exactos** que podrán incrustar las pantallas (se usan en la cabecera `frame-ancestors`). Añadid cada entorno (staging, producción). Cambiarlos: `PATCH /admin/v1/apps/{app_id}`. Desactivar una app (`"status":"disabled"`) corta todas sus claves de golpe.

### 2. Crear el tenant (un cliente de vuestra aplicación)

```bash
adm -X POST $ADMIN/admin/v1/tenants -d '{
  "external_ref": "CLI-0042",
  "name": "Clínica Dental Sol",
  "default_locale": "es",
  "timezone": "Europe/Madrid",
  "data_retention_days": 365
}'
```

`external_ref` es **el identificador del cliente en vuestro sistema** (único): es lo que las apps mandan en `X-Tenant-Ref` y lo que vuelve en cada evento como `tenant_ref`.

### 3. Fijar la licencia

```bash
adm -X PUT $ADMIN/admin/v1/tenants/ten_…/license -d '{
  "status": "active",
  "valid_until": "2027-10-12T00:00:00Z",
  "limits": {
    "max_numbers": 1,
    "max_agents": 5,
    "max_templates": 30,
    "max_outbound_per_month": 5000,
    "features": ["chat", "templates", "api", "embed", "costs"]
  },
  "notes": "Plan Profesional, contrato 2026-118"
}'
```

| `status` | Efecto |
| --- | --- |
| `trial` / `active` | Funciona dentro de los límites y de `valid_until` |
| `suspended` | Todo `403 license_inactive`. Los datos se conservan |
| `expired` | Igual que suspendida; se aplica sola al pasar `valid_until` |

Las `features` apagan funcionalidades concretas (`403 feature_not_licensed`). Los límites se aplican al intentar crear/enviar (`403 license_limit_reached`). Quitar un límite: omitid el campo. La licencia se cachea hasta **5 s**.

### 4. Habilitar la app para ese tenant

```bash
adm -X PUT $ADMIN/admin/v1/tenants/ten_…/apps/app_… -d '{"enabled": true}'
```

Una app puede usarse con un tenant solo si está habilitada aquí. Con `false` se corta el acceso de esa app a ese tenant sin tocar a los demás.

### 5. Crear la clave de API

```bash
# Clave de UN tenant (la más segura):
adm -X POST $ADMIN/admin/v1/apps/app_…/api-keys -d '{
  "name": "agenda-prod-CLI-0042",
  "tenant_id": "ten_…",
  "scopes": ["messages:send", "messages:read", "embed:create", "usage:read"]
}'

# Clave multi-tenant (omitid tenant_id; la app elige con X-Tenant-Ref):
adm -X POST $ADMIN/admin/v1/apps/app_…/api-keys -d '{"name":"agenda-prod","scopes":["messages:send","embed:create"]}'
```

**El valor de la clave (`key`) solo se muestra en esta respuesta.** Guardadla en el gestor de secretos de la app. Se puede poner `expires_at` y `rate_limit_per_min` (por defecto 600). Para rotar: crear la nueva, desplegar y revocar la antigua con `DELETE /admin/v1/api-keys/{key_id}` (o el botón *Revocar* de la consola), que sirve para cualquier clave, también las multi-tenant. Se puede listar con `GET /admin/v1/apps/{app_id}/api-keys`. Poned `expires_at` al crearlas para acotar el riesgo. Alcances: [referencia](referencia/alcances.md).

### 6. Suscribir la app a los eventos

```bash
adm -X POST $ADMIN/admin/v1/apps/app_…/webhook-subscriptions -d '{
  "url": "https://agenda.ejemplo.com/hooks/whatsapp",
  "events": ["whatsapp.message.*", "whatsapp.number.*", "whatsapp.account.*"]
}'
```

Devuelve el **secreto de firma** (`whs_…`, solo se muestra ahí). Cubre a todos los tenants de la app. La app también puede crear suscripciones propias con su clave ([05](05-recibir-eventos.md)).

### 7. El cliente conecta su número

Ahora el cliente (o vuestro soporte, con él delante) pulsa **Conectar un número** en la pantalla de configuración: [09](09-alta-de-numeros-y-onboarding.md).

*Solo en desarrollo*, para saltarse Embedded Signup con un token ya obtenido en el panel de Meta:

```bash
adm -X POST $ADMIN/admin/v1/tenants/ten_…/whatsapp/manual-connect -d '{
  "waba_id": "…", "phone_number_id": "…", "access_token": "EAAG…"
}'
```

Está **desactivado en producción** (`ALLOW_MANUAL_CONNECT=false`). El token se guarda cifrado y nunca vuelve a mostrarse.

## Operación diaria

| Quiero… | Cómo |
| --- | --- |
| Ver todos los tenants | `GET /admin/v1/tenants` (paginado; filtros `status` y `q`) |
| Ver el WABA y los números de un tenant | `GET /admin/v1/tenants/{id}/whatsapp` |
| Releer un número desde Meta · fijar el predeterminado · desconectarlo | `POST …/whatsapp/numbers/{number_id}/refresh` · `PATCH …/numbers/{number_id}` con `{"is_default":true}` · `DELETE …/numbers/{number_id}` |
| Qué parámetros de Meta están configurados | `GET /admin/v1/system` |
| Suspender el servicio de un cliente (impago de la licencia, abuso) | `POST /admin/v1/tenants/{id}/suspend` con `{"reason":"…"}` — pone la **licencia** en `suspended`: corta API, UI y envíos, y los mensajes ya en cola pasan a `failed` (reintentables). Los datos se conservan |
| Reactivarlo | `PUT …/license` con `"status":"active"` (el estado del tenant, `PATCH … {"status"}`, es otra cosa: `suspended`/`closed` lo bloquea por completo) |
| Cambiar límites o fechas | `PUT …/license` otra vez (sustituye la licencia entera) |
| Ver la salud | `GET /admin/v1/health` |
| Reprocesar eventos de Meta que fallaron | `POST /admin/v1/webhook-events/reprocess` con `{"from","to"}` (por defecto solo `failed`) |
| Exportar los datos de un tenant | `GET /admin/v1/tenants/{id}/export` — **aún responde `501`** |

### Qué mirar en `GET /admin/v1/health`

| Campo | Cuándo preocuparse |
| --- | --- |
| `status` | `degraded` |
| `queues.queued_messages`, `oldest_queued_age_seconds` | Crecen o superan ~1 min: el worker está parado o saturado |
| `queues.failed_last_hour` | Pico: revisar errores de Meta o de un tenant concreto |
| `webhook_events.failed` | Eventos de Meta que no se pudieron procesar → corregir y `reprocess` |
| `deliveries.dead` | Receptores de apps que llevan días caídos (los reenvíos se agotaron) |
| `payment_blocked` | Tenants con la tarjeta bloqueada en Meta: avisarles |
| `numbers_with_problems` | Calidad `RED`, números `flagged` o desconectados |

Usadlo como comprobación de vuestro monitor y alertad si `status != ok`. Los procesos exponen también `GET /healthz` (vivo) y `GET /readyz` (con base de datos) sin autenticación para el orquestador.

## Cosas que conviene saber

- **Auditoría.** Queda en `audit_log` (`GET /admin/v1/audit-log`, pantalla *Auditoría*) el alta y edición de tenants, la licencia (incluida la suspensión), la habilitación de apps, el alta y edición de apps, la creación y revocación de claves, las suscripciones de app y las acciones sobre números. Los valores secretos nunca se guardan.
- **Desactivar no borra.** No hay borrado de tenants por API: un tenant cerrado (`closed`) conserva datos hasta su `data_retention_days`.
- Los ids de las respuestas (`ten_…`, `app_…`) son los que usan las rutas. Guardad el de la app y el del tenant junto a `external_ref`.
- Las claves de API, el secreto de firma y los tokens de Meta **no se pueden recuperar**: se regeneran.
