# Auditoría de la base de datos — FASE 0

Fecha: 15 de septiembre de 2026. Estado: análisis terminado; FASE 1 pendiente de aprobación expresa.

## 1. Alcance y evidencia

Se leyó el archivo completo, incluidos DDL, INSERT, metadatos de formularios, configuración, historial de migraciones y datos JSON. El SQL se analizó como datos: no se ejecutaron sus instrucciones. Las capturas 1–8 se usaron como referencia funcional y visual; no se accedió al sitio mostrado ni a los servidores multimedia.

Único entregable del proyecto en esta fase: este documento. No se creó una aplicación, modelos ejecutables, migraciones ni una base de datos. No se modificó el respaldo.

| Dato | Resultado |
|---|---|
| Archivo | `I:/drm/backup-2026-09-15_19-58-57.sql` |
| Tamaño | 252278 bytes |
| SHA-256 | `717a477464363fabb42432d7afedeaefd397793cdf38c58aad3f32dfe9d84520` |
| Motor de origen | MySQL 8.0.46-0ubuntu0.24.04.4, según cabecera |
| Base indicada por el dump | `pedro` |
| Tablas / columnas | 49 / 446 |
| Índices, incluyendo PRIMARY y UNIQUE | 93 |
| Foreign keys declaradas | 27 |
| INSERT / filas decodificadas | 28 / 1377 |
| Almacenamiento | Todas las tablas InnoDB, utf8mb4_unicode_ci |

Los recuentos salen de las tuplas INSERT, no de AUTO_INCREMENT. Se comprobó que cada tupla tiene exactamente las columnas declaradas. No se detectaron valores duplicados en las claves PRIMARY/UNIQUE con comparación literal; esto no sustituye la comparación de collation de MySQL ni una restauración real. Las 27 FK declaradas no presentan huérfanos en los datos suministrados. Sí hay referencias lógicas huérfanas, documentadas más abajo.

No hay vistas, triggers, procedimientos, funciones ni eventos definidos en el archivo. Hay DROP TABLE IF EXISTS, CREATE TABLE, LOCK/UNLOCK, INSERT y directivas de sesión/ALTER de mantenimiento del dump. En una futura restauración debe usarse una base aislada: el propio archivo elimina las tablas homónimas y desactiva temporalmente las comprobaciones de FK. No se ha probado todavía la importación en MySQL/MariaDB.

## 2. Hallazgos principales

1. Es una aplicación Laravel con metadatos de Voyager: aparecen clases TCG\Voyager en data_types y migraciones específicas. La lógica PHP original no está en el respaldo.
2. Se pueden reutilizar las 49 tablas preservando nombres, IDs y columnas. Las convenciones de Eloquent requieren ajustes explícitos, especialmente para Jellyfin/Emby, pivots y tablas del sistema.
3. Hay 6 clientes: 2 Plex, 1 Jellyfin y 3 Emby. Coincide con el resumen visual. Hay 8 usuarios, 5 roles y 6 servidores registrados (3 Plex, 2 Jellyfin, 1 Emby). La tarjeta de 6 vendedores no equivale al total de usuarios: su filtro exacto requiere reconstrucción.
4. Las tres tablas de demos, las tres de paquetes, server_libraries y sessions están vacías. No es posible deducir formatos reales de bibliotecas de paquetes ni sesiones en vivo a partir de esas tablas vacías.
5. sessions es de Plex. notifications es un formato propio. No deben reutilizarse como tablas estándar de sesión web o notificación Laravel.
6. Los estados de cliente Jellyfin/Emby son enteros 0/1/2 según los metadatos BREAD, aunque SQL diga tinyint(1). No aplicar cast booleano. Plex conserva active/inactive/grace; BREAD etiqueta grace como «Caducado» y esa etiqueta no prueba una política de gracia concreta.
7. users.service contiene JSON doblemente codificado en los 8 registros. Un cast array simple no produce la lista final.
8. Se encontraron contraseñas de clientes legibles, tokens y secretos anidados. Este informe no reproduce sus valores, ni correos, IP de clientes o URLs privadas.
9. Las FK no cubren muchas relaciones críticas. Existe un padre ausente, tres solicitudes con autores ausentes y 51 movimientos Emby con parent_user_id ausente.
10. Los saldos users.total_credits no son SUM(credits.qty). El histórico mezcla asignaciones y movimientos; no reconstruir balances con una suma simple ni inferir tarifas desde los nombres de campos.

## 3. Inventario completo y clasificación

La categoría es primaria; solicitudes/tickets son transversales y movimientos también son logs. No existe una tabla separada llamada ventas: la venta identificada está en plans y credit_purchases.

| Tabla | Categoría | Filas | Modelo propuesto | Finalidad |
|---|---|---:|---|---|
| `channels` | Emby | 0 | `Channel` | Canales asociados a un servidor Emby; URLs y logos externos. |
| `content_requests` | Otros | 3 | `ContentRequest` | Solicitudes internas/públicas de películas y series para las tres plataformas. |
| `credit_purchases` | Ventas | 0 | `CreditPurchase` | Compras de planes de créditos y respuesta del proveedor de pago. |
| `credits` | Créditos | 8 | `Credit` | Registros de asignación de créditos entre usuarios; no representan por sí solos el saldo. |
| `customers` | Plex | 2 | `Customer` | Clientes Plex, credenciales, invitación, vigencia y creador. |
| `data_rows` | Sistema | 256 | `LegacyDataRow` | Metadatos BREAD de Voyager: campos, validaciones y relaciones de formularios. |
| `data_types` | Sistema | 21 | `LegacyDataType` | Recursos BREAD, modelos, controladores, políticas y scopes declarados. |
| `demos` | Plex | 0 | `Demo` | Accesos temporales Plex, horas, inicio y fin. |
| `domains` | Configuración | 1 | `Domain` | Dominios para generación de correos de cuentas y demos; relación funcional, no FK. |
| `durations` | Configuración | 14 | `Duration` | Duraciones en meses, coste, servicio, pantallas y recargo de TV. |
| `emby_server_user` | Emby | 0 | `EmbyServerUser (Pivot)` | Asignación de servidores Emby a usuarios del panel. |
| `embycustomers` | Emby | 3 | `EmbyCustomer` | Clientes Emby, vigencia, política y datos remotos. |
| `embydemos` | Emby | 0 | `EmbyDemo` | Demos Emby por horas. |
| `embymovements` | Emby | 226 | `EmbyMovement` | Historial de operaciones Emby; también categoría Logs. |
| `embypackages` | Emby | 0 | `EmbyPackage` | Paquetes de bibliotecas de un servidor Emby. |
| `embyservers` | Emby | 1 | `EmbyServer` | Conexión API Emby, estado y configuración de acceso. |
| `failed_jobs` | Logs | 0 | `FailedJob (infraestructura)` | Fallos de cola; payload y excepción potencialmente sensibles. |
| `jellyfin_server_user` | Jellyfin | 0 | `JellyfinServerUser (Pivot)` | Asignación de servidores Jellyfin a usuarios del panel. |
| `jellyfincustomers` | Jellyfin | 1 | `JellyfinCustomer` | Clientes Jellyfin, vigencia, política y datos remotos. |
| `jellyfindemos` | Jellyfin | 0 | `JellyfinDemo` | Demos Jellyfin por horas. |
| `jellyfinmovements` | Jellyfin | 2 | `JellyfinMovement` | Historial de operaciones Jellyfin; también categoría Logs. |
| `jellyfinpackages` | Jellyfin | 0 | `JellyfinPackage` | Paquetes de bibliotecas de un servidor Jellyfin. |
| `jellyfinservers` | Jellyfin | 2 | `JellyfinServer` | Conexión API Jellyfin, estado y configuración de acceso. |
| `jobs` | Sistema | 0 | `QueuedJob (infraestructura)` | Cola persistente; timestamps UNIX y payload serializado. |
| `logins` | Logs | 8 | `LoginAttempt` | Historial de acceso, IP, intentos y passcode. |
| `menu_items` | Sistema | 43 | `LegacyMenuItem` | Entradas y jerarquía de navegación Voyager. |
| `menus` | Sistema | 1 | `LegacyMenu` | Contenedores de menús Voyager. |
| `migrations` | Sistema | 175 | `MigrationRecord (infraestructura)` | Registro de migraciones ya aplicadas; no contiene su implementación. |
| `movements` | Plex | 52 | `Movement` | Historial de operaciones Plex; también categoría Logs. |
| `notifications` | Notificaciones | 0 | `PanelNotification` | Avisos propios, contenido, caducidad, lecturas y repetición. |
| `packages` | Plex | 0 | `Package` | Paquetes de bibliotecas de un servidor Plex. |
| `password_resets` | Usuarios | 0 | `PasswordReset (repositorio)` | Tokens de recuperación; sin clave primaria, gestionar con el broker. |
| `permission_role` | Usuarios | 308 | `PermissionRole (Pivot)` | Permisos asignados a roles, clave primaria compuesta. |
| `permissions` | Usuarios | 116 | `Permission` | Catálogo de permisos por clave y recurso. |
| `personal_access_tokens` | Usuarios | 0 | `PersonalAccessToken` | Tokens personales y propietario polimórfico. |
| `plans` | Ventas | 0 | `Plan` | Planes comerciales: precio y cantidad de créditos. |
| `proxies` | Configuración | 0 | `Proxy` | Direcciones y puertos de proxies heredados. |
| `roles` | Usuarios | 5 | `Role` | Roles heredados del panel. |
| `server_libraries` | Plex | 0 | `ServerLibrary` | Identificadores de bibliotecas remotas Plex por servidor. |
| `servers` | Plex | 3 | `Server` | Servidores Plex, token, capacidad, permisos y respuesta remota. |
| `sessions` | Plex | 0 | `PlexSession` | Registro mínimo de sesiones Plex; no son sesiones de autenticación web. |
| `settings` | Configuración | 110 | `Setting` | Parámetros por clave: presentación, seguridad, Telegram, pagos y plataformas. |
| `ticket_categories` | Otros | 4 | `TicketCategory` | Categorías de soporte. |
| `ticket_messages` | Otros | 2 | `TicketMessage` | Mensajes y adjuntos de tickets. |
| `tickets` | Otros | 1 | `Ticket` | Tickets de soporte con autor polimórfico y plataforma. |
| `translations` | Sistema | 0 | `LegacyTranslation` | Traducciones identificadas por tabla, columna, registro e idioma. |
| `user_roles` | Usuarios | 0 | `UserRole (Pivot)` | Roles adicionales de usuarios; clave primaria compuesta. |
| `user_servers` | Plex | 1 | `UserServer (Pivot)` | Asignación de servidores Plex a usuarios del panel. |
| `users` | Usuarios | 8 | `User` | Identidades del panel, rol principal, jerarquía, saldo y servicios. |

### Agrupación por categoría

- **Plex:** `customers`, `demos`, `movements`, `packages`, `server_libraries`, `servers`, `sessions`, `user_servers`.
- **Jellyfin:** `jellyfin_server_user`, `jellyfincustomers`, `jellyfindemos`, `jellyfinmovements`, `jellyfinpackages`, `jellyfinservers`.
- **Emby:** `channels`, `emby_server_user`, `embycustomers`, `embydemos`, `embymovements`, `embypackages`, `embyservers`.
- **Usuarios:** `password_resets`, `permission_role`, `permissions`, `personal_access_tokens`, `roles`, `user_roles`, `users`.
- **Créditos:** `credits`.
- **Ventas:** `credit_purchases`, `plans`.
- **Sistema:** `data_rows`, `data_types`, `jobs`, `menu_items`, `menus`, `migrations`, `translations`.
- **Configuración:** `domains`, `durations`, `proxies`, `settings`.
- **Logs:** `failed_jobs`, `logins`.
- **Notificaciones:** `notifications`.
- **Otros:** `content_requests`, `ticket_categories`, `ticket_messages`, `tickets`.

## 4. Relaciones Eloquent y tablas pivot

Cada FK simple del catálogo detallado propone belongsTo(modelo destino, columna local, columna destino), y su inversa hasMany(modelo origen, columna local, columna destino). Ninguna FK simple tiene unicidad suficiente para inferir hasOne. La columna explícita es obligatoria cuando difiere de la convención. Las siguientes relaciones incluyen también referencias lógicas sin FK; no se propone crearlas físicamente en esta fase.

| Origen | Destino | Evidencia | Propuesta |
|---|---|---|---|
| `channels.embyserver_id` | `embyservers.id` | FK declarada | belongsTo(EmbyServer, `embyserver_id`, `id`); inversa hasMany |
| `credit_purchases.plan_id` | `plans.id` | FK declarada | belongsTo(Plan, `plan_id`, `id`); inversa hasMany |
| `credit_purchases.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `credits.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `customers.duration_id` | `durations.id` | FK declarada | belongsTo(Duration, `duration_id`, `id`); inversa hasMany |
| `customers.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `data_rows.data_type_id` | `data_types.id` | FK declarada | belongsTo(LegacyDataType, `data_type_id`, `id`); inversa hasMany |
| `demos.server_id` | `servers.id` | FK declarada | belongsTo(Server, `server_id`, `id`); inversa hasMany |
| `emby_server_user.emby_server_id` | `embyservers.id` | FK declarada | belongsTo(EmbyServer, `emby_server_id`, `id`); inversa hasMany |
| `emby_server_user.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `embypackages.embyserver_id` | `embyservers.id` | FK declarada | belongsTo(EmbyServer, `embyserver_id`, `id`); inversa hasMany |
| `jellyfin_server_user.jellyfin_server_id` | `jellyfinservers.id` | FK declarada | belongsTo(JellyfinServer, `jellyfin_server_id`, `id`); inversa hasMany |
| `jellyfin_server_user.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `jellyfinpackages.jellyfinserver_id` | `jellyfinservers.id` | FK declarada | belongsTo(JellyfinServer, `jellyfinserver_id`, `id`); inversa hasMany |
| `logins.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `menu_items.menu_id` | `menus.id` | FK declarada | belongsTo(LegacyMenu, `menu_id`, `id`); inversa hasMany |
| `packages.server_id` | `servers.id` | FK declarada | belongsTo(Server, `server_id`, `id`); inversa hasMany |
| `permission_role.permission_id` | `permissions.id` | FK declarada | belongsTo(Permission, `permission_id`, `id`); inversa hasMany |
| `permission_role.role_id` | `roles.id` | FK declarada | belongsTo(Role, `role_id`, `id`); inversa hasMany |
| `server_libraries.server_id` | `servers.id` | FK declarada | belongsTo(Server, `server_id`, `id`); inversa hasMany |
| `ticket_messages.ticket_id` | `tickets.id` | FK declarada | belongsTo(Ticket, `ticket_id`, `id`); inversa hasMany |
| `tickets.ticket_category_id` | `ticket_categories.id` | FK declarada | belongsTo(TicketCategory, `ticket_category_id`, `id`); inversa hasMany |
| `user_roles.role_id` | `roles.id` | FK declarada | belongsTo(Role, `role_id`, `id`); inversa hasMany |
| `user_roles.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `user_servers.server_id` | `servers.id` | FK declarada | belongsTo(Server, `server_id`, `id`); inversa hasMany |
| `user_servers.user_id` | `users.id` | FK declarada | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `users.role_id` | `roles.id` | FK declarada | belongsTo(Role, `role_id`, `id`); inversa hasMany |
| `customers.package_id` | `packages.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Package, `package_id`, `id`); inversa hasMany |
| `customers.server_id` | `servers.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Server, `server_id`, `id`); inversa hasMany |
| `demos.package_id` | `packages.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Package, `package_id`, `id`); inversa hasMany |
| `demos.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `customers.proxy_id` | `proxies.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Proxy, `proxy_id`, `id`); inversa hasMany |
| `jellyfincustomers.jellyfinserver_id` | `jellyfinservers.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(JellyfinServer, `jellyfinserver_id`, `id`); inversa hasMany |
| `jellyfincustomers.jellyfinpackage_id` | `jellyfinpackages.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(JellyfinPackage, `jellyfinpackage_id`, `id`); inversa hasMany |
| `jellyfincustomers.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `jellyfincustomers.duration_id` | `durations.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Duration, `duration_id`, `id`); inversa hasMany |
| `jellyfindemos.jellyfinserver_id` | `jellyfinservers.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(JellyfinServer, `jellyfinserver_id`, `id`); inversa hasMany |
| `jellyfindemos.jellyfinpackage_id` | `jellyfinpackages.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(JellyfinPackage, `jellyfinpackage_id`, `id`); inversa hasMany |
| `jellyfindemos.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `embycustomers.embyserver_id` | `embyservers.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(EmbyServer, `embyserver_id`, `id`); inversa hasMany |
| `embycustomers.embypackage_id` | `embypackages.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(EmbyPackage, `embypackage_id`, `id`); inversa hasMany |
| `embycustomers.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `embycustomers.duration_id` | `durations.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Duration, `duration_id`, `id`); inversa hasMany |
| `embydemos.embyserver_id` | `embyservers.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(EmbyServer, `embyserver_id`, `id`); inversa hasMany |
| `embydemos.embypackage_id` | `embypackages.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(EmbyPackage, `embypackage_id`, `id`); inversa hasMany |
| `embydemos.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `users.parent_user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `parent_user_id`, `id`); inversa hasMany |
| `movements.parent_user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `parent_user_id`, `id`); inversa hasMany |
| `jellyfinmovements.parent_user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `parent_user_id`, `id`); inversa hasMany |
| `embymovements.parent_user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `parent_user_id`, `id`); inversa hasMany |
| `credits.user_parent_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_parent_id`, `id`); inversa hasMany |
| `servers.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `sessions.server_id` | `servers.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(Server, `server_id`, `id`); inversa hasMany |
| `content_requests.user_id` | `users.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(User, `user_id`, `id`); inversa hasMany |
| `menu_items.parent_id` | `menu_items.id` | Referencia lógica; contrastar reglas de negocio | belongsTo(LegacyMenuItem, `parent_id`, `id`); inversa hasMany |

### Pivots confirmadas

| Tabla | Relación many-to-many | Claves | Timestamps / unicidad |
|---|---|---|---|
| user_servers | User ↔ Server | user_id, server_id | withTimestamps; id propio; par sin UNIQUE |
| jellyfin_server_user | User ↔ JellyfinServer | user_id, jellyfin_server_id | withTimestamps; id propio; par sin UNIQUE |
| emby_server_user | User ↔ EmbyServer | user_id, emby_server_id | withTimestamps; id propio; par sin UNIQUE |
| user_roles | User ↔ Role adicional | user_id, role_id | Sin timestamps; PRIMARY compuesta |
| permission_role | Role ↔ Permission | role_id, permission_id | Sin timestamps; PRIMARY compuesta |

Usar belongsToMany con nombre de tabla y ambas claves explícitas. Si se necesitan modelos pivot, extender Pivot; los tres con id propio requieren incremento configurado. No tratar los pivots de clave compuesta como modelos ordinarios con id. Evitar duplicados de asignación desde la aplicación; revisar unicidad mediante una mejora posterior si hace falta.

server_libraries no es una pivot local entre dos tablas: no existe libraries; library_id pertenece a la API remota. libraries de los paquetes tampoco apunta a una tabla local. credits contiene eventos con cantidad y fecha, no es una pivot por tener dos referencias de usuario. Los pivot_table: credits que aparecen en relaciones belongsTo de BREAD con pivot=0 son metadatos auxiliares, no evidencia de una pivot real.

### Relaciones especiales

- User.role() usa role_id como rol principal; User.roles() usa user_roles para roles adicionales. La pivot está vacía, pero los 8 usuarios tienen rol principal. Leer solo la pivot dejaría al sistema sin autorización efectiva.
- User.parent() y User.children() usan parent_user_id. Se debe limitar la visibilidad al árbol autorizado, detectar ciclos y tratar padres ausentes sin conceder acceso global. Los nombres de rol no bastan para inferir permisos.
- Ticket.author(): morphTo con user_type y user_id explícitos. La única muestra usa App\Models\User. La columna platform no reemplaza user_type; para autores públicos se necesitan reglas verificadas. Usar una lista de tipos permitidos, no resolver arbitrariamente clases desde datos.
- PersonalAccessToken.tokenable(): morphTo sobre tokenable_type/tokenable_id; tabla vacía, tipos históricos no verificables.
- ContentRequest.user(): belongsTo(User, user_id) lógico. customer_id es varchar y se combina con platform y type; no es un morphTo estándar ni una FK universal. Las tres muestras son internas y no contienen customer_id. Resolver por plataforma solo después de verificar si representa ID local o remoto.
- PlexSession.server(): belongsTo(Server, server_id). plex_user_id es remoto, no customers.id; un vínculo opcional con customers.plex_user_id exige además server_id y control de duplicados. No asumir identidad global ni unicidad local.
- Movement / JellyfinMovement / EmbyMovement guardan user, customer y server como texto histórico, no como IDs. No crear belongsTo a partir de esos textos. Solo parent_user_id permite una relación lógica.
- LegacyMenuItem.parent()/children() usan parent_id. LegacyTranslation apunta de forma lógica por table_name/column_name/foreign_key; no es una FK ni un polimorfismo Eloquent estándar.

## 5. Integridad y anomalías de los datos

| Referencia lógica | Filas con referencia sin destino | Consecuencia |
|---|---:|---|
| users.parent_user_id → users.id | 1 (usuario 59 → 33) | Conservar registro y señalar padre ausente; no reasignar automáticamente |
| content_requests.user_id → users.id | 3 (autores 103 y 106 ausentes) | Mostrar autor no disponible; mantener solicitudes |
| embymovements.parent_user_id → users.id | 51 | Mantener histórico, sin exigir que el autor siga existiendo |

Las demás referencias simples enumeradas en la sección 4 no tienen huérfanos en esta copia; las tablas vacías no aportan evidencia. No se verificaron entidades remotas, relaciones textuales ni customer_id público por carecer de muestras. Los enteros firmados int de varios vínculos no coinciden con bigint unsigned de las PK: Eloquent puede leerlos, pero añadir FK requiere revisar tipos y datos primero.

Las 6 cuentas tienen estado activo y fecha de vencimiento posterior al 15/09/2026. Esto describe el respaldo, no verifica acceso remoto. Los 6 servidores tienen status=1 e is_dead=0; no prueba disponibilidad actual. AUTO_INCREMENT alto no es número de registros ni permite recuperar los eliminados.

Los 8 usuarios tienen hashes con formato bcrypt en password. Los 6 clientes multimedia tienen contraseñas legibles; no se reproducen. users.passcode está poblado en un registro y debe tratarse como secreto.

Los 175 registros de migrations describen evolución, pero no sustituyen DDL ni código: hay nombres históricos que aluden a purchases y columnas hoy ausentes. No existe purchases en este respaldo. No ejecutar migraciones antiguas recreadas a partir de esos nombres.

## 6. JSON y formatos heredados

JSON SQL nativo existe únicamente en customers.json_data, tickets.attachments y ticket_messages.attachments. Otros contenedores son TEXT/LONGTEXT. La siguiente tabla distingue contenido JSON de texto no JSON; texto no JSON no implica corrupción salvo que el consumidor exija JSON.

| Campo | Valores no vacíos | JSON válido detectado | No JSON al intentar decodificar | Formato observado |
|---|---:|---:|---:|---|
| `credit_purchases.json_response` | 0 | 0 | 0 | Sin muestra JSON |
| `customers.json_data` | 2 | 2 | 0 | object |
| `data_rows.details` | 252 | 251 | 1 | object, string |
| `data_types.details` | 19 | 19 | 0 | object |
| `embycustomers.json_data` | 3 | 3 | 0 | object |
| `embydemos.json_data` | 0 | 0 | 0 | Sin muestra JSON |
| `embypackages.libraries` | 0 | 0 | 0 | Sin muestra JSON |
| `embyservers.json_custom_config` | 1 | 1 | 0 | array |
| `jellyfincustomers.json_data` | 1 | 1 | 0 | object |
| `jellyfindemos.json_data` | 0 | 0 | 0 | Sin muestra JSON |
| `jellyfinpackages.libraries` | 0 | 0 | 0 | Sin muestra JSON |
| `jellyfinservers.json_custom_config` | 2 | 2 | 0 | array |
| `notifications.reads` | 0 | 0 | 0 | Sin muestra JSON |
| `packages.libraries` | 0 | 0 | 0 | Sin muestra JSON |
| `personal_access_tokens.abilities` | 0 | 0 | 0 | Sin muestra JSON |
| `servers.json_data` | 3 | 3 | 0 | object |
| `settings.value` | 88 | 1 | 0 | array |
| `settings.details` | 49 | 11 | 38 | object |
| `ticket_messages.attachments` | 2 | 2 | 0 | array |
| `tickets.attachments` | 1 | 1 | 0 | array |
| `users.settings` | 0 | 0 | 0 | Sin muestra JSON |
| `users.service` | 8 | 8 | 0 | string |

La detección revisó todas las celdas: intentó decodificar campos candidatos y valores que comienzan por objeto/array. No afirma que todo texto arbitrario sea JSON. durations.service es un enum, no un contenedor JSON.

- customers.json_data: 2 objetos con plex_login, plex_create_user y plex_invite_friend. Se encuentran plex_login.user.authToken, secretos/token de servicios anidados y plex_invite_friend.inviteToken; incluyen información de identidad y bibliotecas compartidas. Es evidencia de respuestas guardadas, no prueba de endpoints ni de su soporte actual.
- servers.json_data: 3 objetos con name, machine, token, address, port y scheme. El token está duplicado dentro del JSON; cifrar únicamente servers.token sería insuficiente.
- jellyfincustomers.json_data: 1 objeto con Id, ServerId, Name, Policy y Configuration. embycustomers.json_data: 3 objetos análogos, con campos adicionales de Emby/Connect. Conservar Id remoto como texto, no confundirlo con id local. No tienen columna dedicada de usuario remoto.
- users.service: los 8 valores necesitan dos decodificaciones para obtener arrays. Proponer lector compatible con profundidad limitada y lista cerrada de servicios; no reescribir masivamente en FASE 1. Un array vacío no demuestra permiso para todas las plataformas.
- jellyfinservers.json_custom_config y embyservers.json_custom_config contienen arrays vacíos en las muestras. El formato de una configuración no vacía sigue pendiente.
- data_rows.details: 251 valores parseables, incluido al menos un JSON escalar, y 1 no parseable (id=269, field=price). Validar forma de objeto antes de usarlo; no ejecutar reglas/metadatos sin revisión. Hay claves de validación mal escritas o inconsistentes.
- settings.details mezcla JSON y texto: 11 JSON válidos y 38 valores no JSON. No aplicar cast array universal. settings.value mezcla escalares, plantillas y una lista JSON.
- packages.libraries, jellyfinpackages.libraries, embypackages.libraries: TEXT sin filas. No se puede afirmar si el formato previo es JSON, CSV u otro. Se necesita una muestra o código legado antes de escribir paquetes compatibles.
- notifications.reads, credit_purchases.json_response, personal_access_tokens.abilities y los JSON de demos no tienen muestras. Sus casts deben definirse después de confirmar contrato.
- tickets.attachments y ticket_messages.attachments son arrays en las muestras. Mantener referencias a archivos sin asumir que el respaldo incluye los archivos físicos.

## 7. Seguridad: campos afectados y tratamiento propuesto

| Campos | Hallazgo/riesgo | Tratamiento futuro |
|---|---|---|
| servers.token; jellyfinservers.api_key; embyservers.api_key | Credenciales de servidor en columnas de texto | Cifrado reversible backend, ocultación y acceso mínimo |
| servers.json_data; customers.json_data | Copias de tokens y datos de autenticación anidados | Lector heredado privado; nuevas respuestas con lista explícita de campos permitidos |
| customers.plex_user_token; demos.plex_user_token; invited_id | Autenticación o referencias de invitación | Tokens secretos; IDs internos restringidos y nunca usados como autorización |
| customers.password; jellyfincustomers.password; embycustomers.password; contraseñas de demos | Credenciales multimedia; clientes actuales legibles | Evitar persistir nuevas contraseñas si no es necesario; si lo es, cifrar y ocultar |
| users.password | 8 hashes bcrypt | Mantener hashes existentes; Hash::make para cambios, sin doble hash |
| users.passcode; logins.passcode; users.remember_token | Secretos de autenticación | Ocultar, minimizar retención y revisar hash/expiración para códigos de un solo uso |
| password_resets.token; personal_access_tokens.token | Tablas vacías con propósito sensible | Respetar contrato de hash del componente; no cifrar hashes como si fueran API keys |
| settings.value | Claves Telegram, TMDB y pasarelas de pago conviven con configuración pública | Cifrar por clave sensible; separar vistas públicas y privadas, sin exportar la tabla al navegador |
| credit_purchases.json_response; jobs.payload; failed_jobs.payload/exception | Respuestas y trabajos pueden contener credenciales | Sanitizar, restringir acceso y retención; no poner secretos crudos en trabajos/logs |
| json_data/json_custom_config, users.settings/aditional_info | Contenedores extensibles y datos personales | Validar estructura, ocultar por defecto, permitir únicamente campos necesarios |
| email, phone, telegram_chat_id, telegram_user_name, logins.ip, URLs de canales/proxies | Datos personales y red; URL puede incluir credenciales | Acceso por rol y propietario; redactar en logs; validar destinos backend |
| notifications.content, tickets, mensajes, solicitudes, plantillas y observaciones | Texto/HTML y adjuntos no confiables | Escape en Blade, sanitización de HTML permitido y control de archivos/enlaces |

Claves sensibles de settings identificadas por nombre: plex.telegram_bot_token, emby.telegram_bot_token, security.api_key_telegram_bot_security, admin.tmdb_api_key, payments.paypal_secret, payments.stripe_secret_key. Su existencia no implica que todas tengan un valor configurado. Client IDs y publishable keys requieren clasificación distinta de los secretos privados.

No aplicar encrypted cast a todo el legado directamente: fallaría al leer texto plano y el ciphertext puede superar VARCHAR(50), VARCHAR(100) o VARCHAR(255). Antes de escribir secretos cifrados hay que diseñar una transición versionada, medir longitudes y justificar almacenamiento aditivo o ampliaciones compatibles. No usar un catch de descifrado que convierta cualquier ciphertext corrupto en supuesto texto plano. APP_KEY debe conservarse fuera del respaldo público y tener estrategia de recuperación.

Ocultar campos con $hidden no sustituye autorización: las vistas y DTO deben seleccionar campos seguros. No copiar respuestas API completas a auditoría. El dump y las capturas contienen credenciales: mantenerlos fuera de public, del repositorio y de ejemplos de pruebas. No se ha efectuado rotación ni operación remota en esta fase.

## 8. Correspondencia con las capturas

| Captura | Pantalla / función | Tablas y campos | Límites de la evidencia |
|---|---|---|---|
| 1 | Dashboard, tarjetas de sesiones, vendedores y clientes; pestañas de plataforma | servers / jellyfinservers / embyservers; users + roles; customers / jellyfincustomers / embycustomers; movements por plataforma | Sesiones reales dependen de API; sessions está vacía aunque la captura muestra 3 Plex. No reconstruir ese 3 como dato fijo |
| 2 | Cuentas por vencer Plex | customers.date_to, user_id, server_id, phone/telegram_user_name; users y servers | Días restantes calculados; establecer zona horaria y umbral. La captura muestra un cliente a 29 días, no necesariamente filtro de 7 días |
| 3 | Clientes por servidor | customers.server_id → servers.id; count agrupado; servers.status | Servidor mostrado es el que tiene clientes; no significa que solo haya uno registrado |
| 4 | Clientes por vendedor | customers.user_id → users.id; roles por users.role_id | Agrupar por creador/asignado según contrato; no contar textos de movements |
| 5 | Sidebar desplegable y submenús Plex/Jellyfin/Emby | menus, menu_items, permissions, permission_role, roles, user_roles | Recuperar jerarquía y etiquetas, aplicar policies nuevas; no ejecutar rutas heredadas sin reconstrucción |
| 6 y 8 | Añadir cliente Plex (capturas duplicadas) | customers, servers, packages, durations, users; domains para correo generado | «Agregar sin clave» es opción de flujo, sin columna propia. SQL exige password NOT NULL; no inventar significado |
| 7 | Listado Plex y acciones | customers + servers + packages + durations + users | Sin paquete corresponde a package_id nullable. Capacidad usa limit_accounts/accounts_count y reglas por verificar. Las contraseñas visibles del legado deben ocultarse |

El formulario usa customers.name/phone/email/password/date_from/date_to/screens/observation/user_id, con server_id, package_id y duration_id. Bibliotecas se obtienen del paquete/API, no de una columna customer.libraries. Para Jellyfin/Emby cambian nombres de FK y comment; no existen date_from ni una columna username separada. Las capturas no muestran todos los campos inferiores del formulario, por lo que el SQL prevalece para el contrato físico.

Diseño a recuperar en FASE 1: sidebar oscuro plegable, contenido claro, breadcrumbs, tarjetas y tablas, pestañas de plataforma y acciones. Conservar colores funcionales Plex dorado, Jellyfin morado y Emby verde; no es necesario reutilizar Voyager para conservar sus datos.

## 9. Reutilización, compatibilidad y diferencias con el objetivo

### Reutilizable sin alterar el esquema

- Lectura de clientes, servidores, demos, duraciones, movimientos, solicitudes y soporte con nombres actuales.
- Usuarios y hashes bcrypt; roles principales/adicionales y permission_role existentes.
- Jerarquía parent_user_id y pivots de asignación de servidores, con controles de integridad en aplicación.
- Duraciones en meses, demos por horas y fechas ya existentes; mantener ortografías aditional_info y aditional_credits_for_live_tv.
- Catálogos, plantillas y configuración, mediante acceso controlado y parsers específicos.
- jobs/failed_jobs como base de infraestructura a verificar con la versión elegida. migrations como historial protegido.
- Modelos propuestos en este informe: declarar $table explícito, $fillable mínimo y $hidden de secretos. La aplicación debe arrancar en modo de lectura sin sincronizaciones automáticas destructivas.

### Contratos de modelos

Preservar Customer/Server/Package/Demo para Plex, tal como aparecen en data_types. JellyfinCustomer usa $table='jellyfincustomers', y equivalentes para Jellyfin/Emby. No dejar que Eloquent busque jellyfin_customers o emby_servers. Modelos Legacy* aíslan metadatos de Voyager; no cargar clases arbitrarias de data_types desde la base.

Usar casts de fecha para date_from/date_to y datetime para fechas de demos, movement_time y otros timestamps. customers.last_expiration_notification_sent_at es DATE; embycustomers tiene TIMESTAMP; Jellyfin carece de ese campo. Conservar esa diferencia. Duraciones: meses calendario con política explícita de fin de mes; no asumir 30 días por mes.

status de clientes Jellyfin/Emby debe ser integer (0 inactivo, 1 activo, 2 caducado por BREAD); customers.status y users.status son enums de texto. is_demo/is_dead y flags verdaderamente binarios pueden ser booleanos. Montos DECIMAL se manejan sin conversión innecesaria a float; movimientos Jellyfin/Emby usan DOUBLE y deben preservarse sin convertirlos silenciosamente a int.

settings, migrations, password_resets, permission_role y user_roles no tienen el par created_at/updated_at: configurar timestamps de forma explícita. jobs usa created_at entero UNIX, no timestamps Eloquent convencionales. failed_jobs usa failed_at. password_resets no tiene PK y se gestiona por repositorio/broker; los modelos de infraestructura son opcionales, no un CRUD público.

### Funciones que requieren diseño posterior, sin migraciones ahora

| Diferencia | Evidencia | Propuesta para otra fase |
|---|---|---|
| Monitor detallado | Faltan last_checked_at, last_online_at y error_message en los tres servidores | Mejora aditiva documentada; conservar status/is_dead |
| Estados extendidos | No hay campo Error API ni distinción universal suspendido/vencido | Estado de presentación derivado + operación remota separada; no insertar strings en tinyint |
| Duraciones generales | durations solo tiene months; demos hours | Mantener meses y horas actuales; diseñar unidad/cantidad aditivas para días/años si hace falta |
| Inicio y nombre remoto | Jellyfin/Emby no tienen date_from o columna dedicada al Id remoto | Leer Id de JSON con validación; documentar mejoras aditivas de identidad/fechas |
| Demos procesadas | Ninguna tabla de demos tiene estado de expiración/procesamiento | Hace falta control durable e idempotente antes de automatizar retiros; no inferir éxito solo por fecha |
| Paquetes | Solo nombre/servidor/libraries; sin estado/detalles; bibliotecas sin muestra | Confirmar serialización y validar IDs con API antes de crear |
| Capacidades | Jellyfin/Emby carecen de limit_accounts; Plex sí tiene límite y contador | No mostrar límites inventados; decidir ampliación cuando corresponda |
| Sesiones | Plex solo IDs mínimos; no hay tablas de sesiones Jellyfin/Emby | Consultar API y cache temporal; no crear historial permanente por defecto |
| Notificaciones de vencimiento | Fecha única Plex/Emby, nada equivalente Jellyfin | Registro idempotente por cuenta, vencimiento, umbral y canal; notifications no sirve como registro de entrega |
| Créditos/auditoría | credits no tiene cliente/plataforma/acción; movimientos son texto y no tienen IP ni idempotencia | Diseñar trazabilidad aditiva antes de nuevas operaciones de cobro concurrentes |
| Solicitudes comentadas | content_requests.comment es un texto único | Reutilizar comentario simple; historial de comentarios requeriría una extensión justificada |
| Autenticación web | sessions no tiene payload/last_activity; password_resets es formato legado | Configurar sesión por archivo al inicio y broker contra password_resets; verificar compatibilidad de versión |
| Tokens personales | No hay expires_at | Verificar necesidades de la versión antes de habilitar tokens; no presuponer compatibilidad total |

Una transacción SQL no revierte una llamada API exitosa. El futuro servicio de clientes debe registrar el intento y aplicar compensación/reconciliación si falla la persistencia posterior, con idempotencia para reintentos. El respaldo no contiene una tabla dedicada de operaciones remotas. No afirmar «rollback completo» solo por usar DB::transaction().

Los saldos deben bloquearse al modificar créditos y validarse antes de cobrar. No existe restricción CHECK de saldo no negativo ni evidencia suficiente de tarifas o reembolsos. Configuraciones como admin.demos_no_credit y durations.amount aportan pistas, no una especificación completa.

## 10. Roles, permisos y configuración recuperables

Roles reales: `admin` (id 1), `Super Reseller` (id 3), `general_admin` (id 4), `Reseller` (id 5), `Limited Admin` (id 6). No hay rol llamado literalmente Super Admin, Vendedor, Soporte o Cliente. Cualquier equivalencia nueva debe mapearse de forma explícita sin renombrar los registros.

Hay 116 permisos y 308 asignaciones permission_role; users.role_id está poblado y user_roles está vacía. Se debe reconstruir autorización con esos permisos y comprobar además propiedad, jerarquía, servicios habilitados y servidor asignado. No permitir acceso total simplemente porque el nombre contenga Admin.

settings incluye parámetros de vencimiento, demos sin créditos, límites de sesiones/pantallas, servidores compartidos, cambio de servidor por revendedor, permisos de borrado, creación de subrevendedores, plantillas Telegram, pasarelas y corte por webhook. Sus nombres se enumeran sin valores a continuación; no se activan automatizaciones ni integraciones al leerlos.

- `site.title`
- `site.description`
- `site.logo`
- `site.google_analytics_tracking_id`
- `admin.bg_image`
- `admin.title`
- `admin.description`
- `admin.loader`
- `admin.icon_image`
- `admin.google_analytics_client_id`
- `admin.credits_number`
- `admin.screen_message`
- `admin.account_expiration_days`
- `admin.limit_the_sessions`
- `admin.max_reseller_screen`
- `admin.demos_no_credit`
- `jellyfin.live_tv_credits`
- `plex.enable_monitor`
- `jellyfin.enable_monitor`
- `jellyfin.live_tv_validation`
- `emby.enable_monitor`
- `landing.enable_landing_page`
- `landing.contact_email`
- `landing.contact_whatsapp`
- `landing.contact_telegram`
- `landing.plex_server`
- `landing.jellyfin_server`
- `landing.emby_server`
- `landing.brandName`
- `landing.title`
- `landing.heroTitle`
- `landing.heroSubtitle`
- `landing.heroCtaText`
- `landing.contactTitle`
- `landing.contactSubtitle`
- `landing.colors_primary`
- `landing.colors_primary_hover`
- `landing.colors_background_dark`
- `landing.colors_background_light`
- `landing.colors_card_background`
- `landing.colors_text_primary`
- `landing.colors_text_secondary`
- `landing.colors_accent_plex`
- `landing.colors_accent_emby`
- `landing.colors_accent_jellyfin`
- `landing.hero_background_image`
- `landing.platform_order`
- `landing.enable_contact_form`
- `landing.hero_button_link`
- `site.contact_email`
- `site.contact_whatsapp`
- `site.contact_telegram`
- `site.plex_server`
- `site.jellyfin_server`
- `site.emby_server`
- `plex.days_before_expiration`
- `plex.expiration_message`
- `admin.login_alignment`
- `admin.login_style`
- `emby.expiration_message`
- `plex.copy_template`
- `emby.copy_template`
- `plex.telegram_bot_token`
- `plex.telegram_channel_id`
- `plex.telegram_server_id`
- `plex.telegram_message_template`
- `admin.tmdb_api_key`
- `site.in_maintenance`
- `emby.telegram_bot_token`
- `emby.telegram_channel_id`
- `emby.telegram_server_id`
- `emby.telegram_message_template`
- `security.login_security`
- `security.max_attempts`
- `security.api_key_telegram_bot_security`
- `security.channel_id_security`
- `security.telegram_template_message`
- `payments.payment_active`
- `payments.mode`
- `payments.currency`
- `payments.paypal_client_id`
- `payments.paypal_secret`
- `payments.stripe_publishable_key`
- `payments.stripe_secret_key`
- `telegram.reminder_template`
- `telegram.demo_expiry_template`
- `telegram.daily_summary_template`
- `telegram.low_credits_template`
- `telegram.reminder_hour_range`
- `telegram.low_credits_threshold`
- `admin.server_for_alls`
- `admin.enable_demos_jelly_and_emby`
- `plex.show_server_name_as`
- `plex.allow_reseller_change_server`
- `emby.allow_reseller_delete_customers`
- `jellyfin.allow_reseller_delete_customers`
- `admin.allow_sr_create_sr`
- `jellyfin.copy_template`
- `emby.webhook_cutoff_active`
- `emby.max_days_disable`
- `emby.max_days_delete`
- `emby.webhook_message_template`
- `jellyfin.webhook_cutoff_active`
- `jellyfin.max_days_disable`
- `jellyfin.max_days_delete`
- `jellyfin.webhook_message_template`
- `plex.webhook_cutoff_active`
- `plex.max_days_disable`
- `plex.max_days_delete`
- `plex.webhook_message_template`

## 11. Catálogo técnico por tabla

Se transcriben todas las columnas con tipo, nulabilidad, DEFAULT y atributos del DDL, además de todos los índices y FK. CHARACTER SET/COLLATE repetidos por columna se omiten de la tabla para legibilidad: son utf8mb4/utf8mb4_unicode_ci como el origen. «Sin FK» no significa «sin relación»: consultar la sección 4. La regla ON DELETE/ON UPDATE no escrita conserva el comportamiento por defecto del motor, sin inventar CASCADE.

### channels

**Categoría/plataforma:** Emby. **Modelo:** `Channel`. **Filas:** 0.

Canales asociados a un servidor Emby; URLs y logos externos.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `embyserver_id` | `bigint unsigned NOT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `description` | `text` |
| `logo` | `text` |
| `url` | `text` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `channels_embyserver_id_foreign` (`embyserver_id`)`

**Foreign keys:**

- `CONSTRAINT `channels_embyserver_id_foreign` FOREIGN KEY (`embyserver_id`) REFERENCES `embyservers` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `embyserver_id` → `embyservers.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=7928 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### content_requests

**Categoría/plataforma:** Otros. **Modelo:** `ContentRequest`. **Filas:** 3.

Solicitudes internas/públicas de películas y series para las tres plataformas.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `type` | `enum('intern','public') NOT NULL` |
| `user_id` | `bigint unsigned DEFAULT NULL` |
| `platform` | `enum('plex','emby','jellyfin') DEFAULT NULL` |
| `customer_id` | `varchar(255) DEFAULT NULL` |
| `request_type` | `enum('movie','serie') NOT NULL` |
| `title` | `varchar(255) NOT NULL` |
| `poster` | `varchar(255) DEFAULT NULL` |
| `season_serie` | `varchar(255) DEFAULT NULL` |
| `chapter_serie` | `varchar(255) DEFAULT NULL` |
| `status` | `enum('waiting','in_progress','closed') NOT NULL DEFAULT 'waiting'` |
| `comment` | `text` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `content_requests_user_id_index` (`user_id`)`
- `KEY `content_requests_customer_id_index` (`customer_id`)`
- `KEY `content_requests_status_index` (`status`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### credit_purchases

**Categoría/plataforma:** Ventas. **Modelo:** `CreditPurchase`. **Filas:** 0.

Compras de planes de créditos y respuesta del proveedor de pago.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `plan_id` | `bigint unsigned NOT NULL` |
| `user_id` | `bigint unsigned NOT NULL` |
| `platform` | `varchar(255) NOT NULL` |
| `amount` | `decimal(10,2) NOT NULL` |
| `currency` | `varchar(255) NOT NULL` |
| `status` | `varchar(255) NOT NULL DEFAULT 'pending'` |
| `json_response` | `longtext` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `credit_purchases_plan_id_foreign` (`plan_id`)`
- `KEY `credit_purchases_user_id_foreign` (`user_id`)`

**Foreign keys:**

- `CONSTRAINT `credit_purchases_plan_id_foreign` FOREIGN KEY (`plan_id`) REFERENCES `plans` (`id`) ON DELETE CASCADE`
- `CONSTRAINT `credit_purchases_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `plan_id` → `plans.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### credits

**Categoría/plataforma:** Créditos. **Modelo:** `Credit`. **Filas:** 8.

Registros de asignación de créditos entre usuarios; no representan por sí solos el saldo.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user_id` | `bigint unsigned NOT NULL` |
| `qty` | `int NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `user_parent_id` | `int DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `credits_user_id_foreign` (`user_id`)`

**Foreign keys:**

- `CONSTRAINT `credits_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE`

**Relaciones propuestas:** `user_id` → `users.id`; `user_parent_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=92 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### customers

**Categoría/plataforma:** Plex. **Modelo:** `Customer`. **Filas:** 2.

Clientes Plex, credenciales, invitación, vigencia y creador.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user_id` | `bigint unsigned NOT NULL` |
| `name` | `varchar(255) DEFAULT NULL` |
| `phone` | `varchar(50) DEFAULT NULL` |
| `email` | `varchar(255) NOT NULL` |
| `password` | `varchar(50) NOT NULL` |
| `status` | `enum('active','inactive','grace') NOT NULL DEFAULT 'active'` |
| `plex_user_id` | `varchar(255) DEFAULT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `plex_user_name` | `varchar(255) DEFAULT NULL` |
| `plex_user_token` | `varchar(255) DEFAULT NULL` |
| `invited_id` | `varchar(50) DEFAULT NULL` |
| `duration_id` | `bigint unsigned NOT NULL` |
| `date_from` | `date DEFAULT NULL` |
| `date_to` | `date DEFAULT NULL` |
| `last_expiration_notification_sent_at` | `date DEFAULT NULL` |
| `server_id` | `int NOT NULL` |
| `screens` | `int NOT NULL DEFAULT '4'` |
| `proxy_id` | `int DEFAULT NULL` |
| `telegram_user_name` | `varchar(255) DEFAULT NULL` |
| `observation` | `text` |
| `json_data` | `json DEFAULT NULL` |
| `package_id` | `int DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `customers_user_id_foreign` (`user_id`)`
- `KEY `customers_duration_id_foreign` (`duration_id`)`

**Foreign keys:**

- `CONSTRAINT `customers_duration_id_foreign` FOREIGN KEY (`duration_id`) REFERENCES `durations` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE`
- `CONSTRAINT `customers_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE`

**Relaciones propuestas:** `duration_id` → `durations.id`; `user_id` → `users.id`; `package_id` → `packages.id`; `server_id` → `servers.id`; `proxy_id` → `proxies.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=2305 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### data_rows

**Categoría/plataforma:** Sistema. **Modelo:** `LegacyDataRow`. **Filas:** 256.

Metadatos BREAD de Voyager: campos, validaciones y relaciones de formularios.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `data_type_id` | `int unsigned NOT NULL` |
| `field` | `varchar(255) NOT NULL` |
| `type` | `varchar(255) NOT NULL` |
| `display_name` | `varchar(255) NOT NULL` |
| `required` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `browse` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `read` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `edit` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `add` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `delete` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `details` | `text` |
| `order` | `int NOT NULL DEFAULT '1'` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `data_rows_data_type_id_foreign` (`data_type_id`)`

**Foreign keys:**

- `CONSTRAINT `data_rows_data_type_id_foreign` FOREIGN KEY (`data_type_id`) REFERENCES `data_types` (`id`) ON DELETE CASCADE ON UPDATE CASCADE`

**Relaciones propuestas:** `data_type_id` → `data_types.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=285 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### data_types

**Categoría/plataforma:** Sistema. **Modelo:** `LegacyDataType`. **Filas:** 21.

Recursos BREAD, modelos, controladores, políticas y scopes declarados.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `slug` | `varchar(255) NOT NULL` |
| `display_name_singular` | `varchar(255) NOT NULL` |
| `display_name_plural` | `varchar(255) NOT NULL` |
| `icon` | `varchar(255) DEFAULT NULL` |
| `model_name` | `varchar(255) DEFAULT NULL` |
| `policy_name` | `varchar(255) DEFAULT NULL` |
| `controller` | `varchar(255) DEFAULT NULL` |
| `description` | `varchar(255) DEFAULT NULL` |
| `generate_permissions` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `server_side` | `tinyint NOT NULL DEFAULT '0'` |
| `details` | `text` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `data_types_name_unique` (`name`)`
- `UNIQUE KEY `data_types_slug_unique` (`slug`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### demos

**Categoría/plataforma:** Plex. **Modelo:** `Demo`. **Filas:** 0.

Accesos temporales Plex, horas, inicio y fin.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `server_id` | `bigint unsigned NOT NULL` |
| `email` | `varchar(255) NOT NULL` |
| `password` | `varchar(255) NOT NULL` |
| `hours` | `int NOT NULL` |
| `start_date` | `datetime NOT NULL` |
| `end_date` | `datetime NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `plex_user_name` | `varchar(255) DEFAULT NULL` |
| `invited_id` | `varchar(255) DEFAULT NULL` |
| `plex_user_token` | `varchar(255) DEFAULT NULL` |
| `user_id` | `int DEFAULT NULL` |
| `package_id` | `int DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `demos_server_id_foreign` (`server_id`)`

**Foreign keys:**

- `CONSTRAINT `demos_server_id_foreign` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE RESTRICT ON UPDATE CASCADE`

**Relaciones propuestas:** `server_id` → `servers.id`; `package_id` → `packages.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=164 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### domains

**Categoría/plataforma:** Configuración. **Modelo:** `Domain`. **Filas:** 1.

Dominios para generación de correos de cuentas y demos; relación funcional, no FK.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `type` | `enum('account','demo','all') NOT NULL DEFAULT 'all'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### durations

**Categoría/plataforma:** Configuración. **Modelo:** `Duration`. **Filas:** 14.

Duraciones en meses, coste, servicio, pantallas y recargo de TV.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `months` | `int NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `amount` | `int DEFAULT NULL` |
| `service` | `enum('plex','jellyfin','emby','all') NOT NULL DEFAULT 'all'` |
| `screens` | `int DEFAULT NULL` |
| `aditional_credits_for_live_tv` | `double(8,2) NOT NULL DEFAULT '0.00'` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=27 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### emby_server_user

**Categoría/plataforma:** Emby. **Modelo:** `EmbyServerUser (Pivot)`. **Filas:** 0.

Asignación de servidores Emby a usuarios del panel.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user_id` | `bigint unsigned NOT NULL` |
| `emby_server_id` | `bigint unsigned NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `emby_server_user_user_id_foreign` (`user_id`)`
- `KEY `emby_server_user_emby_server_id_foreign` (`emby_server_id`)`

**Foreign keys:**

- `CONSTRAINT `emby_server_user_emby_server_id_foreign` FOREIGN KEY (`emby_server_id`) REFERENCES `embyservers` (`id`) ON DELETE CASCADE`
- `CONSTRAINT `emby_server_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `emby_server_id` → `embyservers.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### embycustomers

**Categoría/plataforma:** Emby. **Modelo:** `EmbyCustomer`. **Filas:** 3.

Clientes Emby, vigencia, política y datos remotos.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `embyserver_id` | `int NOT NULL` |
| `duration_id` | `int NOT NULL` |
| `embypackage_id` | `int DEFAULT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `email` | `varchar(255) DEFAULT NULL` |
| `password` | `varchar(255) NOT NULL` |
| `phone` | `varchar(255) DEFAULT NULL` |
| `comment` | `text` |
| `date_to` | `date NOT NULL` |
| `user_id` | `int NOT NULL` |
| `screens` | `int NOT NULL DEFAULT '1'` |
| `status` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `json_data` | `text` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `with_transcode` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `with_user_connect` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `last_expiration_notification_sent_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `embyserver_id` → `embyservers.id`; `embypackage_id` → `embypackages.id`; `user_id` → `users.id`; `duration_id` → `durations.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=194 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### embydemos

**Categoría/plataforma:** Emby. **Modelo:** `EmbyDemo`. **Filas:** 0.

Demos Emby por horas.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `embyserver_id` | `int NOT NULL` |
| `embypackage_id` | `int DEFAULT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `email` | `varchar(255) DEFAULT NULL` |
| `password` | `varchar(255) NOT NULL` |
| `hours` | `int NOT NULL` |
| `date_to` | `datetime NOT NULL` |
| `user_id` | `int NOT NULL` |
| `with_transcode` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `json_data` | `text` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `embyserver_id` → `embyservers.id`; `embypackage_id` → `embypackages.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=290 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### embymovements

**Categoría/plataforma:** Emby. **Modelo:** `EmbyMovement`. **Filas:** 226.

Historial de operaciones Emby; también categoría Logs.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user` | `varchar(255) DEFAULT NULL` |
| `customer` | `varchar(255) DEFAULT NULL` |
| `description` | `text` |
| `credits` | `double NOT NULL DEFAULT '0'` |
| `movement_time` | `datetime DEFAULT NULL` |
| `server` | `varchar(255) DEFAULT NULL` |
| `parent_user_id` | `int DEFAULT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `parent_user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=227 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### embypackages

**Categoría/plataforma:** Emby. **Modelo:** `EmbyPackage`. **Filas:** 0.

Paquetes de bibliotecas de un servidor Emby.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `embyserver_id` | `bigint unsigned NOT NULL` |
| `libraries` | `text NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `embypackages_embyserver_id_foreign` (`embyserver_id`)`

**Foreign keys:**

- `CONSTRAINT `embypackages_embyserver_id_foreign` FOREIGN KEY (`embyserver_id`) REFERENCES `embyservers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE`

**Relaciones propuestas:** `embyserver_id` → `embyservers.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### embyservers

**Categoría/plataforma:** Emby. **Modelo:** `EmbyServer`. **Filas:** 1.

Conexión API Emby, estado y configuración de acceso.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `host` | `varchar(255) NOT NULL` |
| `port` | `varchar(10) DEFAULT NULL` |
| `api_key` | `varchar(255) NOT NULL` |
| `status` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `with_live_tv` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `is_dead` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `is_demo` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `custom_config` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `limit_apple_devices` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `json_custom_config` | `text` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=37 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### failed_jobs

**Categoría/plataforma:** Logs. **Modelo:** `FailedJob (infraestructura)`. **Filas:** 0.

Fallos de cola; payload y excepción potencialmente sensibles.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `uuid` | `varchar(255) NOT NULL` |
| `connection` | `text NOT NULL` |
| `queue` | `text NOT NULL` |
| `payload` | `longtext NOT NULL` |
| `exception` | `longtext NOT NULL` |
| `failed_at` | `timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jellyfin_server_user

**Categoría/plataforma:** Jellyfin. **Modelo:** `JellyfinServerUser (Pivot)`. **Filas:** 0.

Asignación de servidores Jellyfin a usuarios del panel.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user_id` | `bigint unsigned NOT NULL` |
| `jellyfin_server_id` | `bigint unsigned NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `jellyfin_server_user_user_id_foreign` (`user_id`)`
- `KEY `jellyfin_server_user_jellyfin_server_id_foreign` (`jellyfin_server_id`)`

**Foreign keys:**

- `CONSTRAINT `jellyfin_server_user_jellyfin_server_id_foreign` FOREIGN KEY (`jellyfin_server_id`) REFERENCES `jellyfinservers` (`id`) ON DELETE CASCADE`
- `CONSTRAINT `jellyfin_server_user_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `jellyfin_server_id` → `jellyfinservers.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jellyfincustomers

**Categoría/plataforma:** Jellyfin. **Modelo:** `JellyfinCustomer`. **Filas:** 1.

Clientes Jellyfin, vigencia, política y datos remotos.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `jellyfinserver_id` | `int NOT NULL` |
| `duration_id` | `int NOT NULL` |
| `jellyfinpackage_id` | `int DEFAULT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `email` | `varchar(255) DEFAULT NULL` |
| `password` | `varchar(255) NOT NULL` |
| `phone` | `varchar(255) DEFAULT NULL` |
| `comment` | `text` |
| `date_to` | `date NOT NULL` |
| `user_id` | `int NOT NULL` |
| `screens` | `int NOT NULL DEFAULT '1'` |
| `with_transcode` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `json_data` | `text` |
| `status` | `tinyint(1) NOT NULL DEFAULT '1'` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `jellyfinserver_id` → `jellyfinservers.id`; `jellyfinpackage_id` → `jellyfinpackages.id`; `user_id` → `users.id`; `duration_id` → `durations.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=106 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jellyfindemos

**Categoría/plataforma:** Jellyfin. **Modelo:** `JellyfinDemo`. **Filas:** 0.

Demos Jellyfin por horas.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `jellyfinserver_id` | `int NOT NULL` |
| `jellyfinpackage_id` | `int DEFAULT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `email` | `varchar(255) DEFAULT NULL` |
| `password` | `varchar(255) NOT NULL` |
| `hours` | `int NOT NULL` |
| `date_to` | `datetime NOT NULL` |
| `user_id` | `int NOT NULL` |
| `with_transcode` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `json_data` | `text` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `jellyfinserver_id` → `jellyfinservers.id`; `jellyfinpackage_id` → `jellyfinpackages.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=49 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jellyfinmovements

**Categoría/plataforma:** Jellyfin. **Modelo:** `JellyfinMovement`. **Filas:** 2.

Historial de operaciones Jellyfin; también categoría Logs.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user` | `varchar(255) DEFAULT NULL` |
| `customer` | `varchar(255) DEFAULT NULL` |
| `description` | `text` |
| `credits` | `double NOT NULL DEFAULT '0'` |
| `movement_time` | `datetime DEFAULT NULL` |
| `server` | `varchar(255) DEFAULT NULL` |
| `parent_user_id` | `int DEFAULT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `parent_user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jellyfinpackages

**Categoría/plataforma:** Jellyfin. **Modelo:** `JellyfinPackage`. **Filas:** 0.

Paquetes de bibliotecas de un servidor Jellyfin.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `jellyfinserver_id` | `bigint unsigned NOT NULL` |
| `libraries` | `text NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `jellyfinpackages_jellyfinserver_id_foreign` (`jellyfinserver_id`)`

**Foreign keys:**

- `CONSTRAINT `jellyfinpackages_jellyfinserver_id_foreign` FOREIGN KEY (`jellyfinserver_id`) REFERENCES `jellyfinservers` (`id`)`

**Relaciones propuestas:** `jellyfinserver_id` → `jellyfinservers.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jellyfinservers

**Categoría/plataforma:** Jellyfin. **Modelo:** `JellyfinServer`. **Filas:** 2.

Conexión API Jellyfin, estado y configuración de acceso.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `host` | `varchar(255) NOT NULL` |
| `port` | `varchar(10) DEFAULT NULL` |
| `api_key` | `varchar(255) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `status` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `with_live_tv` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `is_dead` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `is_demo` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `custom_config` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `json_custom_config` | `text` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=44 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### jobs

**Categoría/plataforma:** Sistema. **Modelo:** `QueuedJob (infraestructura)`. **Filas:** 0.

Cola persistente; timestamps UNIX y payload serializado.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `queue` | `varchar(255) NOT NULL` |
| `payload` | `longtext NOT NULL` |
| `attempts` | `tinyint unsigned NOT NULL` |
| `reserved_at` | `int unsigned DEFAULT NULL` |
| `available_at` | `int unsigned NOT NULL` |
| `created_at` | `int unsigned NOT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `jobs_queue_index` (`queue`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### logins

**Categoría/plataforma:** Logs. **Modelo:** `LoginAttempt`. **Filas:** 8.

Historial de acceso, IP, intentos y passcode.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user_id` | `bigint unsigned DEFAULT NULL` |
| `date` | `datetime NOT NULL` |
| `ip` | `varchar(255) NOT NULL` |
| `failed_attempts` | `int NOT NULL DEFAULT '0'` |
| `passcode` | `varchar(255) DEFAULT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `logins_user_id_foreign` (`user_id`)`

**Foreign keys:**

- `CONSTRAINT `logins_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### menu_items

**Categoría/plataforma:** Sistema. **Modelo:** `LegacyMenuItem`. **Filas:** 43.

Entradas y jerarquía de navegación Voyager.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `menu_id` | `int unsigned DEFAULT NULL` |
| `title` | `varchar(255) NOT NULL` |
| `url` | `varchar(255) NOT NULL` |
| `target` | `varchar(255) NOT NULL DEFAULT '_self'` |
| `icon_class` | `varchar(255) DEFAULT NULL` |
| `color` | `varchar(255) DEFAULT NULL` |
| `parent_id` | `int DEFAULT NULL` |
| `order` | `int NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `route` | `varchar(255) DEFAULT NULL` |
| `parameters` | `text` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `menu_items_menu_id_foreign` (`menu_id`)`

**Foreign keys:**

- `CONSTRAINT `menu_items_menu_id_foreign` FOREIGN KEY (`menu_id`) REFERENCES `menus` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `menu_id` → `menus.id`; `parent_id` → `menu_items.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=55 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### menus

**Categoría/plataforma:** Sistema. **Modelo:** `LegacyMenu`. **Filas:** 1.

Contenedores de menús Voyager.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `menus_name_unique` (`name`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### migrations

**Categoría/plataforma:** Sistema. **Modelo:** `MigrationRecord (infraestructura)`. **Filas:** 175.

Registro de migraciones ya aplicadas; no contiene su implementación.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `migration` | `varchar(255) NOT NULL` |
| `batch` | `int NOT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=186 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### movements

**Categoría/plataforma:** Plex. **Modelo:** `Movement`. **Filas:** 52.

Historial de operaciones Plex; también categoría Logs.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user` | `text NOT NULL` |
| `customer` | `text NOT NULL` |
| `description` | `text NOT NULL` |
| `credits` | `int NOT NULL` |
| `movement_time` | `datetime NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `server` | `text NOT NULL` |
| `parent_user_id` | `int DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `parent_user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=53 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### notifications

**Categoría/plataforma:** Notificaciones. **Modelo:** `PanelNotification`. **Filas:** 0.

Avisos propios, contenido, caducidad, lecturas y repetición.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `title` | `varchar(255) NOT NULL` |
| `poster` | `text` |
| `content` | `text NOT NULL` |
| `button_link_action` | `text` |
| `date_to` | `date NOT NULL` |
| `reads` | `text` |
| `is_active` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `each_hours` | `int DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### packages

**Categoría/plataforma:** Plex. **Modelo:** `Package`. **Filas:** 0.

Paquetes de bibliotecas de un servidor Plex.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `server_id` | `bigint unsigned NOT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `libraries` | `text NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `packages_server_id_foreign` (`server_id`)`

**Foreign keys:**

- `CONSTRAINT `packages_server_id_foreign` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`)`

**Relaciones propuestas:** `server_id` → `servers.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### password_resets

**Categoría/plataforma:** Usuarios. **Modelo:** `PasswordReset (repositorio)`. **Filas:** 0.

Tokens de recuperación; sin clave primaria, gestionar con el broker.

| Columna | Definición SQL |
|---|---|
| `email` | `varchar(255) NOT NULL` |
| `token` | `varchar(255) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `KEY `password_resets_email_index` (`email`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### permission_role

**Categoría/plataforma:** Usuarios. **Modelo:** `PermissionRole (Pivot)`. **Filas:** 308.

Permisos asignados a roles, clave primaria compuesta.

| Columna | Definición SQL |
|---|---|
| `permission_id` | `bigint unsigned NOT NULL` |
| `role_id` | `bigint unsigned NOT NULL` |

**Índices:**

- `PRIMARY KEY (`permission_id`,`role_id`)`
- `KEY `permission_role_permission_id_index` (`permission_id`)`
- `KEY `permission_role_role_id_index` (`role_id`)`

**Foreign keys:**

- `CONSTRAINT `permission_role_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE`
- `CONSTRAINT `permission_role_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `permission_id` → `permissions.id`; `role_id` → `roles.id`.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### permissions

**Categoría/plataforma:** Usuarios. **Modelo:** `Permission`. **Filas:** 116.

Catálogo de permisos por clave y recurso.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `key` | `varchar(255) NOT NULL` |
| `table_name` | `varchar(255) DEFAULT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `permissions_key_index` (`key`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=122 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### personal_access_tokens

**Categoría/plataforma:** Usuarios. **Modelo:** `PersonalAccessToken`. **Filas:** 0.

Tokens personales y propietario polimórfico.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `tokenable_type` | `varchar(255) NOT NULL` |
| `tokenable_id` | `bigint unsigned NOT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `token` | `varchar(64) NOT NULL` |
| `abilities` | `text` |
| `last_used_at` | `timestamp NULL DEFAULT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `personal_access_tokens_token_unique` (`token`)`
- `KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### plans

**Categoría/plataforma:** Ventas. **Modelo:** `Plan`. **Filas:** 0.

Planes comerciales: precio y cantidad de créditos.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `price` | `decimal(10,2) NOT NULL` |
| `credits` | `int NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### proxies

**Categoría/plataforma:** Configuración. **Modelo:** `Proxy`. **Filas:** 0.

Direcciones y puertos de proxies heredados.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `ip` | `varchar(100) NOT NULL` |
| `port` | `varchar(10) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=202 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### roles

**Categoría/plataforma:** Usuarios. **Modelo:** `Role`. **Filas:** 5.

Roles heredados del panel.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `display_name` | `varchar(255) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `roles_name_unique` (`name`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### server_libraries

**Categoría/plataforma:** Plex. **Modelo:** `ServerLibrary`. **Filas:** 0.

Identificadores de bibliotecas remotas Plex por servidor.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `server_id` | `bigint unsigned NOT NULL` |
| `library_id` | `varchar(100) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `server_libraries_server_id_foreign` (`server_id`)`

**Foreign keys:**

- `CONSTRAINT `server_libraries_server_id_foreign` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE`

**Relaciones propuestas:** `server_id` → `servers.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=32 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### servers

**Categoría/plataforma:** Plex. **Modelo:** `Server`. **Filas:** 3.

Servidores Plex, token, capacidad, permisos y respuesta remota.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `url` | `varchar(255) NOT NULL` |
| `token` | `varchar(255) NOT NULL` |
| `is_demo` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `status` | `tinyint(1) NOT NULL DEFAULT '1'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `accounts_count` | `int DEFAULT NULL` |
| `user_id` | `int DEFAULT NULL` |
| `limit_accounts` | `int DEFAULT '0'` |
| `is_plex_pass` | `tinyint(1) NOT NULL DEFAULT '0'` |
| `local_name` | `varchar(255) DEFAULT NULL` |
| `library_section_for_remove` | `varchar(255) DEFAULT NULL` |
| `last_token_update` | `datetime DEFAULT NULL` |
| `json_data` | `text` |
| `is_dead` | `tinyint(1) NOT NULL DEFAULT '0'` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=159 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### sessions

**Categoría/plataforma:** Plex. **Modelo:** `PlexSession`. **Filas:** 0.

Registro mínimo de sesiones Plex; no son sesiones de autenticación web.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `server_id` | `int NOT NULL` |
| `plex_user_id` | `int NOT NULL` |
| `plex_session_id` | `varchar(255) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** `server_id` → `servers.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=616 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### settings

**Categoría/plataforma:** Configuración. **Modelo:** `Setting`. **Filas:** 110.

Parámetros por clave: presentación, seguridad, Telegram, pagos y plataformas.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `key` | `varchar(255) NOT NULL` |
| `display_name` | `varchar(255) NOT NULL` |
| `value` | `text` |
| `details` | `text` |
| `type` | `varchar(255) NOT NULL` |
| `order` | `int NOT NULL DEFAULT '1'` |
| `group` | `varchar(255) DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `settings_key_unique` (`key`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=161 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### ticket_categories

**Categoría/plataforma:** Otros. **Modelo:** `TicketCategory`. **Filas:** 4.

Categorías de soporte.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `name` | `varchar(255) NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### ticket_messages

**Categoría/plataforma:** Otros. **Modelo:** `TicketMessage`. **Filas:** 2.

Mensajes y adjuntos de tickets.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `ticket_id` | `bigint unsigned NOT NULL` |
| `message` | `text NOT NULL` |
| `attachments` | `json DEFAULT NULL` |
| `type` | `enum('support','user') NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `ticket_messages_ticket_id_foreign` (`ticket_id`)`

**Foreign keys:**

- `CONSTRAINT `ticket_messages_ticket_id_foreign` FOREIGN KEY (`ticket_id`) REFERENCES `tickets` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `ticket_id` → `tickets.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### tickets

**Categoría/plataforma:** Otros. **Modelo:** `Ticket`. **Filas:** 1.

Tickets de soporte con autor polimórfico y plataforma.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `type` | `enum('intern','public') NOT NULL` |
| `user_id` | `bigint unsigned NOT NULL` |
| `user_type` | `varchar(255) NOT NULL` |
| `platform` | `enum('emby','jellyfin','plex') NOT NULL` |
| `ticket_category_id` | `bigint unsigned DEFAULT NULL` |
| `title` | `varchar(255) NOT NULL` |
| `description` | `text NOT NULL` |
| `attachments` | `json DEFAULT NULL` |
| `status` | `enum('waiting','in_progress','closed') NOT NULL DEFAULT 'waiting'` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `tickets_user_id_user_type_index` (`user_id`,`user_type`)`
- `KEY `tickets_ticket_category_id_foreign` (`ticket_category_id`)`

**Foreign keys:**

- `CONSTRAINT `tickets_ticket_category_id_foreign` FOREIGN KEY (`ticket_category_id`) REFERENCES `ticket_categories` (`id`) ON DELETE SET NULL`

**Relaciones propuestas:** `ticket_category_id` → `ticket_categories.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### translations

**Categoría/plataforma:** Sistema. **Modelo:** `LegacyTranslation`. **Filas:** 0.

Traducciones identificadas por tabla, columna, registro e idioma.

| Columna | Definición SQL |
|---|---|
| `id` | `int unsigned NOT NULL AUTO_INCREMENT` |
| `table_name` | `varchar(255) NOT NULL` |
| `column_name` | `varchar(255) NOT NULL` |
| `foreign_key` | `int unsigned NOT NULL` |
| `locale` | `varchar(255) NOT NULL` |
| `value` | `text NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `translations_table_name_column_name_foreign_key_locale_unique` (`table_name`,`column_name`,`foreign_key`,`locale`)`

**Foreign keys:**

Sin FK declaradas.

**Relaciones propuestas:** Sin belongsTo simple local identificado; ver relaciones especiales/pivots si corresponde.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### user_roles

**Categoría/plataforma:** Usuarios. **Modelo:** `UserRole (Pivot)`. **Filas:** 0.

Roles adicionales de usuarios; clave primaria compuesta.

| Columna | Definición SQL |
|---|---|
| `user_id` | `bigint unsigned NOT NULL` |
| `role_id` | `bigint unsigned NOT NULL` |

**Índices:**

- `PRIMARY KEY (`user_id`,`role_id`)`
- `KEY `user_roles_user_id_index` (`user_id`)`
- `KEY `user_roles_role_id_index` (`role_id`)`

**Foreign keys:**

- `CONSTRAINT `user_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE`
- `CONSTRAINT `user_roles_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE`

**Relaciones propuestas:** `role_id` → `roles.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### user_servers

**Categoría/plataforma:** Plex. **Modelo:** `UserServer (Pivot)`. **Filas:** 1.

Asignación de servidores Plex a usuarios del panel.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `user_id` | `bigint unsigned NOT NULL` |
| `server_id` | `bigint unsigned NOT NULL` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `KEY `user_servers_server_id_foreign` (`server_id`)`
- `KEY `user_servers_user_id_foreign` (`user_id`)`

**Foreign keys:**

- `CONSTRAINT `user_servers_server_id_foreign` FOREIGN KEY (`server_id`) REFERENCES `servers` (`id`) ON DELETE CASCADE ON UPDATE CASCADE`
- `CONSTRAINT `user_servers_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE`

**Relaciones propuestas:** `server_id` → `servers.id`; `user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.

### users

**Categoría/plataforma:** Usuarios. **Modelo:** `User`. **Filas:** 8.

Identidades del panel, rol principal, jerarquía, saldo y servicios.

| Columna | Definición SQL |
|---|---|
| `id` | `bigint unsigned NOT NULL AUTO_INCREMENT` |
| `role_id` | `bigint unsigned DEFAULT NULL` |
| `name` | `varchar(255) NOT NULL` |
| `email` | `varchar(255) NOT NULL` |
| `avatar` | `varchar(255) DEFAULT 'users/default.png'` |
| `email_verified_at` | `timestamp NULL DEFAULT NULL` |
| `password` | `varchar(255) NOT NULL` |
| `passcode` | `varchar(255) DEFAULT NULL` |
| `remember_token` | `varchar(100) DEFAULT NULL` |
| `settings` | `text` |
| `created_at` | `timestamp NULL DEFAULT NULL` |
| `updated_at` | `timestamp NULL DEFAULT NULL` |
| `total_credits` | `int NOT NULL DEFAULT '0'` |
| `status` | `enum('active','inactive') NOT NULL DEFAULT 'active'` |
| `telegram_chat_id` | `varchar(32) DEFAULT NULL` |
| `parent_user_id` | `int DEFAULT NULL` |
| `aditional_info` | `text` |
| `service` | `text` |
| `prefix` | `varchar(100) DEFAULT NULL` |

**Índices:**

- `PRIMARY KEY (`id`)`
- `UNIQUE KEY `users_email_unique` (`email`)`
- `KEY `users_role_id_foreign` (`role_id`)`
- `KEY `users_telegram_chat_id_index` (`telegram_chat_id`)`

**Foreign keys:**

- `CONSTRAINT `users_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`)`

**Relaciones propuestas:** `role_id` → `roles.id`; `parent_user_id` → `users.id`.

**Opciones originales:** `ENGINE=InnoDB AUTO_INCREMENT=120 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.


## 12. Verificación realizada y puerta de entrada a FASE 1

- Lectura completa de 252278 bytes; 49 CREATE TABLE y 28 INSERT procesados; 1377 filas con aridad validada contra 446 columnas.
- Catálogo con todas las tablas, sus índices, FK, enums, defaults y propuestas de modelo.
- Comprobación estática de referencias declaradas y relaciones simples propuestas; revisión de metadatos BREAD, roles, configuración, migraciones y JSON sin volcar valores sensibles.
- Confrontación funcional con las ocho capturas; la sexta y la octava repiten el formulario de cliente.
- No se importó ni ejecutó SQL, no se llamó a APIs y no se modificó esquema. La compatibilidad de restauración y la de ejecución Laravel aún no están probadas.
- php artisan test, route:list y optimize:clear no corresponden en esta fase: el directorio inicial está vacío y todavía no existe artisan ni una aplicación. No se presentan esas comprobaciones como aprobadas.

### Plan concreto para FASE 1, sujeto a aprobación

1. Verificar versiones disponibles de PHP/Composer y elegir una versión Laravel mantenida compatible con el hosting; no fijar Laravel 11 sin comprobar soporte.
2. Preparar una base aislada e importar el dump original; validar recuentos, collation y restricciones sin tocar producción.
3. Crear modelos explícitos sobre las tablas existentes y lectores compatibles para JSON/secretos, con pruebas usando fixtures sintéticos.
4. Configurar autenticación contra users, sesión web sin colisionar con sessions, recuperación de contraseña y autorización con roles/permisos actuales.
5. Crear layout Blade/Bootstrap y navegación de las capturas. Mantener integraciones remotas y scheduler de escritura desactivados hasta implementar sus contratos.
6. Ejecutar pruebas, listado de rutas y limpieza de caché cuando exista la aplicación; corregir errores antes de pasar de fase.

Pendientes que no bloquean esta auditoría: credenciales de una base de pruebas, versión del hosting, muestra de packages.libraries, semántica de tarifas/jerarquía, decisión sobre referencia padre ausente y verificación oficial de APIs. No se deducen endpoints del contenido del backup.

**FASE 0 terminada. Esperar aprobación del usuario antes de crear el proyecto o comenzar FASE 1.**

