puertos

Notificaciones de bajas y apuntes — cómo funcionan y cómo activarlas

El problema que resuelve esto

Antes, el aviso de “alguien ha comunicado una baja” o “alguien se ha apuntado” se generaba desde el propio navegador: planificador.html sondeaba Supabase cada 60 segundos mientras la página estaba abierta, y desde ahí mandaba el mensaje de Telegram. En cuanto cerrabas la app, bloqueabas el móvil un rato, o Android mataba la pestaña en segundo plano (todos los fabricantes lo hacen para ahorrar batería — Xiaomi/Huawei/Samsung de forma especialmente agresiva), el sondeo se paraba y no llegaba nada más. Esto es así en cualquier sitio web que dependa de JavaScript de página para avisar — no es un fallo puntual, es la naturaleza del mecanismo.

La solución: que sea Supabase quien mande el aviso a Telegram, en el momento exacto en que se guarda la fila en la base de datos — sin pasar por tu navegador para nada. Así llega igual con el móvil bloqueado o la app cerrada.

Esto se hace con dos piezas de Supabase, ambas configurables desde su panel web (no hace falta instalar nada ni usar la terminal):

  1. Una Edge Function (supabase/functions/notificar-telegram/index.ts, ya escrita en este repo) — recibe el aviso de Supabase y llama a la API de Telegram.
  2. Dos Database Webhooks — le dicen a Supabase “cuando se inserte una fila en bajas (o en refuerzos), llama a esa función”.

Paso 1 — Crear tu bot de Telegram (si no lo tienes ya)

  1. Habla con @BotFather en Telegram → /newbot → sigue los pasos → te da un token (algo como 123456789:AAbc...).
  2. Habla con tu bot nuevo (dile “hola” o /start) para que sepa quién eres.
  3. Habla con @userinfobot para obtener tu Chat ID (un número).
  4. Opcional: pega ambos datos en Planificador → 🔔 (campana) → Configurar → Telegram y pulsa 📤 Enviar prueba para confirmar que el bot funciona. Esos campos solo sirven para esta prueba — el envío real de Supabase usa su propia copia del token (paso 3).

Paso 2 — Publicar la Edge Function

  1. Entra en tu proyecto de supabase.com.
  2. Menú lateral → Edge Functions → Create a new function.
  3. Nómbrala exactamente notificar-telegram.
  4. Borra el código de ejemplo que trae por defecto y pega el contenido completo de supabase/functions/notificar-telegram/index.ts de este repositorio.
  5. Deploy.
  6. Dentro de la función → pestaña Secrets (o Settings) → añade:
    • TELEGRAM_BOT_TOKEN = el token del paso 1
    • TELEGRAM_CHAT_ID = tu Chat ID del paso 1

    (SUPABASE_URL y SUPABASE_SERVICE_ROLE_KEY ya están disponibles automáticamente en toda Edge Function, no hace falta añadirlas.)

Paso 3 — Crear los dos Database Webhooks

  1. Menú lateral → Database → Webhooks → Create a new webhook.
  2. Webhook 1 — bajas
    • Name: Baja nueva → Telegram
    • Table: bajas
    • Events: marca solo Insert
    • Type: Supabase Edge Functions
    • Edge Function: notificar-telegram
    • HTTP method: POST (por defecto)
    • Guardar.
  3. Webhook 2 — refuerzos
    • Name: Apunte nuevo → Telegram
    • Table: refuerzos
    • Events: solo Insert
    • Type: Supabase Edge Functions → notificar-telegram
    • Guardar.

Al elegir el tipo “Supabase Edge Functions” (en vez de “HTTP Request”), Supabase añade la autenticación necesaria automáticamente — no hay que tocar cabeceras a mano.

Paso 4 — Probar

Ampliación A — Registro de actividad y avisos de cancelación

Con lo anterior te avisa cuando alguien se da de baja o se apunta. Lo que no avisaba (ni dejaba rastro con hora en ninguna tabla) es lo contrario: un apunte cancelado, una baja anulada o alguien quitado de un turno. Esto lo resuelve.

  1. Supabase → SQL Editor → New query → pega el contenido de supabase/01_actividad.sql → Run. Crea la tabla actividad y unos disparadores que anotan, con hora, cada alta/baja de apuntes, bajas y equipos, lo haga quien lo haga y aunque la app esté cerrada. También hace que una baja que se vuelve a registrar sobre otra anulada cuente como nueva (antes no generaba aviso, porque el sistema la trataba como una modificación).
  2. Edge Functions → notificar-telegram → pega de nuevo el código actualizado de index.ts → Deploy.
  3. Database → Webhooks:
    • Edita el webhook de bajas y marca también Update (además de Insert).
    • Crea uno nuevo: tabla actividad, evento Insert, tipo Supabase Edge Functions → notificar-telegram. (Solo genera aviso para apunte cancelado y baja anulada; lo demás ya tenía el suyo.)
  4. Comprobar: cancela un apunte de prueba desde Sustituciones → llega “❌ Apunte cancelado”. En 🛰️ En vivo dejará de salir el aviso de que las cancelaciones “se detectan solo desde este navegador”: ahora salen siempre.

El registro guarda 90 días (lo limpia la tarea del paso B).

Ampliación B — Aviso de “turno en riesgo”

Cada hora, Supabase revisa los turnos de las próximas 48 h y te avisa por Telegram de los que no llegan al mínimo de voluntarios, con quién hay, quién dio baja y el enlace a Sustituciones. No repite el aviso: solo vuelve a escribir si cambia la situación (alguien se apunta o se da de baja), y te dice “✅ Ya cubierto” cuando se resuelve. No escribe entre las 22:00 y las 08:00 (hora de Madrid).

  1. Edge Functions → Create a new function → nómbrala exactamente avisos-turnos → pega el contenido de supabase/functions/avisos-turnos/index.ts (un solo archivo) → Deploy.
  2. En esa función, desactiva “Verify JWT” (Details/Settings). Queda protegida por una clave propia (siguiente paso), no por el JWT.
  3. Secrets de esa función (los de Telegram ya los tienes en la otra; hay que repetirlos aquí porque cada función tiene los suyos):
    • TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID
    • CRON_SECRET = un texto largo cualquiera (inventado por ti)
    • opcionales: MIN_EQ (mínimo por turno, 3 por defecto) y HORIZONTE_H (48).
  4. SQL Editor → abre supabase/02_avisos.sql, cambia CAMBIA_ESTE_SECRETO por el mismo texto de CRON_SECRET y ejecútalo. Crea la tabla avisos_enviados y programa la tarea horaria.
  5. Probar sin esperar a la hora en punto, con la función en modo simulación (no envía nada, solo muestra qué avisaría):

    curl -X POST "https://inifdflvblnonzowbxie.supabase.co/functions/v1/avisos-turnos?dry=1" -H "x-cron-secret: TU_SECRETO"
    

    Para un aviso real, la misma llamada sin ?dry=1 (dentro de 08:00–21:59). Turnos anulados. Si en el planificador anulas un turno (Bajas → 🚫 Anular turno), esta función deja de avisar de él. Si ya la tenías desplegada, vuelve a pegar el código actualizado de index.ts y pulsa Deploy (necesita la tabla de la Ampliación C; si aún no existe, la ignora).

  6. Si algo falla: Edge Functions → avisos-turnos → Logs, y en SQL: select * from net._http_response order by id desc limit 5;

Ampliación C — Turnos que no salieron adelante (para las estadísticas)

Al importar cuadrantes de meses anteriores (Estadísticas → 📥 Importar turnos anteriores), las franjas en blanco o canceladas se guardan en una tabla propia.

  1. Supabase → SQL Editor → New query → pega el contenido de supabase/03_turnos_no_realizados.sql → Run. Crea la tabla turnos_no_realizados (solo fechas y franjas, sin nombres).
  2. Comprobar: select * from public.turnos_no_realizados; debe devolver 0 filas sin error.

Sin este paso, la importación sigue funcionando pero solo guarda las asignaciones (la ventana de importación lo avisa).

Ampliación D — Bajas que solo se recuerdan (mínimo por mes)

Para indicar “en abril hubo al menos 2 bajas” sin saber cuáles (Estadísticas → 🩺 Calidad de los datos → ✎ Indicar bajas).

  1. Supabase → SQL Editor → New query → pega supabase/04_ajustes_estadisticas.sql → Run. Crea la tabla ajustes_estadisticas (un mes, un número y una nota).
  2. Comprobar: select * from public.ajustes_estadisticas; debe devolver 0 filas sin error.

Sin este paso, el botón “Indicar bajas” te avisa de que falta la tabla.

Ampliación E — Apuntes y bajas de meses hechos solo con apuntes (Telegram)

Para meses como julio y agosto, gestionados con el formulario de apuntes de Sustituciones y sin cuadrante, los avisos que el bot te mandó a Telegram guardan quién se apuntó y quién se dio de baja. La app los lee del chat exportado.

  1. Supabase → SQL Editor → New query → pega supabase/05_archivo_apuntes_bajas.sql → Run. Crea apuntes_archivo y bajas_archivo. Son tablas aparte a propósito: refuerzos y bajas disparan avisos a Telegram y no quieres un mensaje por cada apunte antiguo.
  2. En Telegram Desktop: abre el chat del bot → ⋮ → Exportar historial del chat → marca solo Mensajes de texto → formato JSON → Exportar.
  3. En la app: 📈 Estadísticas → 💬 Desde Telegram → elige el result.json. Revisa la vista previa (apuntes y bajas nuevos, cuáles ya existían, cuántos turnos llegan a 3), asigna a quién corresponde cada nombre dudoso y pulsa Importar. Se puede deshacer desde la misma ventana.

Límites que conviene saber:

Ampliación F — Marcar un mes como «sin actividad» o «sin datos»

Para un mes que no aparece en la app y que no se puede recuperar: sin actividad (no hubo turnos, p. ej. agosto) o sin datos (los hubo, pero no queda registro, p. ej. julio si el bot que avisaba era otro). Así deja de avisarse como hueco y no cuenta en las cifras.

  1. Supabase → SQL Editor → New query → pega supabase/06_marca_mes.sql → Run (requiere haber ejecutado antes el 04).
  2. En la app: Estadísticas → 🩺 Calidad de los datos, en la fila del mes → 🌴 No hubo actividad o 🗂️ No hay datos. Se puede quitar con Quitar marca.

Ampliación G — Cómo salió cada turno (indicado a mano) y estadística de apuntes

Para turnos cuyo registro no cuenta lo que pasó, sobre todo de meses gestionados fuera de la app: el que se iba a caer por las bajas y salió gracias a los apuntes, el que solo estaba en apuntes y salió adelante, o el que tenía 3 confirmadas y acabaron siendo 4.

  1. Supabase → SQL Editor → New query → pega supabase/07_turnos_resultado.sql → Run (si ya lo habías ejecutado, vuelve a ejecutarlo: añade la columna tipo).
  2. En la app: Estadísticas → 🩺 Calidad de los datos → 🧾 Turnos (fila del mes), o 🙋 Turnos que salieron gracias a los apuntes → 🧾 Indicar. Para cada turno:
    • 🛟 Salvado por apuntes: tenía equipo confirmado, se iba a caer por las bajas y los apuntes lo salvaron.
    • 🆕 Solo en apuntes: no tenía cuadrante, solo estaba disponible en apuntes y salió adelante (como junio y julio si se hicieron así). Hay botones para marcar todos los del mes de una u otra manera.
    • Asistieron: cuántas personas vinieron en total. Vacío = lo que consta.

Lo indicado manda sobre el registro. Si marcas cualquiera de las dos pero el turno no llega a MIN_EQ (3) personas, no cuenta como realizado. Solo se pueden indicar turnos que ya existen en la app (si falta alguno, impórtalo antes).

La sección 🙋 Turnos que salieron gracias a los apuntes da la cifra de todo el histórico (sin depender del periodo) con el desglose por mes, separando 🛟 salvados tras una baja y 🆕 solo en apuntes; junta lo que el registro muestra y lo indicado a mano (✍).

Ampliación H — Apuntes visibles en el cuadrante y franjas alternativas

El cuadrante ahora avisa de los apuntes. Antes, un voluntario apuntado en Sustituciones para un turno que ya tenía equipo (o que resolvía el motor) no aparecía por ningún sitio del cuadrante — solo “disponibles adicionales” calculado por disponibilidad general, no por apuntes reales. Ahora, si alguien está apuntado a un turno concreto, sale ahí con ✋, aunque el turno ya esté confirmado. Y en Apuntes, cada tarjeta muestra si ese turno ya tiene equipo confirmado (y cuántos) antes de la lista de apuntados, para no confundir “hay un apunte” con “el turno está sin cubrir”.

Franjas alternativas. En Calendario, cada franja habilitada se puede marcar como 🔀 alternativa (clic otra vez sobre el botón ya habilitado). Se sigue ofreciendo igual en Sustituciones, pero si al final nadie la cubre no cuenta como turno perdido en las estadísticas — se ofrecía por si acaso, no era un hueco que hubiera que cubrir sí o sí. Si sí se cubre, cuenta como cualquier otro turno.

Los apuntes de días ya pasados dejan de mostrarse. En Sustituciones → Mis apuntes, un apunte de un día que ya pasó no aparece más (si se confirmó, ya está en el cuadrante; si no, no hay nada que hacer con él). El dato se conserva en la base de datos para las estadísticas, solo deja de listarse.

Ampliación I — Avisos por correo a cada voluntario (no al coordinador)

Hasta aquí todos los avisos van siempre al mismo sitio: tu Telegram, como coordinador. Esto es distinto: que cada voluntario reciba su propio correo cuando se registra su baja, cuando se confirma un turno suyo, cuando le quitan uno ya confirmado, y un recordatorio el día antes de su turno (con una nota de “lleva la llave” si es portador). También manda un recibo por correo al guardar la disponibilidad por bloques.

El email de cada voluntario lo pones tú, desde su ficha en el planificador — no se pide en el formulario público, para no fiarse de que cada uno lo escriba bien.

  1. Crea una cuenta en Resend (gratis hasta 3.000 correos/mes). En Domains, verifica un dominio propio si tienes uno; si no, usa el dominio de pruebas de Resend para empezar (los correos solo llegan a direcciones que tú mismo hayas verificado en esa cuenta — para enviar a cualquier voluntario hace falta verificar un dominio propio tarde o temprano). En API Keys, crea una clave.
  2. Supabase → SQL Editor → New query → pega supabase/12_notificar_email.sql, cambia CAMBIA_ESTE_SECRETO por un texto largo inventado por ti → Run. Añade voluntarios.email, la tabla recordatorios_enviados y la tarea diaria.
  3. Edge Functions → Create a new function → nómbrala exactamente notificar-email → pega index.ts → Deploy. Secrets: RESEND_API_KEY (paso 1), RESEND_FROM (p. ej. Turnos Puerto <avisos@asispuerto.org>, con el remitente verificado en Resend).
  4. Database → Webhooks → Create a new webhook:
    • Name: Actividad → correo voluntario
    • Table: actividad, Events: solo Insert
    • Type: Supabase Edge Functions → notificar-email
  5. Edge Functions → Create a new function → nómbrala exactamente recordatorios-turnos → pega index.ts → Deploy. Desactiva Verify JWT (la protege CRON_SECRET, igual que avisos-turnos). Secrets: RESEND_API_KEY, RESEND_FROM (los mismos del paso 3) y CRON_SECRET_RECORDATORIOS = el mismo texto del paso 2 (no CRON_SECRET: ese ya lo usa avisos-turnos).
  6. En el planificador, pestaña Voluntarios → Ficha de cada persona: escribe su correo y pulsa 💾. Queda guardado al momento.
  7. Probar: registra una baja de prueba desde Sustituciones (de alguien con correo guardado) — debería llegarle el aviso en unos segundos. Para el recordatorio diario, sin esperar al cron:
    curl -X POST "https://inifdflvblnonzowbxie.supabase.co/functions/v1/recordatorios-turnos" -H "x-cron-secret: TU_SECRETO"
    
  8. Si algo falla: Edge Functions → notificar-email o recordatorios-turnos → Logs. Un voluntario sin correo guardado nunca da error, simplemente no recibe nada (comprobar con select nombre, email from public.voluntarios where email is not null;).

Qué queda igual

Si algún día quieres notificaciones nativas con el icono de la app

En vez de (o además de) Telegram, existe Web Push: notificaciones del sistema con el nombre e icono de “Turnos Puerto” en vez de llegar como mensaje de Telegram. Necesita generar un par de claves VAPID, guardar la “suscripción” push de tu móvil en Supabase, y que notificar-telegram (o una función hermana) llame al endpoint de push en vez de a Telegram. Es más trabajo de configurar y, en algunos móviles con gestión de batería muy agresiva, menos fiable que Telegram. Si lo quieres más adelante, dímelo y lo montamos.