diff --git a/docs/ANALYTICS_DASHBOARD_SPEC.md b/docs/ANALYTICS_DASHBOARD_SPEC.md new file mode 100644 index 00000000..178b17be --- /dev/null +++ b/docs/ANALYTICS_DASHBOARD_SPEC.md @@ -0,0 +1,1240 @@ +# Especificación: Dashboard Web de Estadísticas en Tiempo Real — lnp2pBot + +> **Documento de diseño e implementación** +> Versión: 1.0 · Estado: Propuesta · Audiencia: equipo de desarrollo +> +> Este documento es una guía de implementación completa para construir un sistema +> web que se conecta a la base de datos de lnp2pBot (MongoDB) y muestra +> estadísticas operativas y financieras en tiempo real: monedas/fiat con más +> operaciones exitosas, métricas diarias/mensuales/por rango, filtros por tipo y +> estado de orden, flujo de sats entrante y saliente del nodo Lightning, y +> conciliación de ganancias del bot y de las comunidades. + +--- + +## Tabla de contenidos + +1. [Objetivos y alcance](#1-objetivos-y-alcance) +2. [Contexto del dominio (cómo funciona el bot)](#2-contexto-del-dominio) +3. [Modelo de datos relevante](#3-modelo-de-datos-relevante) +4. [Definiciones canónicas de métricas](#4-definiciones-canónicas-de-métricas) +5. [Arquitectura del sistema](#5-arquitectura-del-sistema) +6. [Diseño de la API (REST + WebSocket)](#6-diseño-de-la-api) +7. [Catálogo de consultas de agregación (MongoDB)](#7-catálogo-de-consultas-de-agregación) +8. [Conciliación de ganancias (módulo financiero)](#8-conciliación-de-ganancias) +9. [Flujo de fondos del nodo Lightning](#9-flujo-de-fondos-del-nodo-lightning) +10. [Tiempo real: estrategia](#10-tiempo-real-estrategia) +11. [Frontend: vistas, filtros y componentes](#11-frontend-vistas-filtros-y-componentes) +12. [Seguridad, autenticación y privacidad](#12-seguridad-autenticación-y-privacidad) +13. [Rendimiento, índices y caché](#13-rendimiento-índices-y-caché) +14. [Despliegue y configuración](#14-despliegue-y-configuración) +15. [Plan de implementación por fases](#15-plan-de-implementación-por-fases) +16. [Pruebas](#16-pruebas) +17. [Anexos](#17-anexos) + +--- + +## 1. Objetivos y alcance + +### 1.1 Objetivo + +Construir una aplicación web independiente ("Analytics Dashboard") que se conecte +**en modo solo-lectura** a la base de datos MongoDB de lnp2pBot y exponga: + +- **Estadísticas en tiempo real** que se actualizan sin recargar (vía WebSocket / + SSE alimentado por MongoDB Change Streams). +- **Rankings de monedas (fiat)** con más operaciones exitosas (diario, mensual y + por rango de fechas elegido por el usuario). +- **Filtros por tipo de orden** (`buy` / `sell`) y por **estado** (`SUCCESS`, + `CANCELED`, `EXPIRED`, `ACTIVE`, `DISPUTE`, etc.), p. ej. "número de órdenes + canceladas en cierto periodo". +- **Flujo de sats del nodo Lightning**: monto entrante (hold invoices retenidos), + monto saliente (pagos a compradores), y costos de ruteo. +- **Conciliación y ganancias**: ingresos del bot, ganancias de comunidades, + costos de ruteo, saldos pendientes de retiro y verificación de integridad. + +### 1.2 Principios de diseño + +| Principio | Implicación | +|-----------|-------------| +| **Solo lectura** | El dashboard NUNCA escribe en las colecciones del bot. Usuario de DB con rol `read`. Esto evita interferir con la operación del bot. | +| **No tocar el bot** | El dashboard es un proceso separado. No se modifica el código del bot (salvo, opcionalmente, índices de DB). | +| **Reutilizar la lógica de negocio** | Las fórmulas de fees/ganancias deben replicar EXACTAMENTE las del bot (ver §8) para que la conciliación cuadre. | +| **Tiempo real eficiente** | Change Streams para deltas; agregaciones cacheadas para totales históricos. | +| **Privacidad** | No exponer PII (Telegram IDs, usernames, invoices, hashes) en vistas públicas. Ver §12. | + +### 1.3 Fuera de alcance (v1) + +- Escritura/administración de órdenes (eso lo hace el bot). +- Saldos de canales / wallet de LND en vivo (el bot **no** los almacena; ver §9.4 + para la propuesta opcional de snapshots). +- Exportación contable formal/declaraciones fiscales (se provee export CSV/JSON + como base). + +--- + +## 2. Contexto del dominio + +lnp2pBot es un bot de Telegram para trading P2P de Bitcoin sobre Lightning usando +**hold invoices** como escrow sin confianza. Dos patrones de trade: + +**Orden de venta (`sell`)** — el creador vende sats: +1. Vendedor crea orden → publicada en canal (`PENDING`). +2. Comprador toma → vendedor paga el hold invoice (sats **entran/se retienen** en el nodo). +3. Comprador envía fiat → vendedor confirma recepción. +4. Hold invoice se liquida → comprador recibe BTC (sats **salen** del nodo). + +**Orden de compra (`buy`)** — el creador compra sats: +1. Comprador crea orden → publicada (`PENDING`). +2. Vendedor toma → vendedor paga el hold invoice. +3. Comprador provee invoice → vendedor envía fiat. +4. Comprador confirma recepción → hold invoice liquidado → comprador recibe BTC. + +**Implicación para analítica:** el único estado que representa un trade **exitoso y +completado** es `SUCCESS`. Es el momento en que el bot incrementa +`trades_completed` y `volume_traded` de ambas partes y registra `routing_fee`. + +### 2.1 Stack del bot (referencia) + +- **Runtime:** Node.js ≥18, TypeScript (strict). +- **DB:** MongoDB vía Mongoose 8. +- **LN:** librería `lightning` 10.25.0 contra LND (gRPC). +- **Colecciones:** `orders`, `users`, `communities`, `disputes`, `pendingpayments`, `configs`, `blocks`. + +--- + +## 3. Modelo de datos relevante + +> Fuente de verdad: `models/*.ts` del repositorio. Resumen de campos usados por el +> dashboard. **Nota crítica:** los IDs de usuario/comunidad se almacenan como +> `string` (no ObjectId nativo en las referencias cruzadas como `seller_id`, +> `community_id`, etc.). El `_id` de cada documento sí es ObjectId pero tipado como +> `string` en las interfaces. + +### 3.1 `orders` (colección central de analítica) + +| Campo | Tipo | Significado / uso analítico | +|-------|------|------------------------------| +| `_id` | ObjectId | Identificador. **Contiene timestamp de creación** (útil para rango por tiempo sin índice extra). | +| `status` | string (enum) | Estado del ciclo de vida. Ver §3.1.1. Filtro principal. | +| `type` | string | `buy` o `sell`. Filtro principal. | +| `amount` | number | Monto en **satoshis**. Métrica de volumen. | +| `fiat_amount` | number | Monto fiat (orden de monto fijo). | +| `min_amount` / `max_amount` | number | Rango fiat (órdenes de rango). | +| `fiat_code` | string | Código ISO 4217 (USD, EUR, ARS, VES…). Dimensión de "moneda". | +| `payment_method` | string | Método de pago declarado (texto libre). Dimensión secundaria. | +| `fee` | number | Fee total cobrado al vendedor (sats) = porción bot + porción comunidad. | +| `bot_fee` | number | **Snapshot** de `MAX_FEE` al crear la orden (decimal, p.ej. 0.002). | +| `community_fee` | number | **Snapshot** de `FEE_PERCENT` al crear la orden (decimal, p.ej. 0.7). *Ojo: el nombre engaña, es el % que se queda el bot.* | +| `routing_fee` | number | Fee de ruteo LN realmente pagado al liquidar el pago al comprador (sats). **Costo** del bot. | +| `community_id` | string\|null | Comunidad dueña de la orden (null = orden global/sin comunidad). | +| `creator_id`, `seller_id`, `buyer_id` | string | Referencias a usuarios. PII — no exponer crudo. | +| `created_at` | Date | Fecha de creación. **Eje temporal principal.** | +| `taken_at` | Date\|null | Cuándo se tomó la orden. | +| `invoice_held_at` | Date | Cuándo el vendedor pagó el hold invoice (sats retenidos). | +| `calculated` | boolean | `true` si el job de earnings ya contabilizó esta orden a la comunidad. | +| `is_frozen` / `is_public` | boolean | Flags de estado. | +| `price_from_api` / `price_margin` | boolean/number | Si el precio fue de API y el margen aplicado. | +| `hash` / `secret` | string\|null | Hash/secret del hold invoice. PII/sensible — no exponer. | + +#### 3.1.1 Estados (`status` enum) y semántica analítica + +| Estado | Significado | Clasificación para dashboard | +|--------|-------------|------------------------------| +| `PENDING` | Publicada, no tomada | **Abierta** (en mercado) | +| `WAITING_PAYMENT` | Esperando que el vendedor pague el hold invoice | **En progreso** | +| `WAITING_BUYER_INVOICE` | Esperando invoice del comprador | **En progreso** | +| `ACTIVE` | Tomada, fondos retenidos en escrow | **En progreso** (sats retenidos) | +| `FIAT_SENT` | Comprador marcó fiat enviado | **En progreso** | +| `PAID_HOLD_INVOICE` | Vendedor liberó; pago al comprador en curso | **En progreso** (saliendo) | +| `SUCCESS` | **Trade completado con éxito** | **Exitosa** ✅ | +| `DISPUTE` | Disputa abierta | **Disputa** | +| `FROZEN` | Congelada por admin | **Disputa/Admin** | +| `CANCELED` | Cancelada por usuario(s) | **Cancelada** | +| `CANCELED_BY_ADMIN` | Cancelada por admin | **Cancelada** | +| `EXPIRED` | Expirada por job | **Expirada** | +| `HOLD_INVOICE_EXPIRED` | Hold invoice expiró antes de cobrar | **Expirada/Fallida** | +| `CLOSED` | Cerrada | **Cerrada** | + +> **Agrupaciones sugeridas para UI** (chips de filtro de alto nivel): +> - **Exitosas** = `[SUCCESS]` +> - **Abiertas** = `[PENDING]` +> - **En progreso** = `[WAITING_PAYMENT, WAITING_BUYER_INVOICE, ACTIVE, FIAT_SENT, PAID_HOLD_INVOICE]` +> - **Canceladas** = `[CANCELED, CANCELED_BY_ADMIN]` +> - **Expiradas/Fallidas** = `[EXPIRED, HOLD_INVOICE_EXPIRED]` +> - **Disputas** = `[DISPUTE, FROZEN]` + +> ⚠️ **Sesgo de supervivencia en datos históricos:** el job `delete_published_orders` +> **borra físicamente** órdenes `PENDING` viejas (más antiguas que +> `ORDER_PUBLISHED_EXPIRATION_WINDOW`). Por lo tanto el conteo de `PENDING` solo es +> fiable para el presente, no para series históricas. Documentar esto en la UI. + +### 3.2 `users` + +| Campo | Uso analítico | +|-------|---------------| +| `trades_completed` | Trades exitosos del usuario (ya descontados trades circulares). | +| `volume_traded` | Volumen acumulado en sats. | +| `total_rating`, `total_reviews`, `last_rating`, `reviews[]` | Reputación. | +| `disputes` | N.º de disputas del usuario. | +| `created_at` | Alta del usuario → métrica de crecimiento. | +| `default_community_id` | Comunidad por defecto → membresía. | +| `banned`, `admin` | Flags. | +| `volume_traded`, `show_volume_traded`, `show_username` | Banderas de privacidad del propio usuario. Respetar en vistas públicas. | + +### 3.3 `communities` + +| Campo | Uso analítico | +|-------|---------------| +| `name`, `_id`, `group`, `currencies[]`, `payment_methods[]` | Dimensión "comunidad". | +| `fee` | % (0–100) que define cuánto de la porción comunitaria toma la comunidad. | +| `earnings` | Sats acumulados pendientes de retiro (alimentado por el job). | +| `orders_to_redeem` | N.º de órdenes ya contabilizadas a la comunidad. | +| `created_at`, `enabled`, `public` | Estado/altas. | +| `solvers[]`, `banned_users[]` | Operación de la comunidad. | + +### 3.4 `disputes` + +| Campo | Uso | +|-------|-----| +| `status` | `WAITING_FOR_SOLVER`, `IN_PROGRESS`, `SETTLED`, `SELLER_REFUNDED`, `RELEASED`. | +| `community_id`, `order_id`, `solver_id`, `initiator` | Dimensiones. | +| `created_at` | Eje temporal. | + +### 3.5 `pendingpayments` + +| Campo | Uso | +|-------|-----| +| `paid`, `is_invoice_expired` | Estado del pago saliente. | +| `amount` | Sats. | +| `attempts`, `next_retry` | Reintentos (backoff exponencial). | +| `last_error` | `TIMEOUT`, `ROUTING_FAILED`, `INSUFFICIENT_BALANCE`, `AMOUNT_MISMATCH`, `UNKNOWN`. Salud de pagos. | +| `community_id` | `null` = pago de orden a comprador; no-null = retiro de earnings de comunidad. | +| `created_at`, `paid_at` | Latencia de pago. | + +### 3.6 `configs` (estado del nodo) + +Un único documento, actualizado cada minuto por el job `node_info`: +`node_status` (`up`/`down`), `node_alias`, `node_version`, `node_block_height`, +`node_channels_count`, `node_peers_count`, `node_synced_to_chain`, +`node_synced_to_graph`, `maintenance`. + +> **Limitación:** `configs` **no** contiene saldos de wallet ni de canales. Para +> "liquidez disponible" se requiere o bien consultar LND directamente o crear +> snapshots (ver §9.4, opcional). + +--- + +## 4. Definiciones canónicas de métricas + +Estas definiciones son **normativas**: toda la app (backend y frontend) debe +usarlas para evitar discrepancias. + +### 4.1 Dimensión temporal + +- **Campo de tiempo por defecto:** `orders.created_at`. +- **Alternativas seleccionables** (para análisis avanzado): `taken_at` (cuándo se + tomó), `invoice_held_at` (cuándo entraron sats). El selector de "campo de fecha" + es opcional pero recomendado. +- **Granularidades:** `hour`, `day`, `week`, `month`, `year`, `custom range`. +- **Zona horaria:** todas las fechas en DB son UTC. La UI debe permitir elegir TZ + (por defecto UTC); el bucketing por día/mes se hace con `$dateTrunc`/`$dateToString` + usando el parámetro `timezone`. + +### 4.2 Métricas de órdenes + +| Métrica | Definición | Fórmula (conceptual) | +|---------|------------|----------------------| +| **Órdenes creadas** | Conteo de órdenes con `created_at` en el rango. | `count(orders)` | +| **Órdenes exitosas** | Órdenes `SUCCESS` en el rango. | `count(status=SUCCESS)` | +| **Tasa de éxito** | exitosas / (exitosas + canceladas + expiradas). | ver nota* | +| **Órdenes canceladas** | `status ∈ {CANCELED, CANCELED_BY_ADMIN}`. | `count(...)` | +| **Órdenes expiradas** | `status ∈ {EXPIRED, HOLD_INVOICE_EXPIRED}`. | `count(...)` | +| **Órdenes activas (en curso)** | `status ∈ {en progreso}` (snapshot actual). | `count(...)` | +| **Disputas** | desde `disputes` o `status ∈ {DISPUTE, FROZEN}`. | `count(...)` | +| **Volumen (sats)** | suma de `amount` de órdenes `SUCCESS`. | `sum(amount)` | +| **Volumen (fiat)** | suma de `fiat_amount` por `fiat_code` (no sumar entre monedas distintas). | `sum(fiat_amount) group by fiat_code` | +| **Ticket promedio (sats)** | `volumen / nº exitosas`. | `avg(amount)` | +| **Tiempo a completar** | `SUCCESS` ⇒ tiempo desde `created_at`/`taken_at`. | percentiles | + +> \* La "tasa de éxito" excluye órdenes en progreso (aún no resueltas). Definir el +> denominador explícitamente en la UI (tooltip). Recomendado: +> `SUCCESS / (SUCCESS + CANCELED + CANCELED_BY_ADMIN + EXPIRED + HOLD_INVOICE_EXPIRED)`. + +> ⚠️ **Nunca sumar `fiat_amount` entre monedas distintas.** Siempre agrupar por +> `fiat_code`. Para un total único cross-moneda, convertir todo a sats (ya está en +> `amount`) o a una moneda de referencia vía tasa (no recomendado por consistencia +> histórica). + +### 4.3 Ranking de monedas con más operaciones exitosas + +Dimensión = `fiat_code`. Por cada moneda en el rango: +- `successful_orders` = `count(status=SUCCESS)` +- `volume_sats` = `sum(amount)` +- `volume_fiat` = `sum(fiat_amount)` (en su propia moneda) +- `avg_ticket_sats`, `unique_traders` (cardinalidad de `buyer_id`∪`seller_id`). + +Ordenable por cualquiera de las anteriores; default: `successful_orders` desc. + +### 4.4 Métricas financieras (ver §8 para fórmulas exactas) + +- **Fees totales cobrados** = `sum(fee)` de órdenes `SUCCESS`. +- **Ingreso del bot** = `sum( (amount * bot_fee) * community_fee )` (órdenes con + comunidad) + `sum(fee)` (órdenes sin comunidad). Ver matiz en §8. +- **Ganancias de comunidades** = `sum(fee) - ingreso_bot` (porción comunitaria). +- **Costos de ruteo** = `sum(routing_fee)`. +- **Beneficio neto del bot** = `ingreso_bot - costos_ruteo`. + +### 4.5 Métricas del nodo (flujo de fondos) + +- **Sats entrantes (retenidos)** = `sum(amount)` de órdenes que alcanzaron al menos + `ACTIVE` (hold invoice pagado) en el rango — ver §9.1. +- **Sats salientes (pagados)** = `sum(amount)` de órdenes `SUCCESS` en el rango. +- **Sats actualmente en escrow** (snapshot) = `sum(amount)` de + `status ∈ {ACTIVE, FIAT_SENT, PAID_HOLD_INVOICE, DISPUTE, FROZEN}`. +- **Pagos salientes fallidos/pendientes** = desde `pendingpayments`. + +--- + +## 5. Arquitectura del sistema + +``` + ┌─────────────────────────────┐ + │ Navegador (SPA) │ + │ React + Recharts/ECharts │ + └───────────┬─────────────────┘ + HTTPS │ WSS (tiempo real) + ▼ + ┌──────────────────────────────────────────────┐ + │ Backend Analytics (Node/TS) │ + │ ┌────────────┐ ┌───────────┐ ┌───────────┐ │ + │ │ REST API │ │ WS/SSE hub│ │ Auth (JWT)│ │ + │ └─────┬──────┘ └─────┬─────┘ └───────────┘ │ + │ ┌─────▼─────────────────────────────────────┐│ + │ │ Capa de servicios (queries + reconcile) ││ + │ └─────┬───────────────────┬─────────────────┘│ + │ ┌─────▼──────┐ ┌─────────▼──────────┐ │ + │ │ Caché │ │ Change Stream │ │ + │ │ (Redis/mem)│ │ listeners │ │ + │ └────────────┘ └─────────┬──────────┘ │ + └──────────────────────────────┼────────────────┘ + read-only │ │ change streams + ▼ ▼ + ┌──────────────────────────────┐ + │ MongoDB (replica set) │ + │ orders, users, communities… │ + └──────────────────────────────┘ + ▲ + │ (opcional, §9.4) snapshots de balance + ┌──────────┴───────────┐ + │ LND (read-only macaroon)│ + └──────────────────────┘ +``` + +### 5.1 Decisiones tecnológicas recomendadas + +| Componente | Recomendación | Justificación | +|------------|---------------|---------------| +| Lenguaje backend | **TypeScript + Node.js** | Reutiliza tipos/modelos del bot (`IOrder`, etc.), mismo ecosistema. | +| Framework HTTP | **Fastify** (o Express) | Rápido, buen soporte de schemas/validación. | +| Acceso a datos | **Driver nativo de MongoDB** o Mongoose en modo lectura | Aggregation pipeline. Reutilizar interfaces de `models/`. | +| Tiempo real | **Change Streams + WebSocket** (ws/socket.io) o **SSE** | Requiere replica set (ver §10). | +| Caché | **Redis** (prod) / in-memory LRU (dev) | Cachear agregaciones costosas. | +| Frontend | **React + Vite + TypeScript** | SPA. | +| Gráficas | **ECharts** o **Recharts** | Series temporales, barras, donuts, heatmaps. | +| Auth | **JWT** + login admin, o reverse-proxy con OAuth | Datos sensibles. | +| Programación de tareas | **node-cron** (refresco de caché/rollups) | Igual que el bot usa node-schedule. | + +> **Reutilización:** el backend puede importar directamente las interfaces de +> `models/*.ts` del repo (p. ej. publicarlas como paquete o copiar tipos) para +> garantizar que los nombres de campos y enums no se desincronicen. + +### 5.2 Estructura de proyecto propuesta (monorepo o repo aparte) + +``` +analytics/ +├── backend/ +│ ├── src/ +│ │ ├── config/ # env, conexión Mongo (read-only), Redis +│ │ ├── db/ # cliente Mongo, helpers de pipeline +│ │ ├── models/ # tipos compartidos (reflejan models/ del bot) +│ │ ├── services/ +│ │ │ ├── orders.service.ts # métricas de órdenes +│ │ │ ├── currencies.service.ts # rankings por fiat +│ │ │ ├── node.service.ts # flujo de fondos / salud nodo +│ │ │ ├── reconcile.service.ts # conciliación de ganancias +│ │ │ ├── communities.service.ts +│ │ │ └── users.service.ts +│ │ ├── realtime/ # change streams + hub WS +│ │ ├── api/ # rutas REST +│ │ ├── auth/ # JWT, middleware +│ │ └── cache/ # capa de caché + invalidación +│ └── test/ +└── frontend/ + └── src/ + ├── api/ # cliente REST + WS + ├── components/ # gráficos, tablas, filtros + ├── pages/ # Overview, Orders, Currencies, Node, Finance… + └── state/ # store de filtros globales +``` + +--- + +## 6. Diseño de la API + +### 6.1 Convenciones + +- Base: `/api/v1`. +- Todos los endpoints aceptan los **filtros comunes** como query params: + +| Param | Tipo | Default | Descripción | +|-------|------|---------|-------------| +| `from` | ISO8601 | `-30d` | Inicio del rango (inclusive). | +| `to` | ISO8601 | `now` | Fin del rango (exclusive). | +| `granularity` | enum | `day` | `hour\|day\|week\|month\|year`. | +| `tz` | string | `UTC` | Zona horaria IANA para bucketing. | +| `type` | enum\|csv | (todos) | `buy`, `sell`. | +| `status` | csv | (depende) | Lista de estados o grupos (`success`, `canceled`…). | +| `fiat` | csv | (todas) | Códigos ISO (`USD,EUR,ARS`). | +| `community_id` | string | (todas) | Filtra por comunidad. `none` = sin comunidad. | +| `payment_method` | string | (todos) | Texto. | +| `dateField` | enum | `created_at` | `created_at\|taken_at\|invoice_held_at`. | + +- Respuesta estándar: + +```jsonc +{ + "data": { /* ... */ }, + "meta": { + "from": "2026-06-01T00:00:00Z", + "to": "2026-06-30T00:00:00Z", + "generatedAt": "2026-06-30T12:00:00Z", + "cached": true, + "filtersApplied": { "type": "sell", "fiat": ["USD"] } + } +} +``` + +### 6.2 Endpoints REST + +#### Resumen / Overview +- `GET /api/v1/overview` — KPIs de cabecera: total órdenes, exitosas, canceladas, + volumen sats/fiat (top monedas), ingreso del bot, beneficio neto, estado nodo, + sats en escrow. (Respeta filtros comunes.) + +#### Órdenes +- `GET /api/v1/orders/summary` — conteos por estado y por tipo en el rango. +- `GET /api/v1/orders/timeseries` — serie temporal por `granularity` de una + métrica (`metric=count|volume_sats|volume_fiat|success_rate`), opcionalmente + desglosada (`groupBy=status|type|fiat`). +- `GET /api/v1/orders/status-breakdown` — distribución por estado (para donut). +- `GET /api/v1/orders/count?status=CANCELED&from=…&to=…` — caso de uso explícito: + "número de órdenes canceladas en un periodo". (Atajo conveniente.) + +#### Monedas (fiat) +- `GET /api/v1/currencies/ranking` — ranking de monedas con más operaciones + exitosas (parámetro `sortBy=successful_orders|volume_sats|volume_fiat`, + `limit=N`). +- `GET /api/v1/currencies/:code/timeseries` — evolución de una moneda. + +#### Nodo / flujo de fondos +- `GET /api/v1/node/status` — último estado de `configs` (up/down, peers, canales, + sync, block height, maintenance). +- `GET /api/v1/node/flow` — sats entrantes vs salientes (serie temporal y totales), + sats en escrow (snapshot), costos de ruteo. +- `GET /api/v1/node/payments-health` — `pendingpayments`: pendientes, fallidos por + `last_error`, reintentos promedio, latencia de pago. + +#### Finanzas / conciliación +- `GET /api/v1/finance/reconciliation` — informe de conciliación (ver §8.4). +- `GET /api/v1/finance/bot-earnings/timeseries` — ingreso del bot por periodo. +- `GET /api/v1/finance/routing-costs/timeseries` — costos de ruteo por periodo. + +#### Comunidades +- `GET /api/v1/communities/ranking` — comunidades por volumen/órdenes/earnings. +- `GET /api/v1/communities/:id/overview` — métricas de una comunidad. +- `GET /api/v1/communities/:id/earnings` — earnings acumulados, orders_to_redeem, + retiros pendientes. + +#### Usuarios (agregado, sin PII) +- `GET /api/v1/users/growth` — altas por periodo (`users.created_at`). +- `GET /api/v1/users/leaderboard` — top traders **respetando `show_username` / + `show_volume_traded`** (ver §12). Por defecto anonimizado. +- `GET /api/v1/users/activity` — usuarios activos (con ≥1 orden) por periodo. + +#### Disputas +- `GET /api/v1/disputes/summary` — por estado, por comunidad, tasa de disputa + (disputas / órdenes en rango). + +#### Export +- `GET /api/v1/export/orders.csv` — export filtrado (campos no sensibles). +- `GET /api/v1/export/reconciliation.csv`. + +### 6.3 WebSocket / SSE + +- `WS /ws` (o `GET /api/v1/stream` para SSE). El cliente se suscribe a canales: + - `overview` — KPIs recalculados ante cada cambio relevante (con debounce). + - `orders` — eventos de orden creada/actualizada (delta). + - `node` — cambios de estado del nodo. + - `finance` — recálculo de earnings al detectar `SUCCESS` nuevas. + +Mensaje de evento: +```jsonc +{ + "channel": "orders", + "event": "order.updated", + "payload": { "id": "...", "status": "SUCCESS", "type": "sell", "fiat_code": "USD", "amount": 50000, "delta": { "metric": "successful_orders", "by": 1 } }, + "ts": "2026-06-30T12:00:01Z" +} +``` + +> El payload de tiempo real **no** incluye PII (sin `buyer_id`/`seller_id`/`hash`). + +--- + +## 7. Catálogo de consultas de agregación + +Pipelines de MongoDB listos para implementar. (Pseudocódigo cercano a la sintaxis +real; ajustar nombres de colección — Mongoose pluraliza a `orders`, etc.) + +### 7.1 Conteo por estado y tipo (orders/summary) + +```js +db.orders.aggregate([ + { $match: { created_at: { $gte: from, $lt: to }, /* ...filtros opcionales... */ } }, + { $group: { + _id: { status: "$status", type: "$type" }, + count: { $sum: 1 }, + volume_sats: { $sum: "$amount" } + }}, + { $group: { + _id: "$_id.status", + total: { $sum: "$count" }, + byType: { $push: { type: "$_id.type", count: "$count", volume_sats: "$volume_sats" } } + }} +]) +``` + +### 7.2 Serie temporal de órdenes exitosas por día (orders/timeseries) + +```js +db.orders.aggregate([ + { $match: { status: "SUCCESS", created_at: { $gte: from, $lt: to } } }, + { $group: { + _id: { $dateTrunc: { date: "$created_at", unit: "day", timezone: tz } }, + count: { $sum: 1 }, + volume_sats: { $sum: "$amount" } + }}, + { $sort: { _id: 1 } } +]) +``` + +> Variante por `granularity`: cambiar `unit` a `hour|week|month|year`. Para series +> con huecos rellenos (días sin órdenes), usar `$densify` (Mongo ≥5.1) o rellenar en +> el backend. + +### 7.3 Ranking de monedas con más operaciones exitosas (currencies/ranking) + +```js +db.orders.aggregate([ + { $match: { status: "SUCCESS", created_at: { $gte: from, $lt: to } } }, + { $group: { + _id: "$fiat_code", + successful_orders: { $sum: 1 }, + volume_sats: { $sum: "$amount" }, + volume_fiat: { $sum: { $ifNull: ["$fiat_amount", 0] } }, + avg_ticket_sats: { $avg: "$amount" }, + traders: { $addToSet: "$buyer_id" } // aproximación; ver nota + }}, + { $addFields: { unique_buyers: { $size: "$traders" } } }, + { $project: { traders: 0 } }, + { $sort: { successful_orders: -1 } }, + { $limit: limit } +]) +``` + +> Para `unique_traders` exacto (compradores ∪ vendedores) usar dos `$addToSet` +> (`buyer_id`, `seller_id`) y unir con `$setUnion` antes de `$size`. + +### 7.4 "Órdenes canceladas en un periodo" (orders/count) + +```js +db.orders.countDocuments({ + status: { $in: ["CANCELED", "CANCELED_BY_ADMIN"] }, + created_at: { $gte: from, $lt: to }, + // ...type, community_id opcionales +}) +``` + +### 7.5 Órdenes activas / en escrow (snapshot, sin filtro temporal) + +```js +db.orders.aggregate([ + { $match: { status: { $in: ["ACTIVE","FIAT_SENT","PAID_HOLD_INVOICE","DISPUTE","FROZEN"] } } }, + { $group: { _id: "$status", count: { $sum: 1 }, sats_in_escrow: { $sum: "$amount" } } } +]) +``` + +### 7.6 Flujo de fondos del nodo (node/flow) + +**Salientes (pagados) por día:** +```js +db.orders.aggregate([ + { $match: { status: "SUCCESS", created_at: { $gte: from, $lt: to } } }, + { $group: { + _id: { $dateTrunc: { date: "$created_at", unit: granularity, timezone: tz } }, + out_sats: { $sum: "$amount" }, + routing_fee: { $sum: "$routing_fee" } + }}, + { $sort: { _id: 1 } } +]) +``` + +**Entrantes (retenidos) por día** — usar `invoice_held_at` y estados que implican +hold pagado (ver §9.1): +```js +db.orders.aggregate([ + { $match: { + invoice_held_at: { $gte: from, $lt: to, $ne: null }, + status: { $in: ["ACTIVE","FIAT_SENT","PAID_HOLD_INVOICE","SUCCESS","DISPUTE","FROZEN","HOLD_INVOICE_EXPIRED"] } + }}, + { $group: { + _id: { $dateTrunc: { date: "$invoice_held_at", unit: granularity, timezone: tz } }, + in_sats: { $sum: "$amount" } + }}, + { $sort: { _id: 1 } } +]) +``` + +### 7.7 Salud de pagos salientes (node/payments-health) + +```js +db.pendingpayments.aggregate([ + { $facet: { + open: [ { $match: { paid: false, is_invoice_expired: false } }, { $count: "n" } ], + expired: [ { $match: { is_invoice_expired: true } }, { $count: "n" } ], + byError: [ { $match: { paid: false } }, { $group: { _id: "$last_error", n: { $sum: 1 } } } ], + retries: [ { $group: { _id: null, avgAttempts: { $avg: "$attempts" } } } ] + }} +]) +``` + +### 7.8 Conciliación / ingreso del bot — ver §8. + +--- + +## 8. Conciliación de ganancias (módulo financiero) + +Esta es la sección más delicada: **las fórmulas deben coincidir exactamente con el +código del bot**, o la conciliación no cuadra. A continuación, la lógica real del +repo y cómo replicarla. + +### 8.1 Cómo el bot calcula el fee al crear la orden + +`util/index.ts → getFee(amount, communityId)`: + +``` +maxFee = round(amount * MAX_FEE) // porción total destinada a fees +if (!communityId) return maxFee // orden sin comunidad: todo es fee del bot +botFee = maxFee * FEE_PERCENT // lo que se queda el bot +communityFee = round(maxFee - botFee) // base de la comunidad +communityFee = communityFee * (community.fee / 100) // ajuste por % de la comunidad +return botFee + communityFee // => se guarda en order.fee +``` + +Al crear la orden se guardan **snapshots**: +- `order.bot_fee = MAX_FEE` (decimal, p.ej. 0.002) +- `order.community_fee = FEE_PERCENT` (decimal, p.ej. 0.7) +- `order.fee = botFee + communityFee` (sats) + +### 8.2 Cómo el bot calcula las ganancias de la comunidad (job) + +`jobs/calculate_community_earnings.ts` (corre cada 10 min sobre órdenes +`SUCCESS`, `community_id != null`, `calculated == false`): + +``` +amount = order.amount +fee = order.fee +botFee = order.bot_fee || MAX_FEE +communityFeePercent = order.community_fee || FEE_PERCENT +maxFee = amount * botFee +communityFee = fee - maxFee * communityFeePercent // <-- porción de la comunidad +community.earnings += round(communityFee) +community.orders_to_redeem += 1 +order.calculated = true +``` + +> 🔎 **Matiz importante de conciliación:** la fórmula del *job* para +> `communityFee` es `fee - maxFee * communityFeePercent`, que es el **complemento** +> de la porción del bot. Es decir, el bot considera su ingreso por orden como +> `maxFee * communityFeePercent` = `(amount * bot_fee) * community_fee`, y lo que +> sobra del `fee` total va a la comunidad. **El dashboard debe usar exactamente esta +> definición** para que `Σ ingreso_bot + Σ earnings_comunidad == Σ fee`. + +### 8.3 Definiciones financieras canónicas para el dashboard + +Por **cada orden `SUCCESS`**: + +``` +botRevenue_i = + if (community_id == null): fee_i // sin comunidad: todo el fee es del bot + else: amount_i * bot_fee_i * community_fee_i // = maxFee * FEE_PERCENT + +communityEarnings_i = + if (community_id == null): 0 + else: fee_i - (amount_i * bot_fee_i * community_fee_i) + +routingCost_i = routing_fee_i + +netProfit_i = botRevenue_i - routingCost_i +``` + +Agregados en el rango: +``` +TotalFees = Σ fee_i +BotRevenue = Σ botRevenue_i +CommunityEarnings = Σ communityEarnings_i +RoutingCosts = Σ routing_fee_i +BotNetProfit = BotRevenue - RoutingCosts +``` + +> **Identidad de control (debe cumplirse):** +> `BotRevenue + CommunityEarnings == TotalFees` (salvo redondeos por orden — +> el bot hace `round()` en varios puntos; tolerar ±1 sat por orden). + +### 8.4 Informe de conciliación (finance/reconciliation) + +Estructura de respuesta sugerida: + +```jsonc +{ + "period": { "from": "...", "to": "..." }, + "orders": { "successful": 1234 }, + "fees": { + "total_fees_sats": 250000, + "bot_revenue_sats": 175000, + "community_earnings_sats": 75000, + "identity_check": { "expected": 250000, "computed": 250000, "diff": 0, "ok": true } + }, + "routing": { "routing_costs_sats": 4200 }, + "profit": { "bot_net_profit_sats": 170800 }, + "communities": { + "accrued_in_db_sats": 73000, // Σ community.earnings actual + "computed_from_orders_sats": 75000, // Σ communityEarnings_i (rango all-time) + "pending_calculation_sats": 2000, // órdenes SUCCESS con calculated=false + "note": "diferencia explicada por job aún no ejecutado / retiros realizados" + }, + "withdrawals": { + "pending_sats": 1500, // Σ pendingpayments(community_id!=null, paid=false) + "failed": 0 + } +} +``` + +#### 8.4.1 Verificaciones de integridad incluidas + +1. **Identidad de fees:** `BotRevenue + CommunityEarnings ≈ TotalFees`. +2. **Earnings DB vs computado:** comparar `Σ community.earnings` (actual) contra + `Σ communityEarnings_i` de órdenes `SUCCESS, calculated=true`. Diferencias se + explican por **retiros ya pagados** (earnings se ponen a 0 al retirar) — por eso + también hay que sumar retiros históricos pagados. + - Conciliación completa: `Σ communityEarnings_i (calculated=true)` debería ≈ + `Σ community.earnings (actual)` + `Σ pendingpayments(community, paid=true).amount`. +3. **Órdenes pendientes de cálculo:** `count(SUCCESS, community_id!=null, + calculated=false)` y su suma — dinero "en tránsito" hacia earnings. +4. **Backlog de pagos:** `pendingpayments(paid=false)` agrupados por `last_error`. + +> ⚠️ **No** recomputar earnings escribiendo en la DB. El dashboard solo **lee** y +> **compara**; si detecta descuadres, los reporta como alertas, no los corrige. + +### 8.5 Serie temporal de ganancias del bot (finance/bot-earnings/timeseries) + +```js +db.orders.aggregate([ + { $match: { status: "SUCCESS", created_at: { $gte: from, $lt: to } } }, + { $addFields: { + botRevenue: { + $cond: [ + { $eq: ["$community_id", null] }, + "$fee", + { $multiply: ["$amount", "$bot_fee", "$community_fee"] } + ] + } + }}, + { $group: { + _id: { $dateTrunc: { date: "$created_at", unit: granularity, timezone: tz } }, + bot_revenue: { $sum: "$botRevenue" }, + routing_cost: { $sum: "$routing_fee" }, + fees_total: { $sum: "$fee" } + }}, + { $addFields: { net_profit: { $subtract: ["$bot_revenue", "$routing_cost"] } } }, + { $sort: { _id: 1 } } +]) +``` + +--- + +## 9. Flujo de fondos del nodo Lightning + +### 9.1 Sats entrantes (retenidos en escrow) + +Cuando un vendedor paga el hold invoice, los sats quedan **retenidos** en el nodo. +El bot marca esto con `invoice_held_at` y transiciona la orden a `ACTIVE` (o estados +posteriores). Para "monto entrante en el nodo" en un periodo: + +- Filtrar por `invoice_held_at` en el rango y `status` que implique que el hold se + pagó alguna vez: `{ACTIVE, FIAT_SENT, PAID_HOLD_INVOICE, SUCCESS, DISPUTE, + FROZEN, HOLD_INVOICE_EXPIRED}`. (Ver pipeline §7.6.) +- Sumar `amount`. + +> Nota: técnicamente en hold invoices los sats están "en vuelo/retenidos", no +> liquidados, hasta `settle`. Para fines de tablero, "entrante" = monto que el nodo +> retuvo (HTLC aceptado). Aclarar esta semántica en un tooltip. + +### 9.2 Sats salientes (pagados a compradores) + +Al liquidar el hold invoice, el bot paga el invoice del comprador +(`ln/pay_request.ts → payToBuyer`) y marca la orden `SUCCESS`, guardando +`routing_fee = payment.fee`. "Monto saliente" = `Σ amount` de órdenes `SUCCESS` +(ver §7.6). El costo real de la red es `Σ routing_fee`. + +### 9.3 Sats actualmente en escrow (snapshot) + +`Σ amount` de `status ∈ {ACTIVE, FIAT_SENT, PAID_HOLD_INVOICE, DISPUTE, FROZEN}` +(§7.5). Indicador de exposición/fondos comprometidos del nodo en este instante. + +### 9.4 (Opcional) Liquidez real del nodo — snapshots de LND + +El bot **no** almacena saldos de wallet ni de canales (solo `getWalletInfo`, que no +trae balances). Si se quiere mostrar liquidez disponible (capacidad local/remota, +balance on-chain), hay dos opciones: + +- **Opción A (recomendada, sin tocar el bot):** el backend de analítica abre su + propia conexión a LND con un **macaroon de solo lectura** (`readonly.macaroon`) y + llama periódicamente a `getChannelBalance`, `getChainBalance`, `getChannels`. + Guardar snapshots en una colección **propia del dashboard** (p. ej. + `analytics_node_snapshots`) — nunca en las colecciones del bot. +- **Opción B:** proponer al bot un job que persista estos balances en `configs` + (cambio de código del bot, fuera del alcance de v1 pero documentado como mejora). + +Esquema de snapshot propuesto (colección propia): +```jsonc +{ + "_id": ObjectId, + "ts": ISODate, + "chain_balance_sats": 0, + "channel_balance_local_sats": 0, + "channel_balance_remote_sats": 0, + "pending_channel_balance_sats": 0, + "active_channels": 0, + "inactive_channels": 0, + "peers": 0, + "block_height": 0 +} +``` + +### 9.5 Salud del nodo (configs) + +Leer el documento de `configs` para: `node_status`, `node_alias`, `node_version`, +`node_block_height`, `node_channels_count`, `node_peers_count`, +`node_synced_to_chain`, `node_synced_to_graph`, `maintenance`. Mostrar como tarjeta +de estado con semáforo (verde si `up` y `synced_to_chain`). + +--- + +## 10. Tiempo real: estrategia + +### 10.1 MongoDB Change Streams (recomendado) + +El bot usa un bus de eventos **en proceso** (`bot/modules/events`) que **no** es +accesible desde un proceso externo. Por tanto, el dashboard obtiene cambios en +tiempo real vía **Change Streams** de MongoDB. + +**Requisito:** MongoDB debe correr como **replica set** (aunque sea de un nodo) para +soportar change streams. Si el despliegue actual es standalone, documentar la +necesidad de convertirlo a replica set de 1 miembro. + +Listeners a implementar: +```js +db.collection('orders').watch( + [ { $match: { 'operationType': { $in: ['insert','update','replace'] } } } ], + { fullDocument: 'updateLookup' } +) +``` +- Ante `insert` o cambio de `status` → emitir delta al hub WS y **invalidar caché** + de las métricas afectadas (overview, timeseries del día actual, ranking del rango + vigente). +- `watch` también sobre `communities` (earnings), `configs` (estado nodo), + `pendingpayments` (salud de pagos), `disputes`. + +### 10.2 Estrategia de actualización incremental + +Para no recalcular todo en cada evento: +- **Contadores en vivo del día/hora actual:** mantener en memoria/Redis y aplicar + deltas (+1 a `SUCCESS` de `USD`, +amount al volumen, etc.). +- **Totales históricos (rangos cerrados):** cacheados con TTL; no cambian. +- **Debounce:** agrupar ráfagas de eventos (p. ej. ventana de 1–2 s) antes de + empujar al cliente. + +### 10.3 Fallback sin replica set + +Si no se puede habilitar replica set: **polling** del backend (p. ej. cada 5–15 s) +recalculando solo los KPIs "calientes" (día actual) y empujando por WS/SSE. +Documentar que esto es menos eficiente y con mayor latencia. + +### 10.4 Resiliencia de change streams + +- Persistir el **resume token** para reanudar tras desconexión sin perder eventos. +- Reintentos con backoff ante cierre del stream. +- Health-check del listener; alertar si se cae. + +--- + +## 11. Frontend: vistas, filtros y componentes + +### 11.1 Barra de filtros global (persistente en todas las páginas) + +- **Selector de rango de fechas** con presets: Hoy, Ayer, Últimos 7/30/90 días, + Este mes, Mes pasado, Este año, **Personalizado**. +- **Granularidad:** hora/día/semana/mes (se adapta al rango). +- **Tipo de orden:** toggle buy / sell / ambos. +- **Estado:** multiselect con grupos (Exitosas, Canceladas, Expiradas, En progreso, + Disputas) y estados individuales. +- **Moneda (fiat):** multiselect con búsqueda (97 monedas con precio). +- **Comunidad:** selector (incluye "Sin comunidad" y "Todas"). +- **Método de pago:** texto/autocomplete. +- **Zona horaria.** + +Los filtros se reflejan en la URL (querystring) para compartir vistas. Estado global +en el store del frontend. + +### 11.2 Páginas + +#### 11.2.1 Overview (Resumen) +- Tarjetas KPI: Órdenes totales, Exitosas, Tasa de éxito, Volumen (sats), + Canceladas, En escrow (sats), Ingreso bot, Beneficio neto, Estado del nodo. +- Mini-sparklines en cada tarjeta (tendencia del periodo). +- Gráfico principal: órdenes exitosas vs creadas (serie temporal). +- Donut: distribución por estado. +- Top 5 monedas (mini-ranking). +- **Indicador "EN VIVO"** (parpadea con cada evento WS). + +#### 11.2.2 Órdenes +- Serie temporal (selector de métrica: conteo / volumen sats / volumen fiat). +- Desglose apilado por estado o por tipo. +- Tabla de "conteo por estado" con el filtro vigente (resuelve "¿cuántas canceladas + en este periodo?"). +- Heatmap hora-del-día × día-de-semana (actividad). +- Comparativa buy vs sell. + +#### 11.2.3 Monedas +- **Ranking de monedas con más operaciones exitosas** (tabla ordenable): + moneda, nº exitosas, volumen sats, volumen fiat, ticket promedio, traders únicos. +- Gráfico de barras top-N. +- Drill-down: clic en una moneda → su serie temporal y desglose por tipo/estado. +- Treemap de cuota por moneda. + +#### 11.2.4 Nodo (Flujo de fondos) +- Tarjeta de estado del nodo (alias, versión, block height, peers, canales, sync). +- Gráfico entrante vs saliente (sats) por periodo. +- Sats en escrow ahora (gauge) con desglose por estado. +- Costos de ruteo por periodo y % sobre volumen. +- Salud de pagos: pendientes, fallidos por tipo de error, reintentos. +- (Si §9.4 habilitado) Liquidez de canales: local/remoto, balance on-chain. + +#### 11.2.5 Finanzas / Conciliación +- Informe de conciliación (§8.4) con semáforos de integridad. +- Ingreso del bot vs costos de ruteo vs beneficio neto (serie temporal). +- Earnings por comunidad (tabla) + pendientes de cálculo + retiros pendientes. +- Botón de export CSV/JSON. + +#### 11.2.6 Comunidades +- Ranking por volumen/órdenes/earnings. +- Detalle de comunidad: métricas, monedas que opera, solvers, disputas. + +#### 11.2.7 Usuarios (agregado) +- Crecimiento de altas (serie temporal). +- Usuarios activos por periodo. +- Leaderboard (anonimizado por defecto; ver §12). +- Distribución de reputación / nº de disputas. + +#### 11.2.8 Disputas +- Por estado, por comunidad, tasa de disputa, tiempo de resolución (si derivable). + +### 11.3 Componentes reutilizables +- `` +- `` +- `` +- `` +- `` (global) +- `` (estado de conexión WS) +- `` +- `` + +### 11.4 UX +- Estados de carga (skeletons), vacío y error en cada widget. +- Formato de sats (con separador de miles y opción a BTC). +- Formato fiat según `decimal_digits` de `util/fiat.json`. +- Tema claro/oscuro. i18n (es/en como mínimo, alineado a los locales del bot). + +--- + +## 12. Seguridad, autenticación y privacidad + +### 12.1 Acceso a datos +- **Usuario MongoDB de solo lectura** dedicado al dashboard (rol `read` sobre la DB + del bot). Nunca credenciales de escritura. +- Conexión por red privada / túnel; TLS si es remota. + +### 12.2 Autenticación de la app +- El dashboard contiene datos sensibles (finanzas, volúmenes, posiblemente PII). + **No debe ser público sin auth.** +- Opción mínima: login de admin con JWT (usuarios definidos en config del + dashboard, no en la DB del bot). +- Opción robusta: detrás de reverse proxy con OAuth/SSO o IP allowlist. +- Roles: `viewer` (solo agregados anonimizados) vs `admin` (finanzas + detalle). + +### 12.3 Privacidad / PII +Campos **sensibles** que NO deben exponerse en vistas/exports salvo a `admin`, y aun +así con cautela: +- `seller_id`, `buyer_id`, `creator_id`, `tg_id`, `username`, `hash`, `secret`, + `buyer_invoice`, `*_dispute_token`, `payment_request`. + +Reglas: +- **Respetar banderas del usuario:** `users.show_username` y + `users.show_volume_traded`. El leaderboard solo muestra username/volumen si el + usuario lo permitió; de lo contrario, anonimizar (p. ej. "Trader #abcd" derivado + de un hash no reversible). +- Endpoints agregados nunca devuelven IDs crudos. +- Export por defecto excluye columnas PII. + +### 12.4 Hardening +- Rate limiting en la API. +- Validación estricta de query params (whitelist de estados, fiat codes, etc.) para + evitar inyección en pipelines. +- Límites de rango (p. ej. máximo 2 años) y `limit` máximo en rankings. +- CORS restringido al dominio del frontend. +- Auditoría de accesos a finanzas. + +--- + +## 13. Rendimiento, índices y caché + +### 13.1 Índices recomendados (en la DB del bot, solo lectura no los crea) + +El bot ya define: `{creator_id:1, status:1}`, `{_id:1, status:1}`, índices en +`users.tg_id`, `communities.group`, etc. Para analítica conviene **proponer** (a +aplicar por el operador del bot, no por el dashboard): + +``` +orders: { status: 1, created_at: 1 } // filtros + rango temporal +orders: { fiat_code: 1, status: 1, created_at: 1 } // ranking de monedas +orders: { community_id: 1, status: 1, created_at: 1 } // por comunidad +orders: { type: 1, status: 1, created_at: 1 } // por tipo +orders: { invoice_held_at: 1 } // flujo entrante +orders: { status: 1, calculated: 1 } // conciliación +pendingpayments: { paid: 1, last_error: 1 } +disputes: { status: 1, created_at: 1 } +users: { created_at: 1 } +``` + +> Los índices se crean con `background: true`. Coordinar con el equipo del bot; el +> dashboard **no** debe crear índices automáticamente en producción sin acuerdo. + +### 13.2 Estrategia de caché +- **Rangos cerrados/históricos:** caché con TTL largo (p. ej. 1 h) o indefinido con + invalidación por evento; los datos pasados no cambian (salvo borrado de `PENDING`). +- **Día/hora actual:** caché corta (s) + actualización por change stream. +- Clave de caché = hash de (endpoint + filtros normalizados). +- Invalidación selectiva ante eventos relevantes (nueva `SUCCESS` invalida overview, + timeseries del día, ranking del rango vigente, finanzas). + +### 13.3 Pre-agregación / rollups (opcional, escala alta) +- Job programado que materializa rollups diarios por (día, fiat, type, status, + community) en una colección **propia del dashboard** (`analytics_daily_rollups`). + Las consultas de rangos largos leen rollups; el día actual se calcula en vivo. +- Esto reduce drásticamente la carga si `orders` crece a millones. + +### 13.4 Límites operativos +- `allowDiskUse: true` en agregaciones grandes. +- Timeouts de consulta (`maxTimeMS`). +- Paginación en tablas (rankings, listados). + +--- + +## 14. Despliegue y configuración + +### 14.1 Variables de entorno del dashboard + +```bash +# Conexión DB (solo lectura) +ANALYTICS_MONGO_URI='mongodb://readonly_user:pass@host:27017/p2plnbot?authSource=admin&readPreference=secondaryPreferred' + +# Servidor +PORT=4000 +NODE_ENV=production +CORS_ORIGIN='https://analytics.example.com' + +# Auth +JWT_SECRET='...' +ADMIN_USERS='admin:bcrypt_hash,...' # o integración OAuth + +# Caché +REDIS_URL='redis://localhost:6379' # opcional + +# Tiempo real +ENABLE_CHANGE_STREAMS=true # requiere replica set + +# (Opcional) LND read-only para snapshots de liquidez (§9.4) +LND_READONLY_MACAROON_BASE64='' +LND_CERT_BASE64='' +LND_GRPC_HOST='127.0.0.1:10009' +NODE_SNAPSHOT_INTERVAL_SECONDS=300 + +# Límites +MAX_RANGE_DAYS=730 +DEFAULT_TZ='UTC' +``` + +### 14.2 Empaquetado +- Backend y frontend en contenedores Docker separados (o uno sirviendo el build + estático del frontend). +- `docker-compose` para dev (Mongo replica set de 1 nodo + Redis + backend + + frontend). +- El bot ya se despliega en DigitalOcean (ver `DEPLOY_DIGITALOCEAN.md`); el + dashboard puede colocarse junto o en infra separada con acceso de red a Mongo. + +### 14.3 Observabilidad +- Logs estructurados (winston, como el bot). +- Métricas del propio dashboard (latencia de endpoints, hit ratio de caché, estado + del change stream). +- Health endpoint `GET /healthz` (incluye estado de conexión Mongo y change stream). + +--- + +## 15. Plan de implementación por fases + +### Fase 0 — Cimientos (1 sprint) +- Conexión Mongo read-only, tipos compartidos con `models/`, scaffolding + backend (Fastify) + frontend (Vite/React), auth básica JWT, CI. +- `GET /healthz`, `GET /node/status`. + +### Fase 1 — Métricas core de órdenes (1–2 sprints) +- `orders/summary`, `orders/timeseries`, `orders/status-breakdown`, + `orders/count`. +- FilterBar global + página Overview + página Órdenes. +- Definir y testear todas las métricas de §4.2. + +### Fase 2 — Monedas (1 sprint) +- `currencies/ranking` + drill-down. +- Página Monedas (ranking + barras + treemap). + +### Fase 3 — Nodo y flujo de fondos (1–2 sprints) +- `node/flow`, `node/payments-health`, snapshot en escrow. +- Página Nodo. (Opcional §9.4: snapshots LND read-only.) + +### Fase 4 — Finanzas / conciliación (1–2 sprints) +- `finance/reconciliation`, series de ingreso/beneficio. +- Página Finanzas con verificaciones de integridad. **Tests de conciliación + exhaustivos** (clave del proyecto). + +### Fase 5 — Tiempo real (1 sprint) +- Change streams + hub WS/SSE + actualización incremental + LiveBadge. +- Fallback de polling si no hay replica set. + +### Fase 6 — Comunidades, usuarios, disputas, export (1–2 sprints) +- Páginas restantes, export CSV/JSON, refinamiento de privacidad. + +### Fase 7 — Rendimiento y hardening (1 sprint) +- Caché Redis, rollups opcionales, índices propuestos, rate limiting, auditoría. + +--- + +## 16. Pruebas + +### 16.1 Unitarias +- **Cálculos financieros (críticos):** dado un conjunto de órdenes sintéticas con + `amount`, `bot_fee`, `community_fee`, `fee`, verificar que `botRevenue`, + `communityEarnings`, `netProfit` coinciden con la lógica del bot (§8). Incluir + casos con y sin comunidad, y comprobar la **identidad de control**. +- Bucketing temporal por TZ y granularidad. +- Normalización/validación de filtros. + +### 16.2 Integración +- Levantar Mongo de prueba (replica set 1 nodo), sembrar datos representativos + (todos los estados, varias monedas, comunidades), y validar cada endpoint contra + resultados esperados calculados de forma independiente. +- Reusar utilidades de test del bot si aplica (Mocha/Chai ya están en el repo). + +### 16.3 Conciliación contra el bot +- Tomar un snapshot real (anonimizado) y comparar `Σ community.earnings` del bot con + el computado por el dashboard; documentar y explicar cualquier diferencia + (retiros, redondeos, órdenes `calculated=false`). + +### 16.4 Tiempo real +- Simular inserts/updates en `orders` y verificar que el delta llega al cliente y + que la caché se invalida correctamente. + +### 16.5 E2E / UI +- Flujos de filtro (cambiar rango/estado/moneda y ver consistencia). +- Pruebas de privacidad: usuarios con `show_username=false` no aparecen + identificados. + +--- + +## 17. Anexos + +### 17.1 Mapa campo → métrica (resumen rápido) + +| Pregunta del usuario | Fuente | Filtro / fórmula | +|----------------------|--------|------------------| +| Monedas con más operaciones exitosas | `orders` | `status=SUCCESS`, group by `fiat_code`, sort count desc (§7.3) | +| Estadísticas diarias / mensuales | `orders` | `$dateTrunc` por `day`/`month` (§7.2) | +| Por rango de tiempo elegido | `orders` | `created_at ∈ [from,to)` | +| Filtro por tipo de orden | `orders` | `type ∈ {buy,sell}` | +| Nº de canceladas en un periodo | `orders` | `status ∈ {CANCELED,CANCELED_BY_ADMIN}` (§7.4) | +| Órdenes activas | `orders` | snapshot estados "en progreso" (§7.5) | +| Monto entrante en el nodo | `orders` | `invoice_held_at` + estados con hold pagado (§7.6) | +| Monto saliente | `orders` | `status=SUCCESS`, `Σ amount` (§7.6) | +| Conciliación y ganancias | `orders`,`communities`,`pendingpayments` | §8 | +| Estado del nodo | `configs` | documento único | +| Salud de pagos salientes | `pendingpayments` | §7.7 | + +### 17.2 Enumeraciones de referencia + +- **Order.status:** `WAITING_PAYMENT, WAITING_BUYER_INVOICE, PENDING, ACTIVE, + FIAT_SENT, CLOSED, DISPUTE, CANCELED, SUCCESS, PAID_HOLD_INVOICE, + CANCELED_BY_ADMIN, EXPIRED, FROZEN, HOLD_INVOICE_EXPIRED`. +- **Order.type:** `buy, sell`. +- **Dispute.status:** `WAITING_FOR_SOLVER, IN_PROGRESS, SETTLED, SELLER_REFUNDED, + RELEASED`. +- **PendingPayment.last_error:** `TIMEOUT, ROUTING_FAILED, INSUFFICIENT_BALANCE, + AMOUNT_MISMATCH, UNKNOWN` (más `''` por defecto). +- **Fiat codes:** 97 monedas con `price:true` en `util/fiat.json` (USD, EUR, ARS, + VES, COP, BRL, MXN, …). + +### 17.3 Variables de entorno del bot relevantes para fórmulas + +| Var | Significado | Uso en dashboard | +|-----|-------------|------------------| +| `MAX_FEE` | Fee máximo del bot (decimal, p.ej. 0.002) | Snapshot en `order.bot_fee`; fallback en conciliación | +| `FEE_PERCENT` | % del fee que se queda el bot (p.ej. 0.7) | Snapshot en `order.community_fee`; fallback | +| `MAX_ROUTING_FEE` | Fee máx. de ruteo (p.ej. 0.001) | Contexto para validar `routing_fee` | +| `PENDING_PAYMENT_WINDOW` | Minutos entre reintentos | Interpretar latencia de pagos | +| `ORDER_PUBLISHED_EXPIRATION_WINDOW` | Expiración de publicadas | Explica borrado de `PENDING` | + +### 17.4 Riesgos y notas + +- **Replica set:** change streams lo requieren. Verificar el despliegue actual. +- **Borrado físico de `PENDING`:** las series históricas de órdenes "abiertas" están + sesgadas. Avisar en UI. +- **Redondeos:** el bot hace `round()` en fees/earnings; tolerar ±1 sat por orden en + conciliaciones. +- **`community_fee` mal nombrado:** representa el % del bot (`FEE_PERCENT`), no el de + la comunidad. No confundir al implementar §8. +- **Sin balances de wallet en DB:** liquidez real requiere LND read-only (§9.4). +- **PII:** respetar `show_username`/`show_volume_traded` y rol `admin` para finanzas. + +--- + +*Fin del documento.*