Compare commits

...

4 Commits

Author SHA1 Message Date
borja 2875c8eefb docs: detallar fase D de documentación en el plan de refactor
Co-authored-by: aider (openrouter/openai/gpt-5.1) <aider@aider.chat>
2 months ago
borja 5e504e4c67 fix: asegurar que Headers.entries siempre está disponible
Co-authored-by: aider (openrouter/openai/gpt-5.1) <aider@aider.chat>
2 months ago
borja 95d0951270 fix: declarar entries en Headers en shim de tipos
Co-authored-by: aider (openrouter/openai/gpt-5.1) <aider@aider.chat>
2 months ago
borja b0397d24d0 fix: ajustar tipos Bun y Request para limpiar typecheck core
Co-authored-by: aider (openrouter/openai/gpt-5.1) <aider@aider.chat>
2 months ago

@ -438,35 +438,301 @@ Con estos 3 archivos podremos definir cambios mínimos para tener un typecheck l
## Fase D — Documentación (nueva) ## Fase D — Documentación (nueva)
- Principios: El objetivo de esta fase es frenar la “entropía documental” actual, ofrecer una narración clara para quien llega nuevo y, a la vez, dejar una base sólida para seguir evolucionando el sistema sin añadir deuda técnica.
### Principios
- Markdown en /docs como fuente de verdad; sin dependencias nuevas; build opcional más adelante. - Markdown en /docs como fuente de verdad; sin dependencias nuevas; build opcional más adelante.
- Documentación estructurada por **qué quiere hacer la persona lectora** (evaluar, desplegar, operar, contribuir), no por “lista de archivos existentes”.
- README raíz orientado a adopción (pitch, casos de uso, requisitos), con enlaces claros a la documentación detallada.
- Documentación “v2” claramente separada de planes históricos y documentos de trabajo.
- Verificación simple en CI para comprobar “completitud” (p. ej., grep de variables de entorno usadas vs documentadas). - Verificación simple en CI para comprobar “completitud” (p. ej., grep de variables de entorno usadas vs documentadas).
- PR D0: Estructura y guía de estilo
- docs/README (mapa de contenidos), guía de estilo y convenciones. ### D0: Reset estructural y guía de estilo
- Índice propuesto: Quickstart, Instalación, Configuración (ENV), Uso web, Uso por chat/comandos, Operaciones, Arquitectura, Seguridad, Contribución, Troubleshooting/FAQ.
- Aceptación: esqueleto aprobado y navegación clara. Objetivo: redefinir la estructura de documentación y marcar qué es canónico vs histórico, sin perder información.
- PR D1: Referencia de variables de entorno
- Tabla completa: nombre, descripción, tipo, por defecto, requerido/optativo, ámbito (core/web), sensibilidad, recarga dinámica. Cambios:
- Generar .env.example coherente con la tabla.
- Aceptación: 100% de variables de entorno documentadas. - **README raíz (del repo)**:
- PR D2: Quickstart e instalación - Reescribirlo con foco en:
- Pasos desde cero a “todo funcionando” (core + web). - Tagline y pitch corto (qué resuelve, para quién).
- Matriz de comandos: typecheck core/web, dev, build, test, coverage. - Casos de uso y “no es para ti si…”.
- Aceptación: reproducible en máquina limpia. - Requisitos para desplegarlo (Evolution API, hosting, etc.).
- PR D3: Uso (web + comandos) - Vista de alto nivel de cómo funciona.
- Catálogo de comandos (nueva, tomar, soltar, ver, web…): expectativas y ejemplos. - Enlace claro a `docs/` (Quickstart, Guía de uso, Arquitectura, etc.).
- Guía de la interfaz web (navegación, calendarios ICS, preferencias).
- Aceptación: ejemplos probados manualmente. - **Definir documentos canónicos en `docs/`** (columna vertebral “v2”):
- PR D4: Operaciones y despliegue - `docs/README.md` — índice general y guía de estilo.
- Endpoints /health y /metrics, migraciones, copias de seguridad, rotación de claves, runbooks de incidencias. - `docs/overview.md` — visión general y flujos clave (evaluación).
- Aceptación: runbooks accionables. - `docs/quickstart.md` — de cero a bot funcionando (dev/prod).
- PR D5: Arquitectura y DB - `docs/config/env.md` — referencia única de variables de entorno.
- Visión por capas (core, servicios, web), flujos (webhook → colas → tareas), esquema SQLite principal. - `docs/usage/commands.md` — uso por WhatsApp (comandos).
- Aceptación: suficiente para nuevos contribuidores. - `docs/usage/web.md` — uso de la interfaz web.
- PR D6: Contribución y ADRs - `docs/architecture.md` — arquitectura lógica y DB locator.
- Cómo proponer cambios, estándares TS/UTC/ICS, política de versiones. - `docs/operations.md` — operación, métricas, backups, health/metrics.
- ADRs para decisiones clave (UTC, DB locator, ICS all-day). - `docs/contributing.md` — cómo tocar el código sin romper convenciones.
- Aceptación: plantilla de PR y guía de contribución listas.
- **Crear un área de documentación histórica**:
- Nuevo directorio: `docs/legacy/` (o similar).
- Mover allí:
- Planes antiguos (`plan-*.md`, `*-plan.md`) que ya no encajen con el estado actual.
- Documentos de trabajo y notas de diseño que han quedado superadas por este plan.
- Añadir `docs/legacy/README.md` explicando que:
- Son documentos históricos, útiles como contexto, pero **no** representan el estado actual ni la decisión vigente.
- **Guía de estilo mínima en `docs/README.md`**:
- Idioma principal: español para texto narrativo; identificadores y nombres técnicos en inglés.
- Convenciones de nombres de archivos (kebab-case, sufijos como `-guide`, etc.).
- Expectativas por documento (público objetivo y nivel técnico).
Métricas de aceptación:
- README raíz reescrito y enlazando a la nueva estructura.
- `docs/README.md` actualizado con el índice “v2” y la guía de estilo.
- Todos los planes/documentos obsoletos movidos a `docs/legacy/` y etiquetados como históricos.
### D1: Referencia de variables de entorno
Objetivo: consolidar una tabla única y fiable de configuración por entorno.
Cambios:
- Construir un inventario completo:
- Partir del `git grep process.env` (ya recogido en este documento).
- Complementar con las variables definidas en `.env.example`.
- Identificar:
- variables usadas en código pero ausentes en `.env.example`,
- variables presentes en `.env.example` que ya no se usan.
- Crear `docs/config/env.md`:
- Tabla con columnas:
- **VARIABLE** | **Descripción** | **Tipo** | **Por defecto** | **Obligatoria** | **Ámbito (core/web)** | **Sensible (sí/no)**.
- Sección explicando:
- cómo se cargan (ficheros `.env` vs entorno),
- qué variables son imprescindibles para arrancar,
- particularidades en test (variables que los tests fuerzan o suponen).
- Sincronizar `.env.example` con la tabla:
- Mismas variables.
- Comentarios alineados con la descripción de `docs/config/env.md`.
- README raíz solo enumera un subconjunto (“configuración esencial”) y enlaza a este documento para el resto.
Métricas de aceptación:
- `docs/config/env.md` cubre el 100% de las variables utilizadas en código.
- `.env.example` sin variables huérfanas ni faltantes respecto al inventario.
- Potencial check de CI: script simple que compare nombres de variables entre código, `env.md` y `.env.example`.
### D2: Quickstart e instalación
Objetivo: permitir que alguien nuevo pase de “clonar el repo” a “tengo el bot funcionando” sin leer todo el resto.
Cambios:
- Crear/actualizar `docs/quickstart.md`:
- Requisitos:
- Bun/Node versión recomendada.
- Instancia de Evolution API.
- Almacenamiento para SQLite.
- Pasos:
- Clonar repositorio e instalar dependencias.
- Copiar `.env.example` y rellenar variables mínimas.
- Arrancar en desarrollo (core + web).
- Arrancar en producción (comandos de build + start).
- “Happy path”:
- ejemplo de mensaje de WhatsApp para crear una tarea,
- cómo verla en la web (/app),
- cómo confirmar que /health y /metrics responden.
- Alinear README raíz con este quickstart:
- Sección “Instalación rápida” condensando los pasos principales.
- Enlace explícito a `docs/quickstart.md` para detalles.
Métricas de aceptación:
- Una persona que no conoce el proyecto puede seguir `docs/quickstart.md` en una máquina limpia y terminar con:
- webhook recibiendo eventos,
- tareas visibles en /app,
- métricas en /metrics.
### D3: Uso (web + comandos)
Objetivo: documentar claramente la experiencia de uso, separando canales (WhatsApp vs web) y evitando duplicidad.
Cambios:
- Reorganizar la documentación de uso actual en dos archivos:
- `docs/usage/commands.md`:
- Basado en `docs/USER_GUIDE.md` y `docs/commands-inventory.md`.
- Contenido:
- prefijo de comandos y principios (DM vs grupo, fechas, rate limit),
- catálogo de comandos con alias, parámetros y ejemplos,
- notas sobre:
- interpretación de fechas (“hoy”, “mañana”, YYYY-MM-DD),
- menciones reales vs tokens `@número`,
- comportamiento en grupo vs DM,
- sección de administración (/admin…).
- `docs/usage/web.md`:
- Basado en `plan-diseno-web.md`, `plan-web-fases.md` y el estado actual de la UI.
- Contenido:
- estructura de `/app` (lista de tareas, grupos, preferencias),
- flujo típico de uso desde web (reclamar/soltar, editar, completar),
- relación entre lo que ves en WhatsApp y lo que ves en la web,
- vistas de calendario/ICS y cómo se consumen.
- Dejar `docs/USER_GUIDE.md` como entrada que redirige:
- Puede convertirse en un índice breve que apunte a `usage/commands.md` y `usage/web.md`, o bien moverse a `docs/legacy/` cuando la nueva estructura esté madura.
Métricas de aceptación:
- No hay duplicación significativa de contenido entre README, `USER_GUIDE`, `usage/commands` y `usage/web`.
- Los ejemplos de comandos están actualizados y probados manualmente (al menos los principales).
### D4: Operaciones y despliegue
Objetivo: facilitar la vida a quien opera Taskbot en producción (observabilidad, mantenimiento, CI/CD).
Cambios:
- Crear/actualizar `docs/operations.md`:
- Basado en `operations.md`, `metrics-plan.md`, `CI-CD-PLAN.md` y `docs/grafana/`.
- Contenido:
- **Despliegue**:
- opciones típicas (proceso único vs supervisado por systemd, contenedores, etc.),
- recomendaciones de recursos y almacenamiento,
- interacción con Evolution API (timeouts, health checks).
- **Migraciones y base de datos**:
- cómo y cuándo se ejecutan migraciones,
- política de WAL y PRAGMAs más importantes,
- estrategias de backup/restore de SQLite.
- **Observabilidad**:
- uso de `/health` (qué comprueba, señales de fallo),
- uso de `/metrics` (Prometheus/JSON, flags de configuración),
- referencia al dashboard de Grafana (`docs/grafana/taskbot-metrics.json`): qué gráficos hay y qué significan.
- **Schedulers y tareas periódicas**:
- sincronización de grupos,
- recordatorios,
- limpieza de cola de respuestas,
- cómo ajustar intervalos y qué implicaciones tienen.
- Conectar con variables de entorno:
- Enlazar a `docs/config/env.md` para cada opción relevante (p. ej. `GROUP_SYNC_INTERVAL_MS`, `RQ_*`, `METRICS_*`).
Métricas de aceptación:
- Operador externo puede entender:
- qué mirar en caso de problemas (health/metrics/logs),
- cómo hacer backup/restore sin corromper la DB,
- cómo cambiar la frecuencia de tareas periódicas con seguridad.
### D5: Arquitectura y DB
Objetivo: ofrecer una visión clara de cómo está organizado el sistema internamente, especialmente tras el refactor (servicios extraídos, DB locator, ICS central).
Cambios:
- Consolidar `docs/architecture.md`:
- Integrar/actualizar contenido de `architecture.md`, `overview.md`, `database.md` y este propio plan.
- Contenido:
- **Componentes principales**:
- servidor HTTP (WebhookServer y handlers /webhook, /health, /metrics),
- servicios de dominio (TaskService, ResponseQueue, GroupSync, Reminders, Admin…),
- cola de respuestas y cliente Evolution,
- app web SvelteKit.
- **DB Locator**:
- motivación,
- API básica (`getDb`/`setDb`/`withDb`),
- cómo se usa en servicios y tests.
- **Flujos clave**:
- mensaje entrante → comando → creación/actualización de tarea → encolado de respuesta/reacción,
- sincronización de grupos y membresías,
- recordatorios.
- **Base de datos**:
- tablas principales (tasks, task_assignments, groups, group_members, response_queue, user_preferences…),
- invariantes importantes,
- decisiones de diseño (WAL, migraciones up-only).
- Mantener `docs/database.md` como detalle de esquema:
- Puede centrarse en:
- descripción tabla a tabla,
- índices relevantes,
- ejemplos de consultas típicas,
- enlazado desde `docs/architecture.md`.
Métricas de aceptación:
- Contributors nuevos pueden entender dónde “colgar” código nuevo (por ejemplo, un nuevo servicio, una nueva tabla o un nuevo comando).
- Este plan de refactor técnico se percibe como coherente con la arquitectura documentada (no como un documento separado y divergente).
### D6: Contribución y ADRs
Objetivo: ayudar a futuras personas contribuidoras (incluido tú mismo “del futuro”) a cambiar el sistema sin romper las convenciones ni repetir debates ya resueltos.
Cambios:
- Crear/actualizar `docs/contributing.md`:
- Contenido:
- cómo montar entorno de desarrollo (dependencias, comandos básicos),
- cómo ejecutar:
- typecheck core (`bun run typecheck:core`),
- typecheck web (`bun run typecheck:web`),
- tests y cobertura (`bun test --coverage`),
- convenciones de código:
- UTC y formato de timestamps en SQLite,
- uso de helpers centralizados (datetime, crypto, helpers de test),
- tipos estrictos en TS (flags activados y expectativas),
- uso del DB locator.
- criterios para abrir PRs:
- tests verdes,
- sin cambios funcionales en lotes que se declaran “refactor internos”,
- cuándo y cómo añadir un ADR.
- referencia rápida a how-tos existentes:
- `how-to/adding-command.md`,
- `how-to/adding-migration.md`,
- `how-to/adjusting-group-sync.md`,
- `how-to/adding-env.md`.
- Consolidar ADRs:
- Mantener `docs/adr/*.md` como registro de decisiones de arquitectura.
- Añadir ADRs nuevos cuando:
- se tomen decisiones de largo plazo (p. ej., i18n, políticas de rate limiting, cambios de almacenamiento).
- Desde `docs/contributing.md`, explicar:
- cuándo merece la pena un ADR,
- cómo estructurarlo (título, contexto, decisión, consecuencias).
Métricas de aceptación:
- Contributors pueden:
- saber qué comandos ejecutar antes de abrir PR,
- entender las decisiones ya tomadas (UTC, DB locator, ICS, estructura de servicios),
- no tener que “descubrirlas” leyendo código o este plan.
### Orden recomendado de ejecución para Fase D
1. Ejecutar **D0** (reset estructural):
- nuevo README raíz orientado a adopción,
- índice v2 en `docs/README.md`,
- movimiento de documentos antiguos a `docs/legacy/`.
2. Ejecutar **D1** (env) y **D2** (quickstart) en paralelo:
- cerrar la historia de “cómo configuro esto” y “cómo lo echo a andar”.
3. Completar **D3** (uso commands/web) apoyándose en la guía de usuario actual, reorganizando sin perder contenido útil.
4. Redondear con **D4** (operaciones) y **D5** (arquitectura/DB) usando este plan de refactor como referencia.
5. Cerrar con **D6** (contribución+ADRs), conectando:
- decisiones técnicas ya tomadas,
- how-tos existentes,
- y el pipeline de typecheck/tests.
Esta Fase D se coordina con los lotes técnicos previo/posteriores:
- depende de que el estado del core/web esté relativamente estable (Lotes 06),
- y sirve como preparación explícita antes de acometer cambios más invasivos (i18n, nuevos comandos, cambios de esquema), reduciendo el riesgo de deuda técnica futura.
## Fase I — Internacionalización (nueva) ## Fase I — Internacionalización (nueva)

@ -22,7 +22,7 @@ function buildForwardHeaders(req: Request): Headers {
Bun.serve({ Bun.serve({
port: Number(process.env.PORT || process.env.ROUTER_PORT || 3000), port: Number(process.env.PORT || process.env.ROUTER_PORT || 3000),
fetch: async (req) => { fetch: async (req: Request) => {
const url = new URL(req.url); const url = new URL(req.url);
// Health local para el contenedor (evita 404 en healthcheck) // Health local para el contenedor (evita 404 en healthcheck)
@ -36,12 +36,14 @@ Bun.serve({
const headers = buildForwardHeaders(req); const headers = buildForwardHeaders(req);
if (!routeToBot) { if (!routeToBot) {
try { headers.set('accept-encoding', 'identity'); } catch {} try {
headers.set('accept-encoding', 'identity');
} catch {}
} }
const init: RequestInit = { const init: RequestInit = {
method: req.method, method: req.method,
headers, headers,
redirect: 'manual', redirect: 'manual'
}; };
if (req.method !== 'GET' && req.method !== 'HEAD' && req.body !== null) { if (req.method !== 'GET' && req.method !== 'HEAD' && req.body !== null) {
(init as any).body = req.body as any; (init as any).body = req.body as any;
@ -52,7 +54,11 @@ Bun.serve({
const res = await fetch(targetUrl, init); const res = await fetch(targetUrl, init);
const ms = Date.now() - started; const ms = Date.now() - started;
try { try {
console.log(`[proxy] ${req.method} ${url.pathname}${url.search} -> ${routeToBot ? 'bot' : 'web'} ${res.status} (${ms}ms)`); console.log(
`[proxy] ${req.method} ${url.pathname}${url.search} -> ${
routeToBot ? 'bot' : 'web'
} ${res.status} (${ms}ms)`
);
} catch {} } catch {}
// Devuelve la respuesta (incluye Set-Cookie, Location, etc.), asegurando Content-Type en assets por si faltase // Devuelve la respuesta (incluye Set-Cookie, Location, etc.), asegurando Content-Type en assets por si faltase
const passthroughHeaders = new Headers(res.headers); const passthroughHeaders = new Headers(res.headers);
@ -71,14 +77,23 @@ Bun.serve({
} catch {} } catch {}
} }
if (!passthroughHeaders.get('content-type')) { if (!passthroughHeaders.get('content-type')) {
if (url.pathname.endsWith('.js')) passthroughHeaders.set('content-type', 'application/javascript; charset=utf-8'); if (url.pathname.endsWith('.js')) {
if (url.pathname.endsWith('.css')) passthroughHeaders.set('content-type', 'text/css; charset=utf-8'); passthroughHeaders.set(
'content-type',
'application/javascript; charset=utf-8'
);
}
if (url.pathname.endsWith('.css')) {
passthroughHeaders.set('content-type', 'text/css; charset=utf-8');
}
} }
return new Response(res.body, { status: res.status, headers: passthroughHeaders }); return new Response(res.body, { status: res.status, headers: passthroughHeaders });
} catch (err) { } catch (err) {
const msg = err instanceof Error ? err.message : String(err); const msg = err instanceof Error ? err.message : String(err);
console.error(`[proxy] ${req.method} ${url.pathname}${url.search} -> ERROR: ${msg}`); console.error(
`[proxy] ${req.method} ${url.pathname}${url.search} -> ERROR: ${msg}`
);
return new Response(`Proxy error: ${msg}\n`, { status: 502 }); return new Response(`Proxy error: ${msg}\n`, { status: 502 });
} }
}, }
}); });

@ -1,4 +1,4 @@
//// <reference types="bun-types" /> /// <reference types="bun-types" />
import type { Database } from 'bun:sqlite'; import type { Database } from 'bun:sqlite';
import { GroupSyncService } from './services/group-sync'; import { GroupSyncService } from './services/group-sync';
import { ContactsService } from './services/contacts'; import { ContactsService } from './services/contacts';
@ -11,11 +11,6 @@ import { handleHealthRequest } from './http/health';
import { startServices } from './http/bootstrap'; import { startServices } from './http/bootstrap';
import { handleMessageUpsert as handleMessageUpsertFn } from './http/webhook-handler'; import { handleMessageUpsert as handleMessageUpsertFn } from './http/webhook-handler';
// Bun is available globally when running under Bun runtime
declare global {
var Bun: typeof import('bun');
}
export const REQUIRED_ENV = [ export const REQUIRED_ENV = [
'EVOLUTION_API_URL', 'EVOLUTION_API_URL',
'EVOLUTION_API_KEY', 'EVOLUTION_API_KEY',
@ -36,7 +31,8 @@ export class WebhookServer {
private static getBaseUrl(request: Request): string { private static getBaseUrl(request: Request): string {
const proto = request.headers.get('x-forwarded-proto') || 'http'; const proto = request.headers.get('x-forwarded-proto') || 'http';
const host = request.headers.get('x-forwarded-host') || request.headers.get('host'); const host =
request.headers.get('x-forwarded-host') || request.headers.get('host');
return `${proto}://${host}`; return `${proto}://${host}`;
} }
@ -62,7 +58,7 @@ export class WebhookServer {
} }
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
console.log(' Incoming webhook request:') console.log(' Incoming webhook request:');
} }
// 1. Method validation // 1. Method validation
@ -78,15 +74,17 @@ export class WebhookServer {
try { try {
// 3. Parse and validate payload // 3. Parse and validate payload
const payload = await request.json() as WebhookPayload; const payload = (await request.json()) as WebhookPayload;
if (!payload.event || !payload.instance) { if (!payload.event || !payload.instance) {
return new Response('🚫 Invalid payload', { status: 400 }); return new Response('🚫 Invalid payload', { status: 400 });
} }
// 4. Verify instance matches (skip in test environment unless TEST_VERIFY_INSTANCE is set) // 4. Verify instance matches (skip in test environment unless TEST_VERIFY_INSTANCE is set)
if ((process.env.NODE_ENV !== 'test' || process.env.TEST_VERIFY_INSTANCE) && if (
payload.instance !== process.env.EVOLUTION_API_INSTANCE) { (process.env.NODE_ENV !== 'test' || process.env.TEST_VERIFY_INSTANCE) &&
payload.instance !== process.env.EVOLUTION_API_INSTANCE
) {
return new Response('🚫 Invalid instance', { status: 403 }); return new Response('🚫 Invalid instance', { status: 403 });
} }
@ -120,7 +118,9 @@ export class WebhookServer {
case 'contacts.update': case 'contacts.update':
case 'chats.update': case 'chats.update':
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {
console.log(' Handling contacts/chats update event:', { rawEvent: evt }); console.log(' Handling contacts/chats update event:', {
rawEvent: evt
});
} }
ContactsService.updateFromWebhook(payload.data); ContactsService.updateFromWebhook(payload.data);
break; break;
@ -151,7 +151,9 @@ export class WebhookServer {
stack: error instanceof Error ? error.stack : undefined, stack: error instanceof Error ? error.stack : undefined,
time: new Date().toISOString() time: new Date().toISOString()
}); });
try { Metrics.inc('webhook_errors_total'); } catch {} try {
Metrics.inc('webhook_errors_total');
} catch {}
return new Response('Invalid request', { status: 400 }); return new Response('Invalid request', { status: 400 });
} }
} }
@ -162,10 +164,26 @@ export class WebhookServer {
static validateEnv() { static validateEnv() {
console.log(' Checking environment variables...'); console.log(' Checking environment variables...');
console.log('EVOLUTION_API_URL:', process.env.EVOLUTION_API_URL ? '***' : 'MISSING'); console.log(
console.log('EVOLUTION_API_INSTANCE:', process.env.EVOLUTION_API_INSTANCE || 'MISSING'); 'EVOLUTION_API_URL:',
console.log('WEBHOOK_URL:', process.env.WEBHOOK_URL ? `${process.env.WEBHOOK_URL.substring(0, 20)}...` : 'NOT SET'); process.env.EVOLUTION_API_URL ? '***' : 'MISSING'
console.log('WHATSAPP_COMMUNITY_ID:', process.env.WHATSAPP_COMMUNITY_ID ? '***' : 'NOT SET (se mostrarán comunidades disponibles)'); );
console.log(
'EVOLUTION_API_INSTANCE:',
process.env.EVOLUTION_API_INSTANCE || 'MISSING'
);
console.log(
'WEBHOOK_URL:',
process.env.WEBHOOK_URL
? `${process.env.WEBHOOK_URL.substring(0, 20)}...`
: 'NOT SET'
);
console.log(
'WHATSAPP_COMMUNITY_ID:',
process.env.WHATSAPP_COMMUNITY_ID
? '***'
: 'NOT SET (se mostrarán comunidades disponibles)'
);
const missing = REQUIRED_ENV.filter(v => !process.env[v]); const missing = REQUIRED_ENV.filter(v => !process.env[v]);
if (missing.length) { if (missing.length) {
@ -175,8 +193,10 @@ export class WebhookServer {
process.exit(1); process.exit(1);
} }
if (process.env.CHATBOT_PHONE_NUMBER && if (
!/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)) { process.env.CHATBOT_PHONE_NUMBER &&
!/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)
) {
console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits'); console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits');
process.exit(1); process.exit(1);
} }
@ -189,7 +209,9 @@ export class WebhookServer {
await Migrator.migrateToLatest(this.dbInstance); await Migrator.migrateToLatest(this.dbInstance);
// Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort) // Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort)
try { AllowedGroups.seedFromEnv(); } catch {} try {
AllowedGroups.seedFromEnv();
} catch {}
const PORT = process.env.PORT || '3007'; const PORT = process.env.PORT || '3007';
console.log('✅ Environment variables validated'); console.log('✅ Environment variables validated');
@ -211,14 +233,17 @@ export class WebhookServer {
try { try {
await startServices(this.dbInstance); await startServices(this.dbInstance);
} catch (error) { } catch (error) {
console.error('❌ Failed to setup webhook:', error instanceof Error ? error.message : error); console.error(
'❌ Failed to setup webhook:',
error instanceof Error ? error.message : error
);
process.exit(1); process.exit(1);
} }
} }
const server = Bun.serve({ const server = Bun.serve({
port: parseInt(PORT), port: parseInt(PORT),
fetch: (request) => WebhookServer.handleRequest(request) fetch: (request: Request) => WebhookServer.handleRequest(request)
}); });
console.log(`Server running on port ${PORT}`); console.log(`Server running on port ${PORT}`);
return server; return server;

@ -8,6 +8,8 @@ declare global {
toJSON?: any; toJSON?: any;
count?: any; count?: any;
getAll?: any; getAll?: any;
// En Bun y en el estándar, entries existe siempre; no la marcamos como opcional
entries(): any;
} }
// Añadir timeout soportado por Bun.fetch en algunos usos y permitir httpVersion usado en algunos fetch // Añadir timeout soportado por Bun.fetch en algunos usos y permitir httpVersion usado en algunos fetch

@ -2,7 +2,7 @@
"extends": "./tsconfig.json", "extends": "./tsconfig.json",
"compilerOptions": { "compilerOptions": {
"types": ["bun-types"], "types": ["bun-types"],
"lib": ["esnext"], "lib": ["esnext", "dom"],
"strict": false, "strict": false,
"strictNullChecks": true, "strictNullChecks": true,
"noImplicitAny": true, "noImplicitAny": true,

Loading…
Cancel
Save