Cuatro días. Cuatro sesiones. Una superficie de funcionalidades masiva (servidores de base de datos autónomos con 5 motores, interfaces admin, gestión de dominios, respaldos, usuarios, control de acceso). Y ahora, la ronda de consolidación.
El problema del trabajo incremental
Cuando construyes una funcionalidad a lo largo de múltiples sesiones, cada sesión optimiza localmente. La sesión 1 construye el núcleo. La sesión 2 audita y corrige bugs. La sesión 3 agrega las piezas faltantes (fix de race en el aprovisionamiento, detección de colisión de dominio admin). La sesión 4 agrega la paridad de la vista general y la seguridad de la interfaz admin.
Cada sesión es individualmente sólida. Pero después de 4 días: - Un archivo handler monolítico de 3 194 líneas ha crecido orgánicamente - El ejecutor de respaldos programados tiene un bug que refleja un fix ya aplicado en la ruta de disparo manual - Los dominios de los servidores de base de datos viven en columnas separadas, invisibles para la página global de dominios - La arquitectura ha derivado de los patrones originales de stack-app
Este es el problema de «funciona individualmente, no forma un todo coherente». Y por eso tenemos una ronda de consolidación dedicada.
Lo que encontró la Ronda 4
El fallo silencioso de los respaldos
El hallazgo crítico estaba en sh0-backup/src/scheduler.rs. El handler de trigger_backup manual había sido corregido el 8 de abril para probar DatabaseServer::find_by_id antes de App::find_by_id en la rama de respaldo por volumen. Pero el ejecutor de respaldos programados — una ruta de código completamente separada — todavía solo buscaba apps. Cada respaldo programado de Redis por volumen fallaba silenciosamente.
Esta es una clase de bug que solo se descubre a través de una auditoría cross-path. El fix se había aplicado la noche del 8 de abril, pero solo en una de dos rutas de código idénticas. El prompt de auditoría lo señaló específicamente como el item 20, y verificarlo tomó 30 segundos de lectura.
La tabla unificada de dominios
El cambio arquitectónico más importante fue hacer domains.app_id nullable y agregar db_server_id. Este era el item diferido de mayor impacto a lo largo de las 4 sesiones.
La estrategia de migración para SQLite (que no soporta ALTER COLUMN ... DROP NOT NULL):
1. Renombrar la tabla existente a _domains_old
2. Crear una nueva tabla con restricciones relajadas + CHECK
3. Copiar todas las filas existentes
4. Eliminar la tabla vieja
5. Rellenar retroactivamente desde las columnas database_servers.server_domain / admin_domain existentes
La restricción CHECK ((app_id IS NOT NULL AND db_server_id IS NULL) OR (app_id IS NULL AND db_server_id IS NOT NULL)) asegura que exactamente un propietario esté definido.
El efecto cascada tocó 14 archivos en 6 crates. Cada sitio de construcción Domain { app_id: x, ... } necesitó pasar a Some(x) y db_server_id: None. Cada comparación domain.app_id != app_id necesitó .as_deref(). El tipo DomainResponse ganó ambos campos opcionales. Y el endpoint global de dominios cambió de list_all_with_app_name a list_all_with_owner_name (una consulta UNION entre apps y database_servers).
El refactoring del handler
Un archivo handler de 3 194 líneas es un code smell, pero también es un resultado natural de 4 días de trabajo incremental en funcionalidades. La división en 9 sub-módulos fue mecánica pero importante:
database_servers/
mod.rs (293 líneas) -- helpers compartidos
crud.rs (586 líneas) -- create/get/list/update/delete
lifecycle.rs (218 líneas) -- start/stop/connection-info
databases.rs (198 líneas) -- bases de datos gestionadas
users.rs (332 líneas) -- usuarios de bases de datos + ACL
access.rs (589 líneas) -- grants + acceso externo
admin_ui.rs (673 líneas) -- interfaz admin + dominio admin
domains.rs (393 líneas) -- dominio del servidor
logs.rs (68 líneas) -- streaming de logs WebSocketLa decisión clave fue la visibilidad pub(super) para funciones entre módulos. assign_admin_domain vive en admin_ui.rs pero es llamado desde crud.rs (durante la creación del servidor). Hacerlo pub(super) lo mantiene interno al módulo mientras permite el acceso desde módulos hermanos.
La metodología en acción
La metodología build-audit-audit-decide busca la convergencia a través de perspectivas diversas:
- Sesión 1 construye rápido y con optimismo
- Sesiones 2-3 atrapan bugs y agregan las piezas faltantes
- Sesión 4 (esta) da un paso atrás y pregunta: «¿el conjunto forma un todo coherente?»
La respuesta fue: esencialmente sí, con dos correcciones críticas (ejecutor de respaldos, unificación de dominios) y una mejora estructural (división del handler). Cada uno de estos puntos estaba explícitamente identificado en el prompt de auditoría, que fue a su vez producto de observaciones acumuladas a lo largo de 4 sesiones.
Lo que queda
La Ronda 4 difirió explícitamente 10 items a la Ronda 5, cada uno con una justificación específica: - Rediseño visual de las pestañas Respaldos/Logs/Configuración (cosmético, no funcional) - Correcciones de accesibilidad (importante pero no bloqueante) - Banner de detección de contenedor admin UI inseguro (requiere Docker inspect en tiempo de ejecución) - Interfaz de dominio personalizado para db-servers (arquitectónicamente desbloqueado por esta ronda)
La distinción importa: son diferimientos conscientes con justificación documentada, no elementos olvidados. La siguiente sesión sabe exactamente por dónde empezar.
Conclusión
La ronda de consolidación cuesta tiempo al inicio pero previene la acumulación lenta de inconsistencias que hace que el software sea difícil de mantener. Después de la Ronda 4, la superficie de servidores de base de datos está: - Arquitectónicamente integrada (tabla unificada de dominios) - Estructuralmente organizada (9 sub-módulos enfocados) - Funcionalmente completa para todas las rutas auditadas - Lista para verificación manual contra una checklist de pruebas de 25 puntos
Esa es la diferencia entre «funciona» y «es entregable».