# Notificaciones, flujos en vivo y tickets de soporte

Las tres formas en que Zaroz te cuenta lo que pasa, y la única por la que respondes. Los ejemplos usan el ayudante de [Usar la API de Zaroz](/es/guides/api/overview). Para los eventos de las unidades de un Workload (y sus webhooks) consulta [Eventos](/es/guides/units-api/events) y [Webhooks](/es/guides/units-api/webhooks).

## Notificaciones

Mensajes de la plataforma para tu cuenta: una factura, un servicio que falló, una invitación. Van de más reciente a más antigua y paginadas. `unread_count` es el total de todas las páginas, que es lo que muestra una insignia.

**curl**

```bash
curl -sS "$ZAROZ_API/me/notifications?page=1&page_size=20" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/me/notifications/$NOTIFICATION/read" -H "$AUTH"       # idempotente
curl -sS -X POST "$ZAROZ_API/me/notifications/read-all" -H "$AUTH"
```

**TypeScript**

```ts
import { call } from "./zaroz";

const feed = await call("GET", "/me/notifications?page_size=20");
console.log(`${feed.body.unread_count} sin leer`);
for (const n of feed.body.data) console.log(n.kind, n.title, n.href, n.read);
await call("POST", "/me/notifications/read-all");
```

**Python**

```python
from zaroz import call

_, feed = call("GET", "/me/notifications?page_size=20")
print(feed["unread_count"], "sin leer")
for n in feed["data"]:
    print(n["kind"], n["title"], n["href"], n["read"])
call("POST", "/me/notifications/read-all")
```

Marcar una notificación como leída es idempotente. Una notificación que no es tuya responde `404`, igual que una que no existe.

## Flujos en vivo (eventos enviados por el servidor)

Tres rutas mantienen abierto un `text/event-stream` para que no tengas que consultar periódicamente.

| Flujo | Ruta | Tramas |
|---|---|---|
| Tus nuevas notificaciones | `GET /me/notifications/stream` | `notification_created` |
| El estado en vivo de un servicio | `GET /service-instances/{id}/status/stream` | `status`, `network_metrics`, `resource_metrics`, `vm_progress` |
| La conversación de un ticket | `GET /support/tickets/{id}/events` | `message_sent`, `message_edited`, ... |

El servidor cierra cada conexión a los **quince minutos** y la primera trama fija el intervalo de reconexión, así que reconecta cuando termine (un `EventSource` del navegador lo hace solo). La trama `status` del flujo del servicio trae el cuadro completo (estado agregado, las acciones del ciclo de vida que tienen sentido ahora, la operación en curso, cada recurso visible con su uso en vivo) al conectar, cuando cambia un recurso y cada cinco segundos. Responde `404` en un servicio cuya suscripción está suspendida, terminada, cancelada o fallida.

**curl**

```bash
curl -sS -N "$ZAROZ_API/service-instances/$SERVICE/status/stream" -H "$AUTH" -H "Accept: text/event-stream"
```

**TypeScript**

```ts
// Node 20+: un fetch normal puede leer el flujo; EventSource no puede enviar la cabecera Authorization.
const res = await fetch(`${process.env.ZAROZ_API}/service-instances/${SERVICE}/status/stream`, {
  headers: { Authorization: `Bearer ${process.env.ZAROZ_TOKEN}`, Accept: "text/event-stream" },
});
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body as any) {
  buffer += decoder.decode(chunk, { stream: true });
  let i;
  while ((i = buffer.indexOf("\n\n")) >= 0) {
    const frame = buffer.slice(0, i);
    buffer = buffer.slice(i + 2);
    const event = /^event: (.*)$/m.exec(frame)?.[1];
    const data = /^data: (.*)$/m.exec(frame)?.[1];
    if (event === "status" && data) console.log(JSON.parse(data));
  }
}
```

**Python**

```python
import json, os, requests

url = f"{os.environ['ZAROZ_API']}/service-instances/{SERVICE}/status/stream"
headers = {"Authorization": "Bearer " + os.environ["ZAROZ_TOKEN"], "Accept": "text/event-stream"}
with requests.get(url, headers=headers, stream=True, timeout=(10, 60)) as r:
    event = None
    for line in r.iter_lines(decode_unicode=True):
        if line.startswith("event: "):
            event = line[7:]
        elif line.startswith("data: ") and event == "status":
            print(json.loads(line[6:]))
```

Usa un cliente HTTP normal como arriba para scripts. Un `EventSource` del navegador no puede poner la cabecera `Authorization`, así que las apps web hacen el flujo a través de su propio backend.

## Tickets de soporte

Los tickets pertenecen a una organización; todos los miembros ven los de la organización y pueden responder a los de un compañero. Las identidades del personal nunca se exponen: las respuestas aparecen como "Support Team".

**curl**

```bash
# abrir un ticket en una organización
curl -sS -X POST "$ZAROZ_API/organizations/$ORG/tickets" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"subject": "El servidor no arranca", "body": "Se reinicia en bucle desde el último despliegue. Id del servicio: …"}'
# o en tu organización personal
curl -sS -X POST "$ZAROZ_API/me/tickets" -H "$AUTH" -H "Content-Type: application/json" -d '{"subject": "…", "body": "…"}'

curl -sS "$ZAROZ_API/support/tickets?page_size=20" -H "$AUTH"
curl -sS "$ZAROZ_API/support/tickets/$TICKET" -H "$AUTH"                       # con sus mensajes
curl -sS -X POST "$ZAROZ_API/support/tickets/$TICKET/messages" -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"body": "Aquí van los logs que pedisteis."}'
```

**TypeScript**

```ts
const opened = await call("POST", `/organizations/${ORG}/tickets`, {
  subject: "El servidor no arranca",
  body: "Se reinicia en bucle desde el último despliegue.",
});
const TICKET = opened.body.ticket.id;
await call("POST", `/support/tickets/${TICKET}/messages`, { body: "Aquí van los logs que pedisteis." });
const detail = await call("GET", `/support/tickets/${TICKET}`);
for (const m of detail.body.messages) console.log(m.author_name, m.body);
```

**Python**

```python
_, opened = call("POST", f"/organizations/{ORG}/tickets", {
    "subject": "El servidor no arranca",
    "body": "Se reinicia en bucle desde el último despliegue.",
})
TICKET = opened["ticket"]["id"]
call("POST", f"/support/tickets/{TICKET}/messages", {"body": "Aquí van los logs que pedisteis."})
_, detail = call("GET", f"/support/tickets/{TICKET}")
for m in detail["messages"]:
    print(m["author_name"], m["body"])
```

Abrir un ticket en `/me/tickets` usa siempre tu organización personal; no hay organización que equivocar. La lista muestra una vista previa del último mensaje, el recuento de no leídos y el estado de la valoración. Cuando un ticket se cierra puedes enviar una valoración de satisfacción una vez: `POST /support/tickets/{id}/review` con `problem_solved` (booleano), `resolution_speed` y `response_quality` (enteros pequeños) y un `comment` opcional; solo puede hacerlo el propietario del ticket.

## Dar acceso de soporte a un servicio

Soporte puede pedir, dentro de un ticket, acceso a **uno** de tus servicios. Tú decides:

**curl**

```bash
curl -sS "$ZAROZ_API/support/tickets/$TICKET/access-requests" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/support/access-requests/$REQUEST/approve" -H "$AUTH"
curl -sS -X POST "$ZAROZ_API/support/access-requests/$REQUEST/deny" -H "$AUTH"
curl -sS "$ZAROZ_API/support/access-grants" -H "$AUTH"                         # quién está dentro ahora mismo
curl -sS -X DELETE "$ZAROZ_API/support/access-requests/$REQUEST" -H "$AUTH"    # retirar una aprobación
```

**TypeScript**

```ts
const grants = await call("GET", "/support/access-grants");
for (const g of grants.body) console.log(g.service_instance_id, g.expires_at, g.state);
await call("DELETE", `/support/access-requests/${grants.body[0].id}`); // retirar en cualquier momento
```

**Python**

```python
_, grants = call("GET", "/support/access-grants")
for g in grants:
    print(g["service_instance_id"], g["expires_at"], g["state"])
call("DELETE", f"/support/access-requests/{grants[0]['id']}")   # retirar en cualquier momento
```

Una concesión aprobada es para el único servicio que nombra la petición y nada más de lo que tienes. Empieza ahora y **termina sola**: tras `max_duration_hours`, en el momento en que se cierra el ticket o cuando la retiras. `GET /support/access-grants` responde solo las concesiones vivas, así que es la comprobación de "¿hay alguien dentro de mi infraestructura ahora mismo?".
