panels-origin/docs/contexto-sistema.md
Cursor Agent 69bcd297f8
docs: contexto del sistema PANELS para agentes
Añade un documento copiable con arquitectura, contratos RPC, módulos
de negocio, rutas y reglas de no-romper según el código actual.

Co-authored-by: alberto.martinez <alberto.martinez@mrdev.mx>
2026-09-08 16:30:35 +00:00

12 KiB

PANELS — contexto del sistema (estado actual)

Documento para pegar en reglas / contexto de un agente. Describe el producto como está en el código hoy, no un roadmap.

Producto: PANELS (repo panels, paquete raíz panel-obra). Escritorio de obra para constructoras (México): padrón, obras, presupuesto, programa de obra, costos, almacén, gafetes, nómina. Multi-tenant. Consola SaaS aparte para operadores de plataforma.


1. Mapa de repos / carpetas

Ruta Qué es
api/ API Deno 2 + Hono. Punto de entrada api/main.ts. Puerto 8000. Prefijo /v1.
web-panel/ Escritorio de obra. Nuxt 3 SPA (ssr: false), PrimeVue 4, puerto 3000.
web-saas/ Consola operadores. Nuxt 3 SPA, mismo stack, puerto 3001.
web/ Reservada para website pública. No es el producto.
db/ Liquibase: db/platform, db/iam, db/core. Provision: db/provision/.
docs/ Ops (coolify.md), padrón (padron-v2.md), este contexto.
overlays/compose.local.yml Solo local: Postgres + Redis + provision. No usar en Coolify.
docker-compose.yml Producción: migrate → api → web-panel + web-saas.

Fronts (dev y nginx) proxifican /v1 a la API. Cookies same-origin. En prod la API no lleva FQDN propio.


2. Stack y runtime

  • API: Deno 2.9, Hono 4, xlsx, pdf-lib, qrcode. Sin FFI. Sin Java en el contenedor de tráfico.
  • Migraciones: Liquibase + JRE 21 en imagen migrate (un shot por deploy). No corren al arrancar la API.
  • Fronts: Nuxt 3.16, Vue 3, PrimeVue 4, PrimeIcons, Chart.js (panel). Build → nginx Alpine.
  • Postgres 16 + Redis 7. Object storage S3-compatible (Cloudflare R2). Fallback disco /app/data solo local.
  • CI: GitHub Actions — provision + Liquibase + aislamiento + deno check/deno test + build de ambos fronts.

Dev local:

npm run dev:api    # api
npm run dev:web    # web-panel :3000
npm run dev:saas   # web-saas :3001
# Docker todo-en-uno:
docker compose -f docker-compose.yml -f overlays/compose.local.yml up --build

3. Arquitectura de datos (monolito modular)

Una instancia Postgres por ambiente, dos bases. Sin FK entre bases ni entre esquemas iam y core. tenant_id es referencia lógica validada en aplicación.

Base Esquema Changelog Contenido
panels_platform public db/platform/ Control plane: tenants, platform_users, smtp_settings. Solo rutas /v1/saas/*.
panels_product iam db/iam/ Usuarios/roles/permisos del tenant (quien entra a web-panel).
panels_product core db/core/ Negocio: empresas, obras, personal, docs, presupuesto, nómina, gafetes, gastos, almacén, programa de obra.

Dos identidades distintas:

  • platform_users = operadores PANELS (web-saas). Rol conceptual platform_admin.
  • iam.users = usuarios de obra. Roles tenant_admin y user.

Roles Postgres: panels_{platform,iam,core}_{owner,app}. Runtime usa _app. Liquibase/bootstrap usan _owner. Login por username usa DATABASE_URL_IAM_OWNER a propósito (bypassa RLS).

Redis ACL: panels_iam_redis (sesiones) y panels_core_redis (cache).

RLS en core e iam: el middleware withCoreScope (api/scope.ts) fija app.tenant_id en la transacción. Sin eso, políticas fail-closed devuelven 0 filas.

Liquibase: siempre pasar --context-filter. context="dev" = demo (tenant ARCTEC ARCT2608). Producción: --context-filter=!dev. Sin filtro, Liquibase corre todo, incluido demo.


4. Contrato RPC (capa de negocio)

Lógica de core (y parte de iam) vive en funciones Postgres core.fn_* / iam.fn_*. La API no debe db.prepare() contra tablas de negocio en rutas nuevas; solo api/rpc.ts (callCoreFn → SELECT fn($1::jsonb)).

Envelope JSON de toda función:

{
  "ok": true,
  "code": "OK",
  "layer": "db",
  "message": "Mensaje específico en español",
  "context": { "fn": "fn_..." },
  "data": {},
  "errors": null
}

Códigos: OK, CREATED, VALIDATION, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, CONFLICT, INTERNAL, NETWORK.
api/http_errors.ts mapea a HTTP y añade status. layer puede ser db | api | front. Mensajes genéricos están prohibidos.

Convenciones SQL: un parámetro payload jsonb, SECURITY INVOKER, SET search_path = core (o iam), GRANT EXECUTE al rol _app en el mismo changeset. Changesets core RPC: 006 (infra) … 030 (programa de obra).


5. Auth, sesión, permisos

  • Login POST /v1/auth/login. Sesión en Redis IAM, cookie. COOKIE_SECURE en HTTPS.
  • GET /v1/auth/me, POST /v1/auth/logout, POST /v1/auth/change-password.
  • Alternativa: header X-API-Key + X-Tenant-Id obligatorio (no ve todos los tenants).
  • web-panel rechaza realm === "platform" y manda al SaaS.
  • Primer admin: api/scripts/bootstrap-admin.ts + SEED_PASSWORD (no seed de hash en Liquibase).

Permisos (iam.permissions):

Código Uso
manage_workers Padrón / expediente
manage_projects Obras
manage_companies Empresas
manage_budget Presupuesto
manage_payroll Nómina / préstamos
manage_documents Documentos
manage_users Usuarios del tenant
manage_settings Config tenant
view_reports Tableros
view_expenses / manage_expenses Gastos y control de costos
view_warehouse / manage_warehouse / close_warehouse Almacén
manage_cost_settings Umbrales presupuestales

tenant_admin tiene todos. user de fábrica: workers, documents, reports, view_expenses, view_warehouse. tenant_admin no se puede recortar por API.

Granularidad: gastos/almacén/costos ya chequean permiso; el resto de rutas core a menudo solo requireCoreAuth. Evolucionar permisos de forma incremental.


6. Dominio de negocio (core)

Empresas y obras

  • companies: kind principal | sub, parent_id, datos fiscales IMSS/RFC, tenant_id.
  • projects: obra. Status activo | pausado | concluido | cancelado. Tema de gafete, logos, contrato, SIROC, % impuesto nómina, empresa dueña.

Padrón (persona maestra)

Invariantes (docs/padron-v2.md):

  1. No se borra persona: status=baja.
  2. Solo asignar/importar a proyectos activos.
  3. Asignación repetida se reactiva; al finalizar se guarda end_date. Unique (worker_id, project_id).
  4. pipeline_status no es editable. Baja es manual; el resto se recalcula.
  5. CURP, RFC, NSS únicos (citext CURP/RFC).
  6. Expediente obligatorio: foto, INE, CURP, NSS/IMSS, RFC.
  7. Color de gafete = código exacto de risk_levels; Alto/Medio/Bajo es UI.

Campos clave worker: nombres, hire_type, work_type N (jornal) | D (destajo), daily_wage, needs_badge, imss_status (sin_alta | alta | baja_imss), empresa IMSS.

Validación MX en api/mx.ts. Import Excel: template + POST /v1/workers/import.

Documentos cifrados AES-GCM (DOCS_KEY = 64 hex). Storage key + iv en tablas; bytes en R2. Tipos con validity_mode none | freshness | expiry. Mismo patrón para docs de proyecto y empresa.

Gafetes

Temas, jobs PDF, QR/vCard (VCARD_BASE). Pipeline “listo para gafete” derivado. Kanban visualiza ese pipeline.

Presupuesto

Capítulos + partidas (budget_chapters / budget_items). Import/export Excel, preview. Ligado a proyecto.

Programa de obra

Un programa por proyecto. Periodos semanales. Slots de concepto, montos por partida, avance. Import, wizard, recálculo vs costo. Export Excel.

Gastos y control de costos

expense_entries + adjuntos, IVA por periodo, tenant_cost_settings. Control: summary / items / chapters / deviations vs presupuesto.

Almacén

Materiales, almacén central y por obra, stock, movimientos, transferencias. Abrir/cerrar almacén de obra. Links material ↔ presupuesto.

Nómina

Dos líneas históricas:

  • Semanas (payroll_weeks, sheets, lines): asistencia, assemble/pay, CSV, jornal, líneas admin, destajo (unidades, jobs, cortes).
  • Periodos (payroll_periods / payroll_lines) y settings.
  • Préstamos (loans / loan_payments) + PDFs de recibo.
  • Sync de costo de nómina hacia control de costos (025-rpc-payroll-cost-sync).

7. API HTTP (resumen)

Prefijo /v1. CORS credenciales; origins locales 3000/3001 + CORS_ORIGINS.

Área Rutas
Salud GET /health — core, platform, redis.iam, redis.core, storage
Auth /auth/login, logout, me, change-password
SaaS /saas/tenants, SMTP get/put/test, send-access
Catálogos / config /catalogs, /configuracion
Empresas CRUD + documentos
Proyectos CRUD + documentos + budget nested
Workers list/get/create/patch, validate, pipeline, assign, import, documents, photo
Gafetes /badge-qr, /projects/:id/badge-jobs, /badge-jobs
Nómina /attendance, /payroll/*, /destajo/*, /loans
Gastos /expenses, /iva/summary, /cost-settings
Costos /projects/:id/cost-control/{summary,items,chapters,deviations}
Almacén /warehouses, stock, movements, entries, exits, transfers, open/close
Programa /projects/:id/work-program (+ concepts, partidas, vs-cost, import, generate, export, patches de periodos)
IAM /iam/users, /iam/permissions, /iam/roles/:role/permissions, /me/permissions

Handlers: main.ts + payroll_http.ts, expenses_http.ts, warehouse_http.ts, cost_control_http.ts, work_program_http.ts, iam_http.ts.


8. Fronts

web-panel — escritorio

UI tipo escritorio: layout default.vue (cinta, árbol, detalle, statusbar). Estado compartido useDesktop(). Tema claro/oscuro (panel-theme). Auth global; must_change_password → /cambiar-password.

Menú:

Ruta Módulo
/ Inicio
/proyectos Obras / empresas
/presupuesto Presupuesto
/programa-obra Programa de obra
/control-presupuesto Control de costos
/gastos Gastos
/almacen Almacén
/padron, /padron/:id Padrón + ficha
/kanban Pipeline de personal
/gafetes Impresión de gafetes
/nomina Nómina
/usuarios IAM del tenant
/configuracion Config (menú perfil)
/login Login

También existe /obras.vue en pages (legado / no está en el menú principal).

Cliente HTTP: composables/useApi.ts + envelopes en api-response.ts. Proxy Nitro /v1 → 127.0.0.1:8000.

web-saas

Rutas: /login, / (tenants), /smtp. Misma API. No comparte UI con el escritorio.


9. Reglas para agentes (no romper)

  1. No SQLite. Postgres + Redis + R2.
  2. No JOIN/FK iam ↔ core ni platform ↔ product. Snapshots desnormalizados (uploaded_by, etc.).
  3. Schema nuevo = changeset Liquibase nuevo. Nunca editar changeset ya aplicado en ambiente compartido.
  4. Negocio nuevo en core.fn_* + callCoreFn. Envelope + mensaje en español específico.
  5. Rutas core: ...requireCoreAuth para RLS.
  6. Documentos: cifrar; no guardar plaintext en disco/R2.
  7. Padrón: no DELETE de personas; pipeline derivado; unicidad CURP/RFC/NSS.
  8. Coolify: solo docker-compose.yml. Overlay local no va a prod.
  9. Secrets: .env gitignored. DOCS_KEY 64 hex. Fail-fast en production si faltan secretos.
  10. Fronts: SPA + PrimeVue; proxy /v1; no llamar la API cross-origin en prod.
  11. Tests API: cd api && deno task check && deno task test. Front: npm run build en web-panel / web-saas.
  12. web/ no es el panel. Producto = web-panel + web-saas + api.

10. Deploy (Coolify)

Por ambiente: 1 Postgres (2 bases), 1 Redis (2 ACL), 1 bucket R2, 4 contenedores (migrate, api, web-panel, web-saas). Red Docker coolify para hostnames internos UUID (no container id de 12 chars).

Health: GET /v1/health debe reportar core, platform, redis.iam, redis.core y storage (backend: "s3" en prod). Detalle ops: docs/coolify.md, db/README.md, db/provision/README.md.