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>
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/datasolo 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 conceptualplatform_admin.iam.users= usuarios de obra. Rolestenant_adminyuser.
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_SECUREen HTTPS. GET /v1/auth/me,POST /v1/auth/logout,POST /v1/auth/change-password.- Alternativa: header
X-API-Key+X-Tenant-Idobligatorio (no ve todos los tenants). web-panelrechazarealm === "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:kindprincipal|sub,parent_id, datos fiscales IMSS/RFC,tenant_id.projects: obra. Statusactivo|pausado|concluido|cancelado. Tema de gafete, logos, contrato, SIROC,%impuesto nómina, empresa dueña.
Padrón (persona maestra)
Invariantes (docs/padron-v2.md):
- No se borra persona:
status=baja. - Solo asignar/importar a proyectos activos.
- Asignación repetida se reactiva; al finalizar se guarda
end_date. Unique(worker_id, project_id). pipeline_statusno es editable. Baja es manual; el resto se recalcula.- CURP, RFC, NSS únicos (
citextCURP/RFC). - Expediente obligatorio: foto, INE, CURP, NSS/IMSS, RFC.
- 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)
- No SQLite. Postgres + Redis + R2.
- No JOIN/FK
iam↔coreni platform ↔ product. Snapshots desnormalizados (uploaded_by, etc.). - Schema nuevo = changeset Liquibase nuevo. Nunca editar changeset ya aplicado en ambiente compartido.
- Negocio nuevo en
core.fn_*+callCoreFn. Envelope + mensaje en español específico. - Rutas
core:...requireCoreAuthpara RLS. - Documentos: cifrar; no guardar plaintext en disco/R2.
- Padrón: no DELETE de personas; pipeline derivado; unicidad CURP/RFC/NSS.
- Coolify: solo
docker-compose.yml. Overlay local no va a prod. - Secrets:
.envgitignored.DOCS_KEY64 hex. Fail-fast en production si faltan secretos. - Fronts: SPA + PrimeVue; proxy
/v1; no llamar la API cross-origin en prod. - Tests API:
cd api && deno task check && deno task test. Front:npm run buildenweb-panel/web-saas. 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.