# 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: ```bash 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: ```json { "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`.