Resumen del sistema
Estado global de la plataforma Lyzard
Documentación del panel
Referencia completa de todas las secciones, funcionalidades y procesos del panel de administración. Mantén esta página actualizada cada vez que se añada algo nuevo.
Índice
- Visión general del panel
- Login, sesión y seguridad
- Resumen
- Tenants
- Funcionalidades por tenant (feature flags)
- Administradores
- Modelo canónico
- Sources
- Mapeos (panel inline en Sources)
- Staging logs
- Motor de staging (cómo se mueven los datos)
- Enlaces del bloque "Sistema"
- Arquitectura técnica resumida
- Cómo añadir una funcionalidad nueva al panel
Este panel (admin.lyzard.es) es el centro de operaciones del equipo Lyzard. NO es para clientes finales — los clientes acceden al dashboard en lyzard.es.
Desde aquí se gestionan tres cosas:
- Quién usa Lyzard — alta y baja de tenants (clientes) y administradores internos.
- Qué ve cada cliente — feature flags por tenant.
- Cómo se mueven sus datos — desde el ingestion (Airbyte) hasta el data warehouse canonicalizado.
El sidebar agrupa las secciones en cuatro bloques: General, Gestión, Data Warehouse y Sistema. Las dos primeras viven dentro del propio panel; las últimas dos abren herramientas externas (pgweb, Airbyte) o son secciones de operación.
Login
Entras con email + contraseña. Solo aceptan login los usuarios marcados como is_system_admin = true en la BBDD lyzard. Si un cliente con role owner intenta entrar, el panel le rechaza con "Acceso restringido al equipo de Lyzard".
Token JWT
Tras el login el backend devuelve un JWT firmado con JWT_SECRET (caduca en 8h). El token se guarda en sessionStorage del navegador con clave lyzard_admin_token y se envía en cada petición del panel como Authorization: Bearer ….
Cookie de sesión (HttpOnly)
Además del JWT, tras el login el panel llama a POST /api/admin-session y el backend establece una cookie HttpOnly llamada lyzard_admin_session con dominio .lyzard.es. Esta cookie es crítica: nginx la usa con auth_request para proteger /db/ y /dw/ (los visores de BBDD) sin pedir un segundo login.
Logout
Al pulsar el icono de salida se borra la cookie (DELETE /api/admin-session) y se elimina el token de sessionStorage. Si no haces logout, la cookie expira sola a las 8h.
HttpOnly, Secure y SameSite=Strict, pero conviene cerrar sesión en máquinas compartidas.Vista de aterrizaje. Muestra tres contadores globales:
- Tenants — total de filas en la tabla
tenants(incluye inactivos). - Usuarios — total en la tabla
users. - Admins — usuarios con
is_system_admin = true.
Datos en vivo desde GET /api/admin/stats. Se recargan cada vez que abres la página.
Un tenant es un cliente de Lyzard (un workspace aislado). Esta página tiene tres bloques.
4.1 Crear un tenant
Formulario con dos partes: el tenant en sí (nombre + slug) y el primer usuario owner dentro de él (email, contraseña, nombre opcional).
- Nombre — display name del workspace (ej.
Tienda García). - Slug — identificador URL-safe. Se autocompleta al escribir el nombre (sin acentos, solo a-z 0-9 y guiones). Si lo editas a mano, deja de autocompletar.
- Email del owner — único en todo el sistema. No puede repetirse en otro tenant.
- Contraseña — mínimo 8 caracteres. Se hashea con bcrypt cost 12.
- Nombre completo — opcional, solo para mostrar.
Llama a POST /api/tenants, que en una transacción crea la fila en tenants y la fila en users con role owner.
4.2 Lista y activación
Tabla con todos los tenants, su slug, el contador de usuarios y la fecha de creación. La columna Estado es un botón clicable: alterna entre Activo e Inactivo (PATCH /api/admin/tenants/:id). Un tenant inactivo no se elimina — solo se marca is_active = false y los usuarios de ese tenant no pueden hacer login.
4.3 Botón "Funcionalidades"
Abre el panel de feature flags para ese tenant (sección 5). El panel se muestra debajo de la tabla y se cierra con el botón "Cerrar".
Cada tenant ve solo las secciones del dashboard que tenga activadas. La lógica es por capas:
- La tabla
featurescontiene el catálogo global conenabled_by_default. - La tabla
tenant_featurescontiene overrides: solo se inserta una fila cuando el admin cambia el default. - Estado efectivo = override del tenant si existe, si no
enabled_by_default.
Cómo usar el panel
En la fila de un tenant, pulsa Funcionalidades. Aparecen toggles agrupados por categoría (Dashboard, etc.). Cada toggle hace PATCH /api/admin/tenants/:id/features/:key con UPSERT en tenant_features.
Cómo añadir una feature nueva al catálogo
Hoy se hace por SQL: INSERT INTO features (key, name, description, category, sort_order, enabled_by_default) VALUES (...). Convención de key: dashboard.<seccion>. El nombre y descripción aparecen tal cual en el toggle.
Lista los usuarios del equipo Lyzard (is_system_admin = true). Estos son los únicos que pueden entrar en este panel.
6.1 Crear admin
Formulario con email, nombre completo y contraseña. POST /api/admin/admins crea un usuario con role='owner', is_system_admin=true y tenant_id = el tenant del admin que lo crea (el "tenant interno" de Lyzard, no se ve en producto).
6.2 Lista
Email, nombre, workspace, último acceso y estado. Hoy no hay botón para desactivar/borrar admins — se haría manualmente vía SQL.
users.is_active = false o is_system_admin = false directamente en la BBDD.Define qué columnas tiene cada entidad estable del data warehouse (products, orders, customers, …). Es el contrato que verá el dashboard del cliente, independiente de qué source provee los datos.
7.1 Tabla maestra
Las filas viven en config.canonical_columns (en la BBDD lyzard_warehouse). Cada fila tiene entity, column_name, data_type (text/numeric/timestamp/boolean) y required.
7.2 Añadir una columna canónica
Formulario con entidad (autocompletada con las que ya existen, pero puedes escribir una nueva), nombre de columna, tipo y checkbox "Requerida". Al guardar:
- Inserta la fila en
config.canonical_columns. - Llama a
syncEntityTable(entity): sicore.<entity>no existía la crea con todas sus columnas +id,source_raw_id,sourceyUNIQUE(source_raw_id, source). Si ya existía, solo haceALTER TABLE ADD COLUMN. - La columna física aparece al instante en
core.<entity>, vacía.
Para que se rellene con datos hay que mapearla a alguna raw_column en alguna source y esperar al próximo run del motor (sección 11).
7.3 Eliminar una columna canónica
Borra la fila de config.canonical_columns pero NO borra la columna física en core.<entity>. Esto es a propósito: nunca perdemos histórico. La columna queda "huérfana": existe pero el motor deja de actualizarla.
7.4 Tipos válidos y mapeo a Postgres
| data_type | Tipo Postgres |
|---|---|
text | TEXT |
numeric | NUMERIC |
timestamp | TIMESTAMPTZ |
boolean | BOOLEAN |
data_type de una columna ya existente NO ejecuta ALTER COLUMN TYPE. Si necesitas cambiarlo, hazlo a mano en SQL o borra y re-crea la columna.Lista los schemas RAW de Airbyte detectados en lyzard_warehouse. Convención obligatoria de naming:
tenant_<tenant>_<source>_raw
Ejemplos: tenant_test_faker_raw, tenant_cocacola_woo_raw. Cualquier schema que no siga este patrón se ignora.
8.1 Botón "Detectar sources"
Llama a POST /api/admin/sources/detect. El backend busca con regex ^tenant_[^_]+_[^_]+_raw$, parsea cada nombre y hace UPSERT en config.sources. Cualquier source antigua cuyo schema haya desaparecido se marca active=false.
8.2 Tabla
Cada source detectada se muestra con su tenant, source, schema_name, fecha de detección y estado (Activa/Inactiva). El botón Configurar mapeos abre el panel inline (sección 9).
detectSources() antes de cada tick — no necesitas pulsar el botón si das tiempo.El panel se abre debajo de la tabla cuando pulsas "Configurar mapeos" en una source. Define cómo cada columna RAW se vierte a una columna canónica del core.
9.1 Selector de tabla y entidad
Eliges una tabla RAW (las que existan en el schema de la source) y una entidad canónica destino. Si la tabla ya tenía mapeos, la entidad se preselecciona automáticamente.
9.2 Tabla de columnas RAW
Para cada columna de la tabla RAW se muestra:
- Nombre de la columna RAW.
- Tipo origen (lo que dice
information_schema). - 3 ejemplos reales de la BBDD para que sepas qué contiene.
- Selector "→ Columna canónica": las opciones son las columnas canónicas de la entidad seleccionada. Por defecto va en "— No mapear —".
Las columnas _airbyte_* nunca aparecen en este selector.
9.3 Auto-inyección
El motor pone solo cuatro columnas, NO las mapees:
tenant_id← tomado del nombre del schema.source← tomado del nombre del schema.synced_at←_airbyte_extracted_at.source_raw_id←_airbyte_raw_id(clave de deduplicación).
9.4 Botones
- Guardar mapeos —
PUT /api/admin/sources/:id/mappings. Borra todos los mapeos existentes para (source, raw_table) y guarda el set actual. Las columnas en "No mapear" se omiten. - Ejecutar staging para esta source —
POST /api/admin/sources/:id/staging/run. Procesa todas las (raw_table, entity) de esta source ahora mismo, sin esperar al cron.
Histórico y estado del motor. Datos en vivo desde config.staging_runs.
10.1 Última sincronización por source
Tarjetas con la ejecución más reciente por (source, entidad). Útil para ver de un vistazo si algo lleva sin actualizarse.
10.2 Histórico
Tabla con las últimas 200 ejecuciones. Por cada una: inicio, source, entidad, filas procesadas/insertadas/actualizadas, duración y estado (success / error / running). Hover sobre el badge error muestra el mensaje completo.
10.3 Botón "Ejecutar staging ahora"
POST /api/admin/staging/run. Lanza el motor para TODAS las sources activas que tengan mapeos. Útil para no esperar al cron horario.
"Staging" es un proceso, no un schema. Convierte los datos crudos de Airbyte en tablas canónicas. Vive en private/src/services/staging/.
11.1 Pipeline
- Airbyte sincroniza fuentes externas →
tenant_<t>_<s>_raw.<tabla>. - El motor lee
config.column_mappings, agrupa por (source, raw_table, canonical_entity). - Por cada grupo genera UN SQL:
INSERT … SELECT … FROM raw … ON CONFLICT (source_raw_id, source) DO UPDATE SET …. - Filtra siempre:
WHERE _airbyte_meta->>'changes' = '[]'(descarta filas con errores de Airbyte). - Inserta/actualiza en
core.<entity>con casts a los tipos canónicos. - Registra cada ejecución en
config.staging_runscon filas procesadas/insertadas/actualizadas y duración.
11.2 Idempotencia
La clave UNIQUE(source_raw_id, source) permite ejecutar el motor mil veces sin duplicar nada. Las filas existentes se actualizan, las nuevas se insertan.
11.3 Cron
node-cron en el proceso de la API. Frecuencia controlada por la env var STAGING_CRON (default 0 * * * *, cada hora en :00, TZ Europe/Madrid). Si una tick aún corre cuando llega la siguiente, la nueva se salta para no solapar.
11.4 Triggers manuales
- Botón "Ejecutar staging ahora" en Staging logs (todas las sources).
- Botón "Ejecutar staging para esta source" en el panel de mapeos.
11.5 Cuándo se actualiza el core
Solo cuando el motor corre. Cambiar un mapping no actualiza el core hasta el próximo tick. Añadir una columna canónica SÍ refleja en el core inmediatamente (es ALTER TABLE, no datos), pero la columna queda vacía hasta que la mapees y corra el motor.
Los tres ítems del bloque Sistema abren herramientas externas en una pestaña nueva. NO son páginas internas del panel.
12.1 Base de datos (/db/)
Visor read-only de la BBDD operativa lyzard. Implementado con pgweb en 127.0.0.1:5433. nginx lo proxea bajo /db/ con auth_request que valida la cookie lyzard_admin_session. Sin sesión activa → 302 al login.
12.2 Warehouse (/dw/)
Igual que el anterior pero apuntando a lyzard_warehouse (pgweb en :5434, prefijo /dw/). Aquí ves los schemas RAW de Airbyte, config.* y core.*.
12.3 Airbyte (airbyte.lyzard.es)
UI propia de Airbyte (versión 0.63.14 fijada). Aquí se configuran sources, destinations y connections. La autenticación reutiliza la cookie lyzard_admin_session de este panel — no pide segundo login.
12.4 Metabase (metabase.lyzard.es)
UI de Metabase (versión 0.52.13). Es la herramienta de Business Intelligence — aquí construyes dashboards y queries sobre los datos canonicalizados en core.* (o sobre cualquier otra BBDD que conectes desde la propia UI).
- Dónde corre: docker compose en
/opt/metabase/.network_mode: hostcon bind127.0.0.1:3020— nunca expuesto al WAN directamente. - Internal store: Postgres del propio host, BBDD
metabase_internal, usuariometabase_user. NO usa H2 — toda la configuración (dashboards, queries, usuarios de Metabase) persiste en Postgres. - Auth: el subrequest a
/api/admin-sessionprotege el acceso ANTES de llegar a la UI de Metabase. Sin cookie de admin Lyzard → 302 aadmin.lyzard.es. Después Metabase pide su propio login interno la primera vez (paso de setup). Ese login interno se gestiona desde la UI de Metabase, no desde aquí. - Recursos:
JAVA_OPTS=-Xmx512m. Si las queries son lentas, subir gradualmente vigilando memoria del VPS. - Conexión a las BBDD del proyecto: al añadir una database en Metabase usar
Host: 127.0.0.1(porque está en network_mode: host), puerto 5432, y la BBDD que toque (lyzard_warehousees la útil para BI; idealmente con un usuario de solo lectura).
MB_ENCRYPTION_SECRET_KEY en /opt/metabase/.env cifra las contraseñas de las DBs que Metabase guarda. Si se pierde, Metabase no puede leer las credenciales y hay que reconfigurar todas las conexiones a mano. Backup de ese fichero junto con los demás secretos.- Frontend admin: HTML estático en
/var/www/lyzard-admin/index.html. Sin build step, vanilla JS. - API: Node 18 + Express en
/var/www/lyzard/private/, escucha en127.0.0.1:3010, proxiada por nginx. Arrancada víastart.sh(nodemon). - Postgres: una instancia, dos BBDD:
lyzard(operativa) ylyzard_warehouse(DW + staging). - Pools de conexión:
db/pool.js→lyzard;db/warehousePool.js→lyzard_warehouse. - Auth: JWT (8h) + cookie HttpOnly. Middleware
requireAuth+requireSystemAdmin. - Logs: Winston a
private/logs/combined.logyprivate/logs/error.log.
Endpoints actuales del panel
| Método | Ruta | Función |
|---|---|---|
| GET | /api/admin/stats | Contadores del Resumen |
| GET / PATCH | /api/admin/tenants[/:id] | Listar / activar tenants |
| POST | /api/tenants | Crear tenant + owner |
| GET / PATCH | /api/admin/tenants/:id/features[/:key] | Feature flags por tenant |
| GET | /api/admin/features | Catálogo global de features |
| GET / POST | /api/admin/admins | Listar / crear administradores |
| GET / POST / DELETE | /api/admin/canonical-columns | Modelo canónico |
| POST | /api/admin/core/sync | Re-sincroniza todas las core.<entity> |
| GET / POST | /api/admin/sources[/detect] | Listar / detectar sources |
| GET | /api/admin/sources/:id/raw-tables | Tablas dentro de un schema RAW |
| GET | /api/admin/sources/:id/raw-tables/:table/preview | Columnas + 3 filas reales |
| GET / PUT | /api/admin/sources/:id/mappings | Mapeos por source |
| POST | /api/admin/staging/run | Ejecutar motor (todas) |
| POST | /api/admin/sources/:id/staging/run | Ejecutar motor (una source) |
| GET | /api/admin/staging/runs | Histórico de ejecuciones |
| POST / GET / DELETE | /api/admin-session | Cookie HttpOnly para nginx auth_request |
Cada vez que añadas algo, sigue este checklist:
- Implementa la lógica (backend + frontend).
- Si requiere un toggle por tenant, registra la feature en la tabla
features(ver sección 5). - Documenta en esta misma página: añade una sección nueva al final con su ID anclado, su entrada en el índice y un explainer en el mismo tono que las demás (qué hace, cómo se usa, gotchas).
- Si tocaste schemas
config.*ocore.*, añade migración enprivate/src/db/migrations/y ejecútala contralyzard_warehouse. - Si añadiste un nuevo endpoint, agrégalo a la tabla de la sección 13.
Tenants
Gestión de workspaces de clientes
| Nombre | Slug | Usuarios | Creado | Estado | |
|---|---|---|---|---|---|
Cargando... | |||||
Administradores
Usuarios con acceso al panel de administración
| Nombre | Workspace | Último acceso | Estado | |
|---|---|---|---|---|
Cargando... | ||||
Modelo canónico del core
Define qué columnas tiene cada entidad del data warehouse. Al añadir una nueva, la tabla core.{entidad} se actualiza automáticamente.
Sources
Sources detectadas automáticamente desde los schemas de Airbyte (tenant_{tenant}_{source}_raw).
| Tenant | Source | Schema | Detectada | Estado | |
|---|---|---|---|---|---|
Cargando... | |||||
Staging logs
Histórico de ejecuciones del motor de staging.
| Inicio | Source | Entidad | Filas | Insertadas | Actualizadas | Duración | Estado |
|---|---|---|---|---|---|---|---|
Cargando... | |||||||