Soporte ZH Consultores d124c65639 fix(backup): respaldar TODO (DB + secrets + caddy + systemd) atómicamente
Bug: el script previo solo respaldaba la BD donna. /etc/donna/donna.env,
/etc/caddy/Caddyfile y los systemd units quedaban sin respaldo. Además,
si mariadb-dump fallaba a media corrida el .sql.gz parcial quedaba
como si fuera válido (se vio hoy con un archivo de 20 bytes).

Cambios:
- Output ahora es un solo tar.gz con DB + GRANTs + configs + MANIFEST
  (sha256 de cada archivo incluido)
- Atómico: tar se escribe a .tmp y se renombra solo al final
- Incluye SHOW GRANTS para poder recrear el usuario donna_app en un
  server nuevo sin tener que conocer el password de root del MariaDB
- Limpieza de archivos .sql.gz legacy también en la retención
- README documenta el procedimiento de restore en server nuevo
2026-06-04 22:46:43 -05:00
2026-05-11 19:10:36 +00:00

Donna — Portal interno AALN Law

Portal interno de la firma Arias, Abrego, López & Noriega (aalnlaw.com) que aloja las herramientas operativas del equipo: control de escrituras y trámites legales, registro de horas, calendario, panel de administración y auditoría.

Producción: https://donna.aal-law.com


Arquitectura

Navegador
   │  HTTPS
   ▼
Cloudflare → Caddy infra (SSL termination)
                   │  HTTP :80
                   ▼
            ┌──────────────────────┐
            │ Caddy (este server)  │  /opt/donna/public
            │ rewrite *.php        │
            └──────┬───────────────┘
                   │  unix socket
                   ▼
            ┌──────────────────────┐
            │ PHP-FPM 8.4          │
            └──────┬───────────────┘
                   ▼
            ┌──────────────────────┐
            │ MariaDB 11.8         │  DB: donna
            └──────────────────────┘

Stack:

  • PHP 8.4-fpm (/run/php/php8.4-fpm.sock)
  • Caddy 2.6 en :80 (terminación SSL en Cloudflare → infra Caddy)
  • MariaDB 11.8 (127.0.0.1:3306), zona horaria America/Panama (UTC-5)
  • Autenticación OAuth2 contra Mailcow (mail.aal-law.com)
  • Frontend: HTML + JS vanilla con window.DONNA inyectado por Bootstrap::inject()
  • Exportación a Excel/PDF cliente-side con SheetJS y window.print()
  • Backups diarios automáticos vía systemd timer

Roles y permisos

Rol Descripción Visibilidad
admin Programador / modo Dios oculto No aparece en dropdowns ni en listados para socios
socio Acceso total al sistema Ve todo, gestiona usuarios y auditoría
abogado Puede crear trámites y escrituras Solo ve trámites donde es creador, abogado solicitante o involucrado
usuario Apoyo administrativo (pasantes, asistentes) Solo ve trámites donde está involucrado; crea escrituras

Helpers en src/Auth.php:

  • Auth::isSocio() — true para admin y socio
  • Auth::isAdmin() — solo admin
  • Auth::canCreate() — socio, abogado, admin (trámites)
  • Auth::canDelete() — igual que canCreate

Usuarios nuevos al loguearse por primera vez nacen con rol usuario.


Estructura del repo

donna/
├── public/                       ← DocumentRoot
│   ├── index.php                 ← Landing + grid de apps + "Eventos de hoy"
│   ├── assets/
│   │   ├── donna-export.css      ← UI compartida del botón Exportar
│   │   └── donna-export.js       ← Helpers DonnaExport.toExcel / toPDF / attach
│   ├── auth/
│   │   ├── login.php             ← Inicia OAuth flow
│   │   ├── callback.php          ← Recibe code de Mailcow + crea/actualiza user
│   │   └── logout.php
│   ├── admin/
│   │   ├── index.php             ← Auth-guard + view (solo socios/admin)
│   │   └── api.php               ← CRUD usuarios + query del audit log
│   ├── calendario/
│   │   ├── index.php
│   │   └── api.php               ← CRUD calendar_events (manuales)
│   ├── escrituras/
│   │   ├── index.php
│   │   └── api.php               ← CRUD escrituras + historial
│   ├── horas/
│   │   ├── index.php
│   │   └── api.php               ← Reporte de horas con filtros + agregaciones
│   └── tramites/
│       ├── index.php
│       └── api.php               ← CRUD trámites + observaciones + horas
├── src/                          ← Librerías PHP compartidas (fuera de DocumentRoot)
│   ├── Apps.php                  ← Catálogo de apps del portal (filtra por isSocio)
│   ├── AuditLog.php              ← Helper para registrar acciones en audit_log
│   ├── Auth.php                  ← Sesión + OAuth guard + roles + whitelist
│   ├── Bootstrap.php             ← Inyecta window.DONNA en cada view HTML
│   ├── Config.php                ← Lee /etc/donna/donna.env, fija TZ a Panama
│   ├── Db.php                    ← PDO singleton, fija session TZ a -05:00
│   ├── Lookups.php               ← Queries para dropdowns (abogados, usuarios)
│   └── Oauth.php                 ← Cliente HTTP a Mailcow
├── views/                        ← Templates HTML servidos por PHP tras auth
│   ├── admin.html                ← Panel de Control (2 pestañas: Usuarios + Log)
│   ├── calendario.html           ← Vista año/mes/semana/día con eventos manuales
│   ├── escrituras.html
│   ├── horas.html                ← Reporte de horas con desglose por trámite
│   └── tramites.html             ← 4 tabs: Admin / Corp / Mig / Litigios
├── sql/                          ← Migraciones (aplicar manualmente, ordenadas)
│   ├── 001_core.sql              ← users
│   ├── 002_escrituras.sql        ← escrituras_tramites + escrituras_historial
│   ├── 003_escrituras_entrada.sql
│   ├── 004_tramites.sql          ← tramites_legales + tramites_seq
│   ├── 005_users_roles.sql       ← Roles socio/abogado/usuario
│   ├── 006_tramites_involucrados.sql
│   ├── 007_drop_apellido.sql     ← Nombre Completo en toda la app
│   ├── 008_admin_role.sql        ← Rol admin (programador)
│   ├── 009_panama_timezone.sql   ← Convertir timestamps históricos UTC → -05:00
│   ├── 010_audit_log.sql         ← Tabla audit_log
│   ├── 011_calendar_events.sql   ← Eventos manuales del calendario
│   ├── 012_drop_users_orden.sql  ← Simplificación: orden alfabético siempre
│   ├── 013_normalize_abogado_names.sql  ← Backfill nombres legacy
│   └── 014_add_user_id_to_hours_obs.sql ← FK user_id en hours/obs
├── bin/
│   └── donna-backup.sh           ← mysqldump diario, retención 14 días
├── docs/                         ← Diseños y archivos de infra de referencia
├── donna.env.example             ← Plantilla del archivo de secrets
└── README.md

Fuera del repo (en el servidor):

  • /etc/donna/donna.env — secrets reales (640 root:www-data)
  • /etc/caddy/Caddyfile — config del Caddy local
  • /etc/systemd/system/donna-backup.{service,timer} — backups
  • /var/log/donna/ — access logs
  • /var/backups/donna/ — dumps SQL

Apps

Registradas en src/Apps.php::catalog($isSocio). El landing las renderiza como cards.

Slug Estado Visible a Ruta Tablas DB
escrituras Live Todos /escrituras/ escrituras_tramites, escrituras_historial
tramites Live Todos (filtrado por canView()) /tramites/ tramites_legales, tramites_involucrados, tramites_obs, tramites_hours, tramites_seq
calendario Live Todos /calendario/ calendar_events (+ deriva de escrituras/trámites)
horas Live Todos (no-socios solo sus propias) /horas/ tramites_hours (vista)
admin Live Solo socios/admin /admin/ users, audit_log

Control de Escrituras (escrituras)

Seguimiento de trámites notariales: estado, abogado, notaría, alteración de turno, historial y auditoría. Estatus: Solicitado → Recibido → En Proceso → Protocolizado → En Registro → Inscrito.

Control de Trámites Legales (tramites)

4 departamentos (Administrativo, Corporativo/Civil, Migración, Litigios) con campos específicos por depto. Cada trámite tiene:

  • Involucrados (M:N con users): determinan acceso y dropdowns
  • Observaciones (tramites_obs): timeline de comentarios
  • Horas Incurridas (tramites_hours): registro en bloques de 15 min (0:15, 0:30… 12:00). Editable/eliminable por autor, involucrados o socios. Almacena abogado (string) + user_id (FK) para que el filtro siga funcionando si renombras a un usuario.

Calendario (calendario)

Vista año/mes/semana/día. Eventos derivados automáticamente de Escrituras (fecha de solicitud) y Trámites (solicitud, vencimiento migración, término procesal). Más eventos manuales (calendar_events): reserva de sala, cumpleaños, recordatorio, evento general — con horario, ubicación y creador.

Reporte de Horas (horas)

Filtros por rango de fechas y por usuario. Tarjetas por abogado expandibles → muestran el desglose de trámites con total de horas por trámite. KPIs (Total horas, Entradas, Personas). No-socios ven solo sus propias horas.

Panel de Control (admin)

Pestaña Usuarios: CRUD, asignación de roles, activación/desactivación. La columna placeholder original se va reemplazando conforme los abogados se loguean. Listas siempre alfabéticas.

Pestaña Registro de Transacciones: query de audit_log con filtros por fecha, usuario, módulo, acción y búsqueda libre. Cada login, logout, create, update, delete, set_status, add_obs, add_hours y similares queda registrado.


Exportación a Excel + PDF

Módulo compartido en public/assets/donna-export.{js,css} con la misma UI (botón "Exportar ▾" + dropdown verde Excel / rojo PDF) en los 6 puntos del sistema:

Módulo Qué exporta
Escrituras Trámites filtrados
Trámites (cada tab) Filas filtradas del tab activo
Calendario visibleEvents() (respeta chips de origen + búsqueda)
Reporte de Horas Entradas filtradas
Panel · Usuarios Lista visible (respeta filtros)
Panel · Registro de Transacciones Entradas cargadas

API JS:

DonnaExport.buttonHtml(id?)   // returns HTML del botón + menú
DonnaExport.attach(wrapId, { onExcel, onPDF })
DonnaExport.toExcel({ filename, sheetName, headers, rows, columnWidths, onEmpty })
DonnaExport.toPDF  ({ filename, title, headers, rows, pdfCols?, orientation?,
                      statusColIndex?, statusColors?, footer?, onEmpty })
  • Excel: .xlsx con header en navy, ancho de columnas personalizable, UTF-8 nativo.
  • PDF: abre ventana nueva con HTML estilizado + window.print() (paisaje A4 por defecto).

Eventos de hoy (Home)

Debajo de las tarjetas de módulos, el portal muestra los eventos del día:

  • Eventos manuales (cumpleaños, sala de conferencias, recordatorios): visibles a todos
  • Escrituras con fecha_solicitud = hoy: visibles a todos
  • Trámites con fecha_solicitud / vencimiento / término = hoy: solo a creador, abogado solicitante o involucrado (mismo canView() que el módulo)

Cada card es clickable y abre la ficha correspondiente.


Setup en un servidor nuevo

# 1. Paquetes del SO (Debian 13)
apt install -y caddy php8.4-fpm php8.4-mysql php8.4-curl \
                php8.4-mbstring php8.4-intl php8.4-zip mariadb-server

# 2. Zona horaria del SO
timedatectl set-timezone America/Panama

# 3. Base de datos
mariadb -e "CREATE DATABASE donna CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb -e "CREATE USER 'donna_app'@'localhost' IDENTIFIED BY 'XXX';
            GRANT ALL ON donna.* TO 'donna_app'@'localhost';"

# 4. Código
git clone https://git.zhconsultores.com/ZH-Consultores/donna.git /opt/donna
chown -R www-data:www-data /opt/donna

# 5. Migraciones (en orden)
for f in /opt/donna/sql/0*.sql; do mariadb donna < "$f"; done

# 6. Secrets
mkdir -p /etc/donna
cp /opt/donna/donna.env.example /etc/donna/donna.env
# Editar /etc/donna/donna.env con valores reales (OAuth, DB_PASS, APP_SECRET)
chown root:www-data /etc/donna/donna.env
chmod 640 /etc/donna/donna.env

# 7. Caddy y backups: ver docs/infra/ para Caddyfile y systemd units
systemctl reload caddy
systemctl enable --now donna-backup.timer

OAuth2 con Mailcow

  1. En https://mail.aal-law.com/admin → Access → OAuth2 Apps
  2. Crear app con Redirect URI = https://donna.aal-law.com/auth/callback
  3. Copiar Client ID y Client Secret a /etc/donna/donna.env

El flujo:

/  ──login──▶  /auth/login.php  ──redirect──▶  mail.aal-law.com/oauth/authorize
                                                       │
                                                       ▼  ?code=...&state=...
/auth/callback.php  ◀── redirect ◀── usuario autorizado
  │
  ├── valida state (anti-CSRF)
  ├── intercambia code → token
  ├── lee profile (email, nombre)
  ├── valida dominio @aal-law.com o @aalnlaw.com
  ├── inserta/actualiza fila en `users` (rol=usuario si es nuevo)
  ├── registra login en audit_log
  └── redirige a /

Sesión: cookie donna_session, path=/, Secure, HttpOnly, SameSite=Lax, lifetime 8h. SSO automático entre todas las apps del portal.


Patrones de código

Bootstrap::inject($viewPath)

Cada <app>/index.php hace:

require_once __DIR__ . '/../../src/Bootstrap.php';
Auth::requireAuth();
header('Content-Type: text/html; charset=utf-8');
Bootstrap::inject(__DIR__ . '/../../views/<app>.html');

Esto inyecta antes del primer </head>:

<script>window.DONNA = {
  me: 'email@aal-law.com',
  name: 'Nombre Completo',
  role: 'socio',
  isAdmin: false,
  isSocio: true,
  canCreate: true,
  canDelete: true,
  abogados: ['Mario Arias', ...],
  users: [{id, email, name, role}, ...]
};</script>

Importante: usa strpos/substr para reemplazar solo el primer </head> (no str_replace), porque algunos JS embebidos contienen </head> dentro de template literals que rompen el HTML si se inyecta ahí.

AuditLog::log($modulo, $accion, $entityType, $entityId, $entityLabel, $detalles?)

Falla silenciosamente (try/catch) para no romper el flujo principal. Llamado desde cada API después de operaciones de escritura. Los detalles van como JSON.

Visibilidad de trámites legales (canView())

Socio/admin → siempre
Resto       → creador, o abogado solicitante (match por users.name = t.abogado),
              o involucrado (M:N en tramites_involucrados)

FK user_id en hours/obs

Tanto tramites_hours como tramites_obs tienen abogado VARCHAR(100) (display) y user_id INT NULL (FK canónica ON DELETE SET NULL). Esto evita que renombrar un usuario en el Panel de Control desconecte sus entradas históricas de los filtros. Si lo borras, user_id queda NULL y el string abogado permanece como historia.


Identidad visual

Token Valor
--navy #1B3A6B
--navy-d #112446
--navy-l #2D5090
--gold #C9A84C
--gold-l #E8C96A
--gold-d #9E7C2E
--off #F7F6F3
Fuente headlines DM Serif Display
Fuente UI DM Sans

Gradient hero del landing: #0E1F3F → #1B3A6B → #2D5090.

Colores por departamento de trámites:

  • Admin: #6c5b95 · Corp: #9E7C2E · Mig: #2980b9 · Lit: #722f37

Colores por tipo de evento manual:

  • Reserva sala: #2e7d8f · Cumpleaños: #c2185b · Recordatorio: #e67e22 · Evento: #27ae60

Backups

donna-backup.timer ejecuta diariamente a las 03:00 (jitter ±5min) y genera /var/backups/donna/donna-YYYY-MM-DD_HHMMSS.tar.gz. Retención 14 días. Atómico: el .tmp se renombra al final solo si todo salió bien (no quedan archivos parciales).

Contenido del tarball:

donna-YYYY-MM-DD_HHMMSS/
├── MANIFEST.txt                ← inventario + tamaños + SHA-256 de cada archivo
├── db/
│   ├── schema-and-data.sql.gz  ← mariadb-dump completo (schema + data + routines + triggers + events)
│   └── grants.sql              ← SHOW GRANTS del usuario donna_app
└── etc/
    ├── donna/donna.env         ← secrets (OAuth, DB pass, APP_SECRET)
    ├── caddy/Caddyfile         ← config del Caddy local
    └── systemd/system/
        ├── donna-backup.service
        └── donna-backup.timer

Restaurar en un server nuevo:

TAR=/path/to/donna-YYYY-MM-DD_HHMMSS.tar.gz
tar -xzf "$TAR" -C /tmp && cd /tmp/donna-*
# 1. Configs
cp etc/donna/donna.env    /etc/donna/donna.env  && chmod 640 /etc/donna/donna.env
cp etc/caddy/Caddyfile    /etc/caddy/Caddyfile  && systemctl reload caddy
cp etc/systemd/system/*   /etc/systemd/system/  && systemctl daemon-reload
# 2. DB (crea DB y usuario primero)
mariadb -e "CREATE DATABASE donna CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
mariadb < db/grants.sql   # recrea el usuario y sus permisos
gunzip -c db/schema-and-data.sql.gz | mariadb donna
# 3. Verificar
sha256sum -c <(grep -E '^[a-f0-9]{64}' MANIFEST.txt)  # checksums OK?

Operación

Acción Comando
Reload Caddy systemctl reload caddy
Reload PHP-FPM systemctl reload php8.4-fpm
Ver logs Caddy tail -f /var/log/donna/access.log
Ver errores PHP journalctl -u php8.4-fpm -f
Backup manual /opt/donna/bin/donna-backup.sh
Próximo backup systemctl list-timers donna-backup.timer
Aplicar migración mariadb donna < /opt/donna/sql/0NN_<nombre>.sql
Consultar audit log Panel de Control → Registro de Transacciones

Agregar una app nueva

  1. Crear /opt/donna/public/<slug>/index.php con Bootstrap::inject(__DIR__ . '/../../views/<slug>.html') precedido de Auth::requireAuth().
  2. Crear /opt/donna/public/<slug>/api.php con Auth::requireAuth() (o requireSocio()).
  3. Crear /opt/donna/views/<slug>.html con el HTML + JS del módulo.
  4. Prefijar todas las tablas DB con <slug>_ y crear migración numerada en sql/0NN_<slug>.sql.
  5. Registrar en src/Apps.php::catalog() (con flag isSocio si aplica).
  6. Si tiene exportación, incluir:
    <link rel="stylesheet" href="/assets/donna-export.css">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
    <script src="/assets/donna-export.js"></script>
    
  7. Para escritura de auditoría, llamar AuditLog::log(...) después de cada operación.
  8. Commit + push.

Licencia

Software interno — sin licencia pública. Propiedad de Arias, Abrego, López & Noriega.

S
Description
No description provided
Readme
908 KiB
Languages
HTML 63.8%
PHP 32.3%
JavaScript 2.4%
Shell 1%
CSS 0.5%