From b0397d24d0e16d567d6ad2060eaa78cd4bb54002 Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 24 Nov 2025 09:26:42 +0100 Subject: [PATCH 1/5] fix: ajustar tipos Bun y Request para limpiar typecheck core Co-authored-by: aider (openrouter/openai/gpt-5.1) --- proxy.ts | 31 +++- src/server.ts | 437 ++++++++++++++++++++++++--------------------- tsconfig.core.json | 2 +- 3 files changed, 255 insertions(+), 215 deletions(-) diff --git a/proxy.ts b/proxy.ts index 48d33a2..c477246 100644 --- a/proxy.ts +++ b/proxy.ts @@ -22,7 +22,7 @@ function buildForwardHeaders(req: Request): Headers { Bun.serve({ port: Number(process.env.PORT || process.env.ROUTER_PORT || 3000), - fetch: async (req) => { + fetch: async (req: Request) => { const url = new URL(req.url); // Health local para el contenedor (evita 404 en healthcheck) @@ -36,12 +36,14 @@ Bun.serve({ const headers = buildForwardHeaders(req); if (!routeToBot) { - try { headers.set('accept-encoding', 'identity'); } catch {} + try { + headers.set('accept-encoding', 'identity'); + } catch {} } const init: RequestInit = { method: req.method, headers, - redirect: 'manual', + redirect: 'manual' }; if (req.method !== 'GET' && req.method !== 'HEAD' && req.body !== null) { (init as any).body = req.body as any; @@ -52,7 +54,11 @@ Bun.serve({ const res = await fetch(targetUrl, init); const ms = Date.now() - started; 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 {} // Devuelve la respuesta (incluye Set-Cookie, Location, etc.), asegurando Content-Type en assets por si faltase const passthroughHeaders = new Headers(res.headers); @@ -71,14 +77,23 @@ Bun.serve({ } catch {} } if (!passthroughHeaders.get('content-type')) { - if (url.pathname.endsWith('.js')) passthroughHeaders.set('content-type', 'application/javascript; charset=utf-8'); - if (url.pathname.endsWith('.css')) passthroughHeaders.set('content-type', 'text/css; charset=utf-8'); + if (url.pathname.endsWith('.js')) { + 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 }); } catch (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 }); } - }, + } }); diff --git a/src/server.ts b/src/server.ts index 32b821b..e6b11e3 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -//// +/// import type { Database } from 'bun:sqlite'; import { GroupSyncService } from './services/group-sync'; import { ContactsService } from './services/contacts'; @@ -11,216 +11,241 @@ import { handleHealthRequest } from './http/health'; import { startServices } from './http/bootstrap'; 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 = [ - 'EVOLUTION_API_URL', - 'EVOLUTION_API_KEY', - 'EVOLUTION_API_INSTANCE', - 'CHATBOT_PHONE_NUMBER', - 'WEBHOOK_URL' + 'EVOLUTION_API_URL', + 'EVOLUTION_API_KEY', + 'EVOLUTION_API_INSTANCE', + 'CHATBOT_PHONE_NUMBER', + 'WEBHOOK_URL' ]; type WebhookPayload = { - event: string; - instance: string; - data: any; - // Other fields from Evolution API + event: string; + instance: string; + data: any; + // Other fields from Evolution API }; export class WebhookServer { - static dbInstance: Database = db; - - private static getBaseUrl(request: Request): string { - const proto = request.headers.get('x-forwarded-proto') || 'http'; - const host = request.headers.get('x-forwarded-host') || request.headers.get('host'); - return `${proto}://${host}`; - } - - private static getMessageText(message: any): string { - if (!message || typeof message !== 'object') return ''; - const text = - message.conversation || - message?.extendedTextMessage?.text || - message?.imageMessage?.caption || - message?.videoMessage?.caption || - ''; - return typeof text === 'string' ? text.trim() : ''; - } - - static async handleRequest(request: Request): Promise { - // Health check endpoint y métricas - const url = new URL(request.url); - if (url.pathname.endsWith('/metrics')) { - return await handleMetricsRequest(request, WebhookServer.dbInstance); - } - if (url.pathname.endsWith('/health')) { - return await handleHealthRequest(url, WebhookServer.dbInstance); - } - - if (process.env.NODE_ENV !== 'test') { - console.log('ℹ️ Incoming webhook request:') - } - - // 1. Method validation - if (request.method !== 'POST') { - return new Response('🚫 Method not allowed', { status: 405 }); - } - - // 2. Content-Type validation - const contentType = request.headers.get('content-type'); - if (!contentType?.includes('application/json')) { - return new Response('🚫 Invalid content type', { status: 400 }); - } - - try { - // 3. Parse and validate payload - const payload = await request.json() as WebhookPayload; - - if (!payload.event || !payload.instance) { - return new Response('🚫 Invalid payload', { status: 400 }); - } - - // 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) && - payload.instance !== process.env.EVOLUTION_API_INSTANCE) { - return new Response('🚫 Invalid instance', { status: 403 }); - } - - // 5. Route events - // console.log('ℹ️ Webhook event received:', { - // event: payload.event, - // instance: payload.instance, - // data: payload.data ? '[...]' : null - // }); - - // Normalize event name to handle different casing/format (e.g., MESSAGES_UPSERT) - const evt = String(payload.event); - const evtNorm = evt.toLowerCase().replace(/_/g, '.'); - - // Contabilizar evento de webhook por tipo - try { - Metrics.inc(`webhook_events_total_${evtNorm.replace(/\./g, '_')}`); - } catch {} - - switch (evtNorm) { - case 'messages.upsert': - if (process.env.NODE_ENV !== 'test') { - console.log('ℹ️ Handling message upsert:', { - groupId: payload.data?.key?.remoteJid, - message: payload.data?.message?.conversation, - rawEvent: evt - }); - } - await WebhookServer.handleMessageUpsert(payload.data); - break; - case 'contacts.update': - case 'chats.update': - if (process.env.NODE_ENV !== 'test') { - console.log('ℹ️ Handling contacts/chats update event:', { rawEvent: evt }); - } - ContactsService.updateFromWebhook(payload.data); - break; - case 'groups.upsert': - if (process.env.NODE_ENV !== 'test') { - console.log('ℹ️ Handling groups upsert event:', { rawEvent: evt }); - } - try { - const res = await GroupSyncService.syncGroups(); - GroupSyncService.refreshActiveGroupsCache(); - const changed = GroupSyncService.getLastChangedActive(); - if (changed.length > 0) { - await GroupSyncService.syncMembersForGroups(changed); - } else { - await GroupSyncService.syncMembersForActiveGroups(); - } - } catch (e) { - console.error('❌ Error handling groups.upsert:', e); - } - break; - // Other events will be added later - } - - return new Response('OK', { status: 200 }); - } catch (error) { - console.error('❌ Error processing webhook:', { - error: error instanceof Error ? error.message : String(error), - stack: error instanceof Error ? error.stack : undefined, - time: new Date().toISOString() - }); - try { Metrics.inc('webhook_errors_total'); } catch {} - return new Response('Invalid request', { status: 400 }); - } - } - - static async handleMessageUpsert(data: any) { - return await handleMessageUpsertFn(data, WebhookServer.dbInstance); - } - - static validateEnv() { - console.log('ℹ️ Checking environment variables...'); - console.log('EVOLUTION_API_URL:', process.env.EVOLUTION_API_URL ? '***' : 'MISSING'); - 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]); - if (missing.length) { - console.error('❌ Missing required environment variables:'); - missing.forEach(v => console.error(`- ${v}`)); - console.error('Add these to your CapRover environment configuration'); - process.exit(1); - } - - if (process.env.CHATBOT_PHONE_NUMBER && - !/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER)) { - console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits'); - process.exit(1); - } - } - - static async start() { - this.validateEnv(); - - // Run database migrations (up-only) before starting services - await Migrator.migrateToLatest(this.dbInstance); - - // Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort) - try { AllowedGroups.seedFromEnv(); } catch {} - - const PORT = process.env.PORT || '3007'; - console.log('✅ Environment variables validated'); - // A0: pre-crear contadores para que aparezcan en /metrics - try { - Metrics.inc('onboarding_prompts_sent_total', 0); - Metrics.inc('onboarding_prompts_skipped_total', 0); - Metrics.inc('onboarding_assign_failures_total', 0); - - // Precalentar métricas de reacciones por emoji - for (const emoji of ['robot', 'warn', 'check', 'other']) { - Metrics.inc('reactions_enqueued_total', 0, { emoji }); - Metrics.inc('reactions_sent_total', 0, { emoji }); - Metrics.inc('reactions_failed_total', 0, { emoji }); - } - } catch {} - - if (process.env.NODE_ENV !== 'test') { - try { - await startServices(this.dbInstance); - } catch (error) { - console.error('❌ Failed to setup webhook:', error instanceof Error ? error.message : error); - process.exit(1); - } - } - - const server = Bun.serve({ - port: parseInt(PORT), - fetch: (request) => WebhookServer.handleRequest(request) - }); - console.log(`Server running on port ${PORT}`); - return server; - } + static dbInstance: Database = db; + + private static getBaseUrl(request: Request): string { + const proto = request.headers.get('x-forwarded-proto') || 'http'; + const host = + request.headers.get('x-forwarded-host') || request.headers.get('host'); + return `${proto}://${host}`; + } + + private static getMessageText(message: any): string { + if (!message || typeof message !== 'object') return ''; + const text = + message.conversation || + message?.extendedTextMessage?.text || + message?.imageMessage?.caption || + message?.videoMessage?.caption || + ''; + return typeof text === 'string' ? text.trim() : ''; + } + + static async handleRequest(request: Request): Promise { + // Health check endpoint y métricas + const url = new URL(request.url); + if (url.pathname.endsWith('/metrics')) { + return await handleMetricsRequest(request, WebhookServer.dbInstance); + } + if (url.pathname.endsWith('/health')) { + return await handleHealthRequest(url, WebhookServer.dbInstance); + } + + if (process.env.NODE_ENV !== 'test') { + console.log('ℹ️ Incoming webhook request:'); + } + + // 1. Method validation + if (request.method !== 'POST') { + return new Response('🚫 Method not allowed', { status: 405 }); + } + + // 2. Content-Type validation + const contentType = request.headers.get('content-type'); + if (!contentType?.includes('application/json')) { + return new Response('🚫 Invalid content type', { status: 400 }); + } + + try { + // 3. Parse and validate payload + const payload = (await request.json()) as WebhookPayload; + + if (!payload.event || !payload.instance) { + return new Response('🚫 Invalid payload', { status: 400 }); + } + + // 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) && + payload.instance !== process.env.EVOLUTION_API_INSTANCE + ) { + return new Response('🚫 Invalid instance', { status: 403 }); + } + + // 5. Route events + // console.log('ℹ️ Webhook event received:', { + // event: payload.event, + // instance: payload.instance, + // data: payload.data ? '[...]' : null + // }); + + // Normalize event name to handle different casing/format (e.g., MESSAGES_UPSERT) + const evt = String(payload.event); + const evtNorm = evt.toLowerCase().replace(/_/g, '.'); + + // Contabilizar evento de webhook por tipo + try { + Metrics.inc(`webhook_events_total_${evtNorm.replace(/\./g, '_')}`); + } catch {} + + switch (evtNorm) { + case 'messages.upsert': + if (process.env.NODE_ENV !== 'test') { + console.log('ℹ️ Handling message upsert:', { + groupId: payload.data?.key?.remoteJid, + message: payload.data?.message?.conversation, + rawEvent: evt + }); + } + await WebhookServer.handleMessageUpsert(payload.data); + break; + case 'contacts.update': + case 'chats.update': + if (process.env.NODE_ENV !== 'test') { + console.log('ℹ️ Handling contacts/chats update event:', { + rawEvent: evt + }); + } + ContactsService.updateFromWebhook(payload.data); + break; + case 'groups.upsert': + if (process.env.NODE_ENV !== 'test') { + console.log('ℹ️ Handling groups upsert event:', { rawEvent: evt }); + } + try { + const res = await GroupSyncService.syncGroups(); + GroupSyncService.refreshActiveGroupsCache(); + const changed = GroupSyncService.getLastChangedActive(); + if (changed.length > 0) { + await GroupSyncService.syncMembersForGroups(changed); + } else { + await GroupSyncService.syncMembersForActiveGroups(); + } + } catch (e) { + console.error('❌ Error handling groups.upsert:', e); + } + break; + // Other events will be added later + } + + return new Response('OK', { status: 200 }); + } catch (error) { + console.error('❌ Error processing webhook:', { + error: error instanceof Error ? error.message : String(error), + stack: error instanceof Error ? error.stack : undefined, + time: new Date().toISOString() + }); + try { + Metrics.inc('webhook_errors_total'); + } catch {} + return new Response('Invalid request', { status: 400 }); + } + } + + static async handleMessageUpsert(data: any) { + return await handleMessageUpsertFn(data, WebhookServer.dbInstance); + } + + static validateEnv() { + console.log('ℹ️ Checking environment variables...'); + console.log( + 'EVOLUTION_API_URL:', + process.env.EVOLUTION_API_URL ? '***' : 'MISSING' + ); + 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]); + if (missing.length) { + console.error('❌ Missing required environment variables:'); + missing.forEach(v => console.error(`- ${v}`)); + console.error('Add these to your CapRover environment configuration'); + process.exit(1); + } + + if ( + process.env.CHATBOT_PHONE_NUMBER && + !/^\d+$/.test(process.env.CHATBOT_PHONE_NUMBER) + ) { + console.error('❌ CHATBOT_PHONE_NUMBER must contain only digits'); + process.exit(1); + } + } + + static async start() { + this.validateEnv(); + + // Run database migrations (up-only) before starting services + await Migrator.migrateToLatest(this.dbInstance); + + // Etapa 7: seed inicial de grupos permitidos desde ALLOWED_GROUPS (best-effort) + try { + AllowedGroups.seedFromEnv(); + } catch {} + + const PORT = process.env.PORT || '3007'; + console.log('✅ Environment variables validated'); + // A0: pre-crear contadores para que aparezcan en /metrics + try { + Metrics.inc('onboarding_prompts_sent_total', 0); + Metrics.inc('onboarding_prompts_skipped_total', 0); + Metrics.inc('onboarding_assign_failures_total', 0); + + // Precalentar métricas de reacciones por emoji + for (const emoji of ['robot', 'warn', 'check', 'other']) { + Metrics.inc('reactions_enqueued_total', 0, { emoji }); + Metrics.inc('reactions_sent_total', 0, { emoji }); + Metrics.inc('reactions_failed_total', 0, { emoji }); + } + } catch {} + + if (process.env.NODE_ENV !== 'test') { + try { + await startServices(this.dbInstance); + } catch (error) { + console.error( + '❌ Failed to setup webhook:', + error instanceof Error ? error.message : error + ); + process.exit(1); + } + } + + const server = Bun.serve({ + port: parseInt(PORT), + fetch: (request: Request) => WebhookServer.handleRequest(request) + }); + console.log(`Server running on port ${PORT}`); + return server; + } } diff --git a/tsconfig.core.json b/tsconfig.core.json index fff44bd..1fb9268 100644 --- a/tsconfig.core.json +++ b/tsconfig.core.json @@ -2,7 +2,7 @@ "extends": "./tsconfig.json", "compilerOptions": { "types": ["bun-types"], - "lib": ["esnext"], + "lib": ["esnext", "dom"], "strict": false, "strictNullChecks": true, "noImplicitAny": true, From 95d095127051d9403c5d5382a5926703a3cc5945 Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 24 Nov 2025 09:29:00 +0100 Subject: [PATCH 2/5] fix: declarar entries en Headers en shim de tipos Co-authored-by: aider (openrouter/openai/gpt-5.1) --- src/types/shims.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts index 6fd80db..5f488ce 100644 --- a/src/types/shims.d.ts +++ b/src/types/shims.d.ts @@ -8,6 +8,7 @@ declare global { toJSON?: any; count?: any; getAll?: any; + entries?(): any; } // Añadir timeout soportado por Bun.fetch en algunos usos y permitir httpVersion usado en algunos fetch From 5e504e4c67d7707d5b7ca59a3339ed2ccb120f71 Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 24 Nov 2025 09:34:44 +0100 Subject: [PATCH 3/5] =?UTF-8?q?fix:=20asegurar=20que=20Headers.entries=20s?= =?UTF-8?q?iempre=20est=C3=A1=20disponible?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5.1) --- src/types/shims.d.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/shims.d.ts b/src/types/shims.d.ts index 5f488ce..8446a9d 100644 --- a/src/types/shims.d.ts +++ b/src/types/shims.d.ts @@ -8,7 +8,8 @@ declare global { toJSON?: any; count?: any; getAll?: any; - entries?(): 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 From 2875c8eefb49dc709389d0c33aa61ad1fb7d6d4a Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 24 Nov 2025 11:09:53 +0100 Subject: [PATCH 4/5] =?UTF-8?q?docs:=20detallar=20fase=20D=20de=20document?= =?UTF-8?q?aci=C3=B3n=20en=20el=20plan=20de=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/openai/gpt-5.1) --- docs/2025-11-01-plan-refactor-tecnico.md | 324 +++++++++++++++++++++-- 1 file changed, 295 insertions(+), 29 deletions(-) diff --git a/docs/2025-11-01-plan-refactor-tecnico.md b/docs/2025-11-01-plan-refactor-tecnico.md index 8073b28..ef74fad 100644 --- a/docs/2025-11-01-plan-refactor-tecnico.md +++ b/docs/2025-11-01-plan-refactor-tecnico.md @@ -438,35 +438,301 @@ Con estos 3 archivos podremos definir cambios mínimos para tener un typecheck l ## Fase D — Documentación (nueva) -- Principios: - - Markdown en /docs como fuente de verdad; sin dependencias nuevas; build opcional más adelante. - - 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. - - Í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. -- PR D1: Referencia de variables de entorno - - Tabla completa: nombre, descripción, tipo, por defecto, requerido/optativo, ámbito (core/web), sensibilidad, recarga dinámica. - - Generar .env.example coherente con la tabla. - - Aceptación: 100% de variables de entorno documentadas. -- PR D2: Quickstart e instalación - - Pasos desde cero a “todo funcionando” (core + web). - - Matriz de comandos: typecheck core/web, dev, build, test, coverage. - - Aceptación: reproducible en máquina limpia. -- PR D3: Uso (web + comandos) - - Catálogo de comandos (nueva, tomar, soltar, ver, web…): expectativas y ejemplos. - - Guía de la interfaz web (navegación, calendarios ICS, preferencias). - - Aceptación: ejemplos probados manualmente. -- PR D4: Operaciones y despliegue - - Endpoints /health y /metrics, migraciones, copias de seguridad, rotación de claves, runbooks de incidencias. - - Aceptación: runbooks accionables. -- PR D5: Arquitectura y DB - - Visión por capas (core, servicios, web), flujos (webhook → colas → tareas), esquema SQLite principal. - - Aceptación: suficiente para nuevos contribuidores. -- PR D6: Contribución y ADRs - - Cómo proponer cambios, estándares TS/UTC/ICS, política de versiones. - - ADRs para decisiones clave (UTC, DB locator, ICS all-day). - - Aceptación: plantilla de PR y guía de contribución listas. +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. +- 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). + +### D0: Reset estructural y guía de estilo + +Objetivo: redefinir la estructura de documentación y marcar qué es canónico vs histórico, sin perder información. + +Cambios: + +- **README raíz (del repo)**: + - Reescribirlo con foco en: + - Tagline y pitch corto (qué resuelve, para quién). + - Casos de uso y “no es para ti si…”. + - Requisitos para desplegarlo (Evolution API, hosting, etc.). + - Vista de alto nivel de cómo funciona. + - Enlace claro a `docs/` (Quickstart, Guía de uso, Arquitectura, etc.). + +- **Definir documentos canónicos en `docs/`** (columna vertebral “v2”): + - `docs/README.md` — índice general y guía de estilo. + - `docs/overview.md` — visión general y flujos clave (evaluación). + - `docs/quickstart.md` — de cero a bot funcionando (dev/prod). + - `docs/config/env.md` — referencia única de variables de entorno. + - `docs/usage/commands.md` — uso por WhatsApp (comandos). + - `docs/usage/web.md` — uso de la interfaz web. + - `docs/architecture.md` — arquitectura lógica y DB locator. + - `docs/operations.md` — operación, métricas, backups, health/metrics. + - `docs/contributing.md` — cómo tocar el código sin romper convenciones. + +- **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 0–6), +- 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) From 93db6a9adbcbcb9dce5f3ea4b54db77434c8e2a3 Mon Sep 17 00:00:00 2001 From: borja Date: Mon, 24 Nov 2025 13:36:16 +0100 Subject: [PATCH 5/5] =?UTF-8?q?a=C3=B1ade=20el=20script=20bun=20run=20test?= =?UTF-8?q?=20para=20ejecutar=20cada=20test=20por=20separado=20de=20forma?= =?UTF-8?q?=20que=20si=20alguno=20falla=20breakea=20pero=20sin=20que=20se?= =?UTF-8?q?=20solapen=20unos=20con=20otros?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/package.json b/package.json index 167ffe3..8c4458d 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,18 @@ { - "name": "task-whatsapp", - "module": "index.ts", - "type": "module", - "private": true, - "scripts": { - "typecheck:web": "cd apps/web && bunx svelte-kit sync && bunx tsc --noEmit --pretty false", - "typecheck:core": "bunx tsc -p tsconfig.core.json --noEmit --pretty false" - }, - "devDependencies": { - "@types/bun": "latest", - "bun-types": "^1.3.3" - }, - "peerDependencies": { - "typescript": "^5.8.2" - } + "name": "task-whatsapp", + "module": "index.ts", + "type": "module", + "private": true, + "scripts": { + "typecheck:web": "cd apps/web && bunx svelte-kit sync && bunx tsc --noEmit --pretty false", + "typecheck:core": "bunx tsc -p tsconfig.core.json --noEmit --pretty false", + "test": "for i in $(fd . |rg test.ts); do bun test $i || break ; done" + }, + "devDependencies": { + "@types/bun": "latest", + "bun-types": "^1.3.3" + }, + "peerDependencies": { + "typescript": "^5.8.2" + } }