diff --git a/README.md b/README.md index f1ecf7e7..a4fe20c8 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,21 @@ # Four-Points -Internal operations app for a hotel, complementing its commercial PMS (Opera): -logbook, parking, maintenance, groups, cashier, blacklist, staff scheduling, -shift checklists, F&B revenue and invoice backoffice. +Hotel operations app built from years of real front-desk work, with a shift +scheduler that solves the monthly roster with constraint programming. + +[![Live demo](https://img.shields.io/badge/Live_demo-one_click,_no_sign--up-2ea043?style=flat-square)](https://four-points.stackbp.es/login) +[![CI](https://github.com/bpstack/four-points/actions/workflows/ci.yml/badge.svg)](https://github.com/bpstack/four-points/actions/workflows/ci.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue?style=flat-square)](LICENSE) + +![Four-Points dashboard](docs/images/dashboard.png) + +It complements the hotel's commercial PMS (Opera): logbook, parking, +maintenance, groups, cashier, blacklist, staff scheduling, shift checklists, +F&B revenue and invoice backoffice. + +| Staff scheduling (OR-Tools) | Parking | +| --- | --- | +| ![Monthly roster generated by the solver](docs/images/scheduling.png) | ![Parking overview](docs/images/parking.png) | - **Frontend**: Next.js 16 (App Router), React 19, Tailwind CSS, NextUI. - **Backend**: Express 5 + TypeScript, MySQL 8, Cloudinary for files. @@ -23,6 +36,7 @@ Full documentation lives in [`docs/`](docs/general/README.md): - [Backoffice](docs/backoffice/README.md) - [Blacklist](docs/blacklist/README.md) - [Conciliation](docs/conciliation/README.md) +- [Public demo](docs/demo/README.md) — how the demo account works and when it resets ## Try it diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index e3261d3a..4a70615c 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -9,6 +9,14 @@ > sola. > > El **porqué** vive aquí; el estado actual vive en `SESSION.md`. +> +> **Documentos archivados:** lo que salió del repositorio está listado en +> [ADR-040](#adr-040--la-documentación-obsoleta-se-archiva-fuera-del-repositorio) +> (documentación obsoleta) y +> [ADR-041](#adr-041--los-documentos-de-la-publicación-se-archivan-juntos) (el +> proceso de publicación: `ROADMAP.md`, `GITCLEAN.md`, `VERIFY.md`), con el +> comando que recupera cada uno del historial. La copia local está en +> `docs/_archive/`, que git ignora. --- @@ -870,3 +878,26 @@ - **Consecuencias:** los documentos del proceso de publicación (`GITCLEAN.md`, `VERIFY.md`, `ROADMAP.md`, `SESSION.md`) se archivan igual cuando el repositorio sea público. + +## ADR-041 — Los documentos de la publicación se archivan juntos + +- **Estado:** ✅ aceptada (2026-10-05) +- **Fecha:** 2026-10-05 +- **Decisión:** `ROADMAP.md`, `GITCLEAN.md` y `VERIFY.md` pasan a + `docs/_archive/open-source-release/`, con un README que dice qué es cada + uno. Se recuperan del historial público: + + | Fichero | Comando | + | --- | --- | + | `ROADMAP.md` | `git show 5f0daa2:docs/ROADMAP.md` | + | `GITCLEAN.md` | `git show 025e76c:docs/GITCLEAN.md` | + | `VERIFY.md` | `git show 025e76c:docs/VERIFY.md` | + + Lo pendiente vive solo en `TODO.md`, y si + un punto depende de otro se dice en el propio punto. +- **Motivo:** describían el paso de privado a público, que terminó el + 2026-10-05: todas las fases cerradas, el historial reescrito y auditado, + ninguna fila de `VERIFY.md` sin probar o fallida (194 filas). Juntos cuentan + el proceso entero; sueltos en `docs/` parecían trabajo vivo. +- **Consecuencias:** las ADR anteriores que los citan no se tocan; el comando + de arriba los recupera. `SESSION.md` sigue en `docs/`. Aplica ADR-040. diff --git a/docs/GITCLEAN.md b/docs/GITCLEAN.md deleted file mode 100644 index f0a2ed9c..00000000 --- a/docs/GITCLEAN.md +++ /dev/null @@ -1,261 +0,0 @@ -# GITCLEAN — limpieza del historial antes de publicar - -> **Qué responde:** cómo se quita del historial de Git lo privado antes de -> publicar el repositorio. · **Quién lo lee:** quien ejecute la fase 2 -> (`ROADMAP.md`). · **Cómo se poda:** al terminar la fase 2, se añade el -> resultado de la auditoría y los hashes cambiados. - -Origen: sección 2 de `OPEN-SOURCE.md` (plan del propietario, hoy en -`docs/_archive/OPEN-SOURCE.md`), más lo decidido después (ADR-006, ADR-026, -ADR-027). - ---- - -## Objetivo - -Reescribir el historial de forma controlada para que los archivos y directorios -privados desaparezcan de **todos los commits, ramas y tags** que se publiquen, -conservando intacto todo lo demás. - -Algunos archivos contienen información privada de la empresa que no puede -aparecer, bajo ninguna circunstancia, en el repositorio público. Como pueden -estar en commits antiguos, no basta con `.gitignore`: hay que quitarlos de todo -el historial. - -## Cuándo y dónde - -- **Después de la fase 1c**: con todo `TODO.md` resuelto y probado en producción - (ADR-027). -- **Sobre un clon**, no sobre el repositorio actual, que sigue privado. El - historial limpio se sube a un **repositorio nuevo y público** (ADR-006). -- **Solo dos ramas** se limpian y se publican (ADR-035): `main` y - `claude/compassionate-planck-gh6aof`. Las otras cuatro no tienen ningún commit - fuera de `main` (comprobado el 2026-10-05). Sin tags; las referencias - `pull/*` de GitHub no se publican. -- **Dónde**: una carpeta de trabajo fuera del repositorio, con - `git-filter-repo` 2.47.0 en un entorno virtual propio (2026-10-05). - -### Estado (2026-10-05) - -- ✅ Paso 0: copia `four-points-backup.git` (`clone --mirror`, no se toca) y - `four-points-backup.bundle` (10,8 MB, `git bundle verify` correcto). -- ✅ Análisis de rutas y secretos (abajo, «Candidatos conocidos»). -- ✅ Paso 1: listas aprobadas por el propietario, fuera del repositorio: - - `paths.txt`: 20 reglas de borrado; añade - `EmailsDistListContent.tsx`, componente antiguo con la lista de correos - real; - - `replacements.txt`: secretos (los valores salen de `backend/.env`, no se - escriben a mano), correos y nombres de contactos reales, con los mismos - sustitutos que el árbol actual; - - `replacements-names.txt`: nombres del personal. En mayúsculas pasan a - `EMP_01`…`EMP_11`, como el corpus del solver; en el resto, a nombres - inventados (los mismos que el árbol desde `42f6ac0`). `Salvador Pérez` - se queda como autor; - - `mailmap.txt`: `Claude`, `Azyd` y `bp` pasan a `bpstack` (los tres son - el propietario); - - `messages.txt`: quita `Co-Authored-By: Claude…` y «Generated with Claude - Code» de 3 mensajes de commit; - - renombrado de ruta: la guía diaria del F&B, que llevaba en el nombre el - de una persona, pasa a `night-audit-fb-daily.md` (`--path-rename`); - - `strip-blobs.txt`: los 2 blobs del informe Opera real de ingresos - (`OperaPrint-sample.pdf`, su copia en `backend/test/data/` y el `.txt` - que genera `pdf-parse`), hallado en el barrido de OpenCode y sustituido - en el árbol por una muestra sintética (`c50b09a`) - (`--strip-blobs-with-ids`). -- ✅ Barrido de OpenCode (2026-10-05): no añade secretos ni rutas nuevas; - sí el informe Opera. Su informe escribió en claro la contraseña y el host - de Aiven (ya tapados en el fichero): otro motivo para rotarla. -- ✅ Ensayo (`--dry-run`) sobre un clon nuevo de GitHub con las dos ramas: - 0 secretos, 0 dominios y nombres reales, 0 rutas borradas, autoría - `bpstack` (y `GitHub` en los merges web); 619 → 616 commits (3 solo - tocaban ficheros borrados). - - Ninguna regla toca la punta de las dos ramas (comprobado). El usuario - `demo` está desactivado en Aiven y la contraseña documentada ya no vale - (comprobado el 2026-10-05). - -- ✅ Primera ejecución real en `work.git` (2026-10-05): el historial queda - limpio (0 secretos, nombres y datos reales en 5.651 objetos; autoría - `bpstack` y `GitHub`), pero las puntas no son idénticas: `main` aún no - tenía los arreglos de la rama (nombres, informe Opera) y una regla tocaba - una línea de este documento. Se repite tras fusionar la rama en `main`. -- ✅ Rama fusionada en `main` (PR #9, `2d1005a`). -- ✅ Ejecución definitiva en `work.git` (clon nuevo de GitHub, 2026-10-05), - auditada: - - 0 secretos, 0 dominios y nombres reales, 0 restos del informe Opera en - los 5.656 objetos (blobs y commits); las 16 menciones a - `Co-Authored-By: Claude` son las de `AGENTS.md` y este documento, que - describen la regla, no marcas en commits; - - autoría: solo `bpstack` y `GitHub` (merges desde la web); - - solo las dos ramas, sin tags ni remoto; 9,4 MB; - - **árbol de la punta idéntico** al de GitHub en las dos ramas (`main` - 624 → 621 commits; la rama 622 → 619), así que los tests son los mismos - que pasaron en CI con la PR #9. -- ✅ Tercera pasada en `work.git` (2026-10-05), tras fusionar la PR #10, que - generalizó las guías del checklist: las mismas listas, ampliadas con rutas, - reemplazos y mensajes nuevos. Además quita del historial los 2 blobs del - informe Opera, 120 versiones antiguas de las guías del checklist y una - imagen confidencial, y mantiene el renombrado de la guía del F&B. 12 - commits quedan vacíos (solo tocaban lo borrado) y se podan: `main` queda en - 624 commits y la rama en 621, con el árbol de las dos puntas idéntico al del - repositorio original. -- ✅ Auditoría de la tercera pasada, hecha dos veces por separado (los 4.960 - blobs, mensajes y rutas): 0 secretos, 0 datos personales, 0 referencias al - hotel, la cadena o sus socios y 0 rastros de IA en los commits; autoría - `bpstack` y `GitHub`. Comprobar siempre que el volcado de blobs no sale - vacío antes de dar una búsqueda por limpia. -- ✅ Publicación en privado (ADR-035, 2026-10-05): `bpstack/four-points` con - las dos ramas; el original pasa a `four-points-archive`, privado. Vercel y - Render despliegan ya desde el nuevo, en producción y en preview. Las - referencias a commits de los documentos apuntan a los hashes nuevos. -- ⏳ Archivar `four-points-archive`, pasar `four-points` a público y activar - secret scanning y push protection (GitHub no los deja activar en un - repositorio privado gratuito). - -## Reglas - -- Solo se hacen **tres operaciones**, cada una sobre una lista aprobada - (ADR-034): - 1. **Eliminar** del historial ficheros o directorios enteramente privados - (`--invert-paths --path …`). - 2. **Reemplazar contenido** en ficheros legítimos que llevaron credenciales o - datos privados escritos (`--replace-text`): cada valor se cambia por - `***REMOVED***` o por un sustituto neutro, y el fichero se conserva. - 3. **Cambiar autoría** para unificar la identidad o quitar rastros de IA - (`--mailmap`), sin tocar el contenido. - - Nada más se modifica ni se borra. -- Se hace **poco a poco**: cada operación se prueba en el clon y se audita antes - de la siguiente. Los valores a reemplazar se guardan en un fichero fuera del - repositorio (no se versiona) y se borra al terminar. -- El contenido del proyecto no se altera más allá de lo imprescindible. -- No se pierde ningún commit, rama, tag ni parte del historial que deba - conservarse. -- Los archivos privados desaparecen del historial completo, no solo del estado - actual. -- Los paths privados se añaden a `.gitignore` para que no vuelvan a colarse. -- Antes de tocar nada, se crea o se verifica una copia de seguridad del - repositorio original. -- La reescritura se hace con `git-filter-repo`, no modificando commits a mano. -- No se hace `push --force` ni se publica nada hasta haber verificado el - resultado. -- Toda credencial que aparezca en lo eliminado se rota (ADR-007). - -## Procedimiento - -1. **Análisis.** Qué ramas y tags existen, qué archivos o directorios privados - aparecen en el historial, en qué commits y si hay copias o rutas equivalentes - que también deban eliminarse. -2. **Propuesta.** Qué cambios exactos se van a hacer y qué partes del historial - se verán afectadas. **No se sigue hasta que el propietario lo confirme** - (ADR-005). -3. **Limpieza.** Con el plan aprobado, se reescribe el historial. -4. **Auditoría.** Se verifica que: - - ningún path privado aparece en ningún commit; - - las ramas y los tags son los esperados; - - los archivos públicos siguen presentes; - - no hay modificaciones ajenas a la limpieza; - - el repositorio sigue funcionando; - - `.gitignore` impide que los archivos privados vuelvan a incorporarse. - - Se indica también qué hashes han cambiado. - -El resultado queda preparado y verificado en local, sin push: se revisa antes de -publicar. - -**La prioridad absoluta es eliminar por completo la información privada del -historial sin romper ni alterar innecesariamente el resto del repositorio.** - -## Candidatos conocidos - -Lista de partida para el análisis, comprobada el 2026-09-28 y ampliada el -2026-10-04. **Los volcados no se han abierto**; la lista final la aprueba el -propietario. - -### 1. Eliminar (ficheros enteramente privados) - -- **Volcados de BD**: `backend/db-mysql/backup/backup_hotel_db_*.sql` (cuatro; - versionados hasta el 2026-10-04, aunque `.gitignore` ya los ignoraba) y los - ya borrados `backup_hotel_db-aiven.sql` y `backup_hotel_db-local.sql` - (commits `f3d9898`, `d04504f` y `8109e5e`). -- **Scripts con la contraseña de Aiven**, quitados del árbol el 2026-10-04: - `backend/db-mysql/scripts/basics/` (10), `add-libre-number.ts`, - `backfill-libre-numbers.ts`, `set-holidays-2026.ts` y `backup-aiven.sh`. -- **Importador del Excel de horarios** con 13 nombres del personal: - `backend/scripts/import-planning-2026.ts` (quitado del árbol el 2026-10-04). -- **Alta de una persona real**: - `backend/db-mysql/scripts/20260520_insert_user_*.sql` (quitado del árbol - el 2026-10-04). -- **Excel con datos del personal**: `docs/checklists/PLANNING 2026.xlsx` y - el Excel de presencias del personal en `docs/frontend/schedule/`. -- **Peticiones de prueba**: los 18 ficheros de `backend/API REST/` (quitados - del árbol el 2026-10-04): llevan 12 contraseñas, 24 tokens JWT y nombres de - usuario. Los tokens están firmados con `SECRET_JWT_KEY`: otro motivo para - rotarla. -- **Documentación antigua** con nombres del personal: - `SCHEDULING-CONSTRAINTS.md` y `SCHEDULING-DECISIONS-LOG.md` (raíz, hasta el - 2026-09-28). -- **Encontrados en el análisis del 2026-10-04** (rutas antiguas, ya fuera del - árbol): - - `API REST/` en la raíz (estructura anterior a `backend/`); - - `PLANNING 2026.xlsx` en la raíz y toda la carpeta `z.schedule-docs/` - (Excel y CSV de horarios y presencias del personal); - - `aiven-conexion.md`, en sus tres ubicaciones - (`db-mysql/mysql-Aiven/`, `backend/db-mysql/mysql-Aiven/` y - `backend/db-mysql/aiven/`): datos de conexión con la contraseña de Aiven. - -### 2. Reemplazar contenido (ficheros legítimos) - -- **Contraseña de la BD local** en `backup-local.sh`, `check-collation.sh` y - `recreate-local.sh` (leen `backend/.env` desde el 2026-10-04). -- **Host, usuario y contraseñas de Aiven** en `backend/.env.example` (hasta el - 2026-10-02). -- **Nombres reales del personal** en comentarios de `aiven/19_scheduling.sql` - y `scripts/20251224_add_scheduling.sql` (anonimizados en el árbol el - 2026-10-04) y lo que quede según `TODO.md`. -- **Correos reales de personas y empresas** en - `frontend/content/checklist/references/emails-dist-list.md` (lista de - distribución del night audit: dirección del hotel, empresa gestora y un - auditor, con nombres) y sus códigos en las guías del checklist; - sustituidos por contactos de ejemplo en el árbol el 2026-10-04. -- **Secretos encontrados en el análisis del 2026-10-04** (recuento de valores - distintos en todo el historial, sin abrir ninguno): - - 2 contraseñas de Aiven (`AVNS_…`) en 17 rutas. **Una es la que se usa - hoy** (39 apariciones): rotarla es obligatorio; - - 1 clave de Anthropic (`sk-ant-…`) en `backend/.env.example`; no es la de - `backend/.env` actual y el propietario confirma que está revocada - (2026-10-05); - - valores de `CLOUDINARY_API_SECRET` (`backend.md`, `backend/.env.example`) - y de `SECRET_JWT_KEY` (8 valores en 11 rutas, casi todos marcadores de - ejemplo); ninguno coincide con los de `backend/.env` actual; - - 9 tokens JWT distintos en 20 rutas; - - ninguna clave privada, ni de OpenAI, Google, Resend o GitHub, y ningún - `.env` real versionado nunca. -- En el análisis se busca cada valor en **todo** el historial (`git log -S`), - no solo en estos ficheros: el reemplazo se aplica en cualquier fichero donde - aparezca. `git filter-repo --replace-text` acepta expresiones regulares - (`regex:…==>***REMOVED***`), útil para los tokens JWT. - -### 3. Cambiar autoría - -La sección siguiente. - -## Autoría que se corrige - -Decidido en ADR-031 (2026-10-04). Se hace en la misma pasada de -`git-filter-repo`, con `--mailmap`; no cambia el contenido de ningún commit. - -- **Obligatorio**: los 5 commits con autor `Claude `, - todos de la rama `claude/compassionate-planck-gh6aof` (`c4719b2`, `8af5641`, - `9c583eb`, `1d5b3ca`, `d432c30`), pasan a `bpstack `. - Ninguna otra rama tiene commits de ese autor (_comprobado el 2026-10-04_). -- **A confirmar en el paso 2 (Propuesta)**: unificar también `Azyd` y `bp`, los - dos con el mismo correo, en `bpstack`. - -```text -# mailmap -bpstack Claude -``` - -En la auditoría (paso 4) se comprueba además que -`git log --all --format='%an <%ae>' | sort -u` ya no muestra `Claude`. diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md deleted file mode 100644 index 84448f06..00000000 --- a/docs/ROADMAP.md +++ /dev/null @@ -1,73 +0,0 @@ -# ROADMAP — Four-Points (publicación como open source) - -> **Qué responde:** en qué orden se hace el trabajo y por qué ese orden. · -> **Quién lo lee:** quien abre o cierra una fase, **no en cada sesión**. · -> **Cómo se poda:** al cerrar una fase, su detalle se reduce a un resultado y un -> puntero. - -🔴 **Lo terminado sale del plan.** Una fase cerrada se queda en una o dos -líneas; su detalle pasa a `docs/_archive/roadmap-history.md` (local, ADR-009). -⚠️ Cerrar no es marcar `[x]` y dejarlo: el detalle se va. - -🔴 **Solo es un roadmap porque el orden importa.** Lo que no dependa de otra -cosa va a `TODO.md`. - -## Fases - -| Fase | Qué produce | Depende de | -| ---- | ------------------------------------------------------------------------ | ---------- | -| 1 | `docs/` nueva por módulos, escrita desde el código; lo antiguo archivado | — | -| 1b | Lo trascendental de `docs/_archive/` rescatado a `docs/` o a `TODO.md` | 1 | -| 1c | «Antes de publicar» de `TODO.md` resuelto y probado en producción | 1b | -| 2 | Clon con el historial limpio (dos ramas) + `docs/GITCLEAN.md` | 1c | -| 4 | Repositorio público nuevo con el historial limpio (dos ramas) | 2 | -| 5 | Demo pública en la propia web, con datos ficticios y reinicio diario | 4 | - -El orden lo fija ADR-001, revisado por ADR-022 (fase 1b) y ADR-027 (fase 1c, que -además absorbe la antigua fase 3 de tareas pendientes). ADR-033 limita la 1c a lo -que hay que resolver antes de publicar. La fase 4 sigue ADR-006, ya aceptada, y -ADR-035 deja en dos las ramas que se publican. - ---- - -## Fase 1 — Documentación nueva ✅ cerrada (2026-09-28) - -Los 12 módulos documentados y revisados con `security` L3, más el `README.md` de -la raíz (ADR-008). Detalle en `docs/_archive/roadmap-history.md` (local). - -## Fase 1b — Rescate del archivo ✅ cerrada (2026-09-28) - -Lo trascendental de `docs/_archive/` rescatado a `docs/` y `TODO.md`, comprobado -contra el código; `TODO-old.md` vaciado y borrado. Detalle en -`docs/_archive/roadmap-history.md` (local). - -## Fase 1c — Resolver «Antes de publicar» ✅ cerrada (2026-10-05) - -Las brechas de seguridad arregladas y probadas en producción antes de tocar el -historial (ADR-027, ADR-033), PR #7 a #10. Detalle en -`docs/_archive/roadmap-history.md` (local). - -## Fase 2 — Limpieza del historial ✅ cerrada (2026-10-05) - -Historial de `main` y `claude/compassionate-planck-gh6aof` reescrito y auditado -(ADR-035). Resultado en `GITCLEAN.md`. - -## Fase 4 — Publicación ← **actual** - -El historial limpio está en `bpstack/four-points`, todavía privado; Vercel y -Render despliegan desde él y el original está archivado. La demo de la fase 5 -ya está en producción, así que solo falta pasarlo a público y activar secret -scanning y push protection. Comprobado el 2026-10-05: ningún valor de -`backend/.env` aparece en los 662 commits de todas las ramas, salvo dos valores -públicos por defecto (el usuario `avnadmin` de Aiven y `localhost:11434` de -Ollama). La rotación de credenciales (ADR-007) no bloquea: va en «Después de -publicar» de `TODO.md`. - -## Fase 5 — Demo pública ✅ cerrada (2026-10-05) - -Que cualquiera pruebe la aplicación en la propia web, que es solo un escaparate -(ADR-037): un admin demo restringido que entra con un botón, datos ficticios, -generación de horarios y reinicio diario; y quien quiera verlo todo lo instala -en local con admin completo (ADR-038, ADR-039). En producción desde el -2026-10-05 (PR #3 y #4, `DEMO_MODE` en Render y Vercel), probada en preview y -en la web. diff --git a/docs/SESSION.md b/docs/SESSION.md index 6a4c169b..4e4ce987 100644 --- a/docs/SESSION.md +++ b/docs/SESSION.md @@ -13,48 +13,51 @@ propio fichero) o ruido (se borra). ## Estado -Fase actual: **4 — Publicación** (`ROADMAP.md`). Las fases 1c y 2 quedaron -cerradas el 2026-10-05: las brechas de «Antes de publicar» arregladas y -probadas, y el historial de `main` y `claude/compassionate-planck-gh6aof` limpio -y auditado (`GITCLEAN.md`). - -El historial limpio está en `bpstack/four-points`, **todavía privado**. El -original sigue privado como `four-points-archive`. Vercel y Render despliegan ya -desde el nuevo, en producción y en preview (`VERIFY.md`, «Cambio al repositorio -nuevo»). Decisiones en `DECISIONS.md`, hasta ADR-040. - -La demo pública (fase 5) está en producción desde el 2026-10-05: cuenta demo -con botón, bloqueos, límites, reinicio diario, Configuración → Demo y base de -horarios (octubre a diciembre de 2026). `pnpm setup:local` instala en local -con admin completo y la misma base de horarios (`scheduling-seed.sql`). La -documentación obsoleta está archivada (ADR-040). +**El repositorio es público** desde el 2026-10-05 (`bpstack/four-points`), con +secret scanning y push protection activados y sin alertas. El proceso de +publicación terminó: documentación nueva, arreglos previos, historial reescrito +y auditado, publicación y demo. Sus documentos (`ROADMAP.md`, `GITCLEAN.md`, +`VERIFY.md`) están archivados; cómo recuperarlos, en ADR-041. + +- **Producción:** `four-points.stackbp.es` (Vercel) y + `api.four-points.stackbp.es` (Render), sobre Aiven. Demo pública activa + (`DEMO_MODE`), descrita en `docs/demo/README.md`. +- **Preview:** `preview.four-points.stackbp.es` y + `api-preview.four-points.stackbp.es`, desde + `claude/compassionate-planck-gh6aof`, sobre la misma BD de Aiven. +- **Local:** `pnpm setup:local` instala desde cero con admin completo, datos + ficticios y la base de horarios de la demo (`scheduling-seed.sql`). +- **Repositorio original:** `four-points-archive`, archivado y privado. +- **Decisiones:** `DECISIONS.md`, hasta ADR-041. ## ⚠️ Empieza por aquí -1. **Este repositorio va a ser público**, también su historial: lo que se - commitee no se puede borrar después sin reescribirlo. Nada de secretos, hosts - o usuarios de la BD, IDs de servicios, rutas locales ni datos del personal o - de la empresa. -2. **Clones nuevos:** activar los hooks una vez con +1. **El repositorio es público**, también su historial: lo que se commitee se + publica al momento y no se puede borrar sin reescribir la historia. Nada de + secretos, hosts o usuarios de la BD, IDs de servicios, rutas locales ni datos + del personal o de la empresa. Push protection solo frena los secretos con + formato conocido. +2. **Flujo:** commit en `claude/compassionate-planck-gh6aof` → push → probar en + preview → PR a `main` con merge commit. `main` no acepta push directo. +3. **Clones nuevos:** activar los hooks una vez con `git config core.hooksPath .githooks` (no viene con el clon; ver `AGENTS.md`). -3. **Idiomas:** se trabaja en español; la documentación pública de `docs/` va en - inglés y no cita ADR ni `TODO.md`. `DECISIONS.md`, `ROADMAP.md`, - `SESSION.md`, `TODO.md`, `VERIFY.md` y `GITCLEAN.md` siguen en español. +4. **Idiomas:** se trabaja en español; la documentación pública de `docs/` va en + inglés y no cita ADR ni `TODO.md`. `DECISIONS.md`, `SESSION.md` y `TODO.md` + siguen en español. ## Siguiente -1. Pasar `four-points` a público y activar secret scanning y push protection. - Historial revisado contra `backend/.env` el 2026-10-05 (`ROADMAP.md`, - fase 4). -2. Archivar `GITCLEAN.md`, `VERIFY.md`, `ROADMAP.md` y `SESSION.md` como - dice ADR-040, cuando ya no hagan falta. -3. «Después de publicar» de `TODO.md`, empezando por la rotación de - credenciales (se puede adelantar). +«Después de publicar» de `TODO.md`, por prioridad: -Hecho el 2026-10-05: Vercel y Render despliegan solos desde este repositorio -(producción y preview), `four-points-archive` está archivado y Aiven tiene -usuarios anonimizados y datos ficticios (`mock-data.sql`). +1. **Rotar las credenciales de producción** (BD, clave JWT, Cloudinary, claves + de IA). Ninguna está en el historial; se hace en los paneles y se comprueba + que producción y preview siguen funcionando. +2. **Rastro de cambios en dinero:** ingresos de F&B, pagos y recuentos de caja, + cobros de parking. +3. Las de prioridad media, empezando por las comprobaciones cortas (versión de + Node y cron en Render) y el pago automático del día 10, que nunca ha + funcionado. ## Esperando decisión @@ -62,20 +65,24 @@ Nada ahora mismo. ## Cuidado con esto -- **Los `CLAUDE.md`/`AGENTS.md` de módulo no son fuente fiable**: el de logbook - citaba tres tablas que nunca existieron, y varios más tenían datos - desactualizados. Se contrastan siempre con el código y la BD; las correcciones - encontradas quedan en `TODO.md` y siguen sin aplicarse a los `AGENTS.md`. -- **Nada de `docs/_archive/` se publica**, tampoco `roadmap-history.md` - (ADR-009). Si algo de ahí debe ser público, se copia fuera. -- **`backend/db-mysql/aiven/19_scheduling.sql` sigue citando - `SCHEDULING-CONSTRAINTS.md §7.5`** en un comentario: los scripts 01–19 no se - tocan (ADR-021). La sección equivalente es - `docs/scheduling/constraints.md §7.5`. +- **`main` está protegida** (ruleset `protect-main`): solo entra por PR, con + los checks `backend` y `frontend` en verde y como merge commit (sin squash ni + rebase); no admite force push ni borrado. La rama de trabajo no tiene reglas. +- **Preview y producción comparten la BD de Aiven** y las cookies de sesión: + lo que se prueba en preview cambia datos de producción, y entrar en preview + sustituye la sesión de producción en ese navegador. - **La cuenta demo es un admin** (`users.is_demo = 1`): lo que no deba tocar se bloquea en la ruta con `denyDemo`/`denyDemoWrites` o en `demoRestriction`. Una ruta nueva que escriba fuera de lo que reinicia el - mock (usuarios, catálogos, configuración) necesita su guarda. -- **Enlaces rotos conocidos** tras archivar: comentarios que citan `Global-Plan.md` o - `SCHEDULING-SOLVER-PLAN.md`. Se arreglan al escribir cada módulo (ADR-010). - _Sin repasar desde el 2026-09-28._ + mock (usuarios, catálogos, configuración) necesita su guarda. Si cambia la + base de horarios de la demo, se regenera el seed local con + `pnpm seed:scheduling:export`. +- **Los `AGENTS.md` de módulo** ya se corrigieron, pero no son fuente fiable: + se contrastan siempre con el código y la BD. +- **Nada de `docs/_archive/` se publica** (git lo ignora). Si algo de ahí debe + ser público, se copia fuera. +- **Enlaces rotos conocidos:** 10 ficheros fuera de `docs/` citan + `Global-Plan.md`, `SCHEDULING-SOLVER-PLAN.md` o `SCHEDULING-CONSTRAINTS.md`, + ya archivados. Uno es `backend/db-mysql/aiven/19_scheduling.sql`, que no se + toca (ADR-021); la sección equivalente es `docs/scheduling/constraints.md + §7.5`. Los demás se arreglan al escribir cada módulo (ADR-010). diff --git a/docs/TODO.md b/docs/TODO.md index ba4bab6e..473ba85c 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -7,7 +7,7 @@ 🔴 **Lo terminado sale de la lista.** Este fichero no tiene archivo detrás: lo hecho se borra y queda en `git log`. -📌 **Si un punto necesita que otro se haga antes, va en `ROADMAP.md`**, no aquí. +📌 **Si un punto necesita que otro se haga antes, se dice en el propio punto.** ⚠️ **Antes de dar algo por pendiente, mira el código.** Un TODO desactualizado hace que alguien reimplemente lo que ya existe. @@ -280,6 +280,12 @@ preview. ### 🟢 Baja +- [ ] **Artículo en el blog del portfolio sobre el motor de horarios** — cómo + se modelaron los cuadrantes de recepción con OR-Tools (CP-SAT): reglas + duras y blandas, bloques de noches, continuidad entre meses y el daemon + de Python. Enlazar la demo y el repo. Es el gancho para promocionar el + proyecto. Se escribe en el repo `portfolio-bp`, no aquí. _Decidido el + 2026-10-05._ - [ ] **`excludeMantenimiento` es una lista de exclusión** — parking, lista negra, caja, partes, conciliación y departamentos dejan pasar cualquier rol salvo `mantenimiento`, y `services/auth/module-access.ts` lo replica. diff --git a/docs/VERIFY.md b/docs/VERIFY.md deleted file mode 100644 index 98e13223..00000000 --- a/docs/VERIFY.md +++ /dev/null @@ -1,254 +0,0 @@ -# VERIFY — arreglos de la fase 1c y su prueba en producción - -> **Qué responde:** qué se arregló en la rama `claude/compassionate-planck-gh6aof` -> y si está comprobado en producción. · **Quién lo lee:** quien pruebe la rama -> antes del merge y quien quiera saber, después, qué cerró la fase 1c. · **Cómo -> se poda:** no se poda. Al terminar las pruebas queda como registro de la fase -> 1c (ADR-027, ADR-031). - -Los puntos terminados se **borran** de `TODO.md` (su propia regla), así que esta -tabla es la lista de lo hecho: cada fila es un commit de la rama, con el punto de -`TODO.md` que cerró. Se generó el 2026-10-04 a partir de -`git log origin/main..HEAD` y de las líneas `- [ ]` que cada commit quita de -`docs/TODO.md`. - -**Dónde se prueba:** el entorno preview (`docs/general/README.md`, «Preview -environment»): `https://preview.four-points.stackbp.es` con el backend de la -rama y la base de datos de producción. - -**Resultado:** ⏳ sin probar · ✅ comprobado (fecha y cómo) · ❌ falla (enlace a -la nota o al punto nuevo de `TODO.md`) · 🧪 solo test (no se puede provocar en producción sin -romper algo; lo cubre el test indicado) · — no aplica (documentación, formato, -CI). - -**Fuera de la tabla:** - -- `8af5641` y `9c583eb` (fnb) y sus reverts `1d5b3ca` y `d432c30`: se anulan - entre sí y no cambian código. Sus dos puntos siguen abiertos en `TODO.md`. -- `5236a84`: documentación del entorno preview. -- Las filas marcadas «(otra IA)» las probó un segundo modelo con el mismo entorno y las mismas reglas; se revisaron sus peticiones. Las que había probado contra la ruta equivocada (`997b80d`, `31c99c7`, `3d8dd70` y las cuatro de logbook) se repitieron. - -## Estado previo - -- **CI** de la rama en verde (2026-10-04, run `37198286379`): backend con lint, - formato, tipos y tests; frontend con lint, formato y tipos. El build del - frontend lo valida Vercel. -- **Entorno preview** operativo (2026-10-04): login de `admin`, el bundle llama - a `api-preview` y el CORS acepta el origen de preview. -- **Ruido que no es de la rama**: en preview, la barra de Vercel - (`feedback.js`) y la protección «Vercel Authentication» producen - `OPTIONS /…` con 400, `/.well-known/vercel/jwe` con 503 y prefetch `?_rsc=` - con 503. En producción la misma navegación no da ninguna petición fallida. - -## Cambio al repositorio nuevo (2026-10-05) - -Vercel y Render se reconectaron de `four-points-archive` a `bpstack/four-points` -(ADR-036) y se desplegó desde él en los cuatro entornos: - -- **Producción:** `main` en `0953e94`. Vercel _Ready_ y `four-points.stackbp.es` - responde; Render _live_, conecta con la BD, arranca los cron y el solver, y - `api.four-points.stackbp.es` responde 200. -- **Preview:** la rama en `d9e7230`. Vercel _Ready_; el dominio está tras la - protección de Vercel y lo abrió el propietario. Render _live_ con la misma - comprobación y `api-preview` responde 200. -- Los cuatro despliegues se lanzaron a mano; el despliegue automático con un - push al repositorio nuevo **queda sin comprobar** hasta el primer push. - -## Arreglos - -| Commit | Qué cambia | Punto de `TODO.md` que cierra | Prueba | Resultado | -| ------ | ---------- | ----------------------------- | ------ | --------- | -| `c4719b2` | fix(logbook): validate messages, dates, route params and list queries | Logbook: validación incompleta | offset -1, limit 501, fecha 2026-02-31, mensaje vacío, importancia inválida, id no numérico → 400 | ✅ 2026-10-04 | -| `ee00b32` | fix(logbook): take the entry author from the session, not the body | Logbook: cualquiera puede crear entradas en nombre de otro | qa_recep crea entrada con `author_id` de admin → se guarda con qa_recep | ✅ 2026-10-04 | -| `9342a3b` | chore(githooks): mark commit-msg and pre-push as executable | — | no aplica | — | -| `997b80d` | fix(conciliation): validate dates, form entries, status and route params | Conciliation: sin validación Zod, a diferencia del resto del proyecto
Conciliation: un `reason` que no sea uno de los válidos falla en… | PUT /conciliations/:id/form con motivo inventado, importe sin número o líneas que faltan → 400 Zod | ✅ 2026-10-04 | -| `dc134dc` | fix(search): limit global search to the modules the role can open | `mantenimiento` lee la lista negra por la búsqueda global | /api/search?q=ab|an|er: admin y recep ven parking y blacklist; mant solo maintenance y groups | ✅ 2026-10-04 | -| `095a09a` | fix(activity): show only activity from modules the role can open | `mantenimiento` lee actividad de módulos vetados por `/api/activity` | /api/activity/recent: mant solo ve maintenance y groups | ✅ 2026-10-04 | -| `6b1e523` | fix(api): stop sending caught error messages in 500 responses | Maintenance: los errores 500 devuelven el mensaje de MySQL
El controlador de notificaciones devuelve `error.message` de MySQL
Parking: `listAvailableSpots` devuelve el mensaje de error de MySQL | `tests/security/no-raw-error-messages.test.ts` | 🧪 2026-10-04 | -| `812f64f` | docs(todo): record how to merge the working branch without its extra history | — | no aplica | — | -| `990bc72` | fix(cashier): answer known shift, payment and count errors with 4xx | Cashier: los 500 de turnos, pagos y recuentos devuelven… | `tests/cashier/cashier-errors.test.ts` | 🧪 2026-10-04 | -| `6c5d33b` | fix(checklist): cap image uploads in multer instead of after buffering | Checklist: subida de imágenes sin límite de tamaño en multer | Avatar de 3 MB (límite 2 MB) → 413; checklist usa el mismo middleware | ✅ 2026-10-04 | -| `17b496b` | fix(uploads): check image uploads by their bytes, not the declared type | Blacklist: el tipo de imagen solo se valida por el mimetype que declara… | Avatar y blacklist con bytes no-imagen declarados `image/png` → 400 | ✅ 2026-10-04 | -| `b122d5e` | fix(uploads): use the checked image upload for maintenance and avatars | — | Avatar con tipo `text/plain` → 400; PNG válido → 200 | ✅ 2026-10-04 | -| `7e28102` | fix(checklist): reject unknown checklists, foreign steps and bad record ids | Checklist: `:id` sin validar
Checklist: comentarios e imágenes no comprueban que el paso exista | Historial de un checklist inexistente → 404 (otra IA) | ✅ 2026-10-04 | -| `3295769` | fix(checklist): validate the history query instead of failing with 500 | Checklist: fechas de `/history` sin validar | /checklists/:id/history con fecha imposible, limit 0 y 101 → 400 (otra IA) | ✅ 2026-10-04 | -| `50c0c85` | fix(maintenance): reject blank and oversized texts and invalid route ids | Maintenance: validación incompleta | Parte con título en blanco o descripción corta → 400 (otra IA) | ✅ 2026-10-04 | -| `a628c55` | fix(blacklist): reject blank texts and impossible dates | — | Blacklist con nombre en blanco o fecha imposible → 400 (otra IA) | ✅ 2026-10-04 | -| `31c99c7` | fix(api): reject impossible calendar dates in every date param and field | — | /conciliations/day/2026-02-31 → 400 `INVALID_DATE_FORMAT`; /activity/recent?date_from=2026-02-31 → 400 | ✅ 2026-10-04 | -| `f06d8d5` | fix(notifications): keep direct links inside the app and validate notices | `direct_link` de las notificaciones admite cualquier URL
Notificaciones manuales: destinatarios y textos sin validar | `direct_link` `//…`, `/…`, `https://…`, `javascript:` → 400; `userIds` no UUID y título vacío → 400 | ✅ 2026-10-04 | -| `2ca2847` | docs(todo): restore the blank line between the groups and scheduling blocks | — | no aplica | — | -| `6377b61` | fix(messages): validate participants and query params, escape LIKE search | Mensajería: sin validación de entrada ni transacción al crear
Mensajería: `limit` sin validar, `LIKE` sin escapar y fallo de la… | Conversación con participantes repetidos o UUID inválido → 400 (otra IA) | ✅ 2026-10-04 | -| `9783e4c` | docs(todo): detail why the parking booking source is lost and what to decide | — | no aplica | — | -| `17f13b4` | fix(logbook): answer 403 when editing someone else's entry | Logbook: editar una entrada ajena devuelve 500 | qa_gadmin edita la entrada de qa_recep → 403 `LOGBOOK_ONLY_AUTHOR_UPDATE` | ✅ 2026-10-04 | -| `88dfd58` | chore(backend): remove the unused email service and EJS view engine | — | Backend de preview arranca y sirve sin el servicio de email ni EJS | ✅ 2026-10-04 | -| `420a534` | fix(api): send Cache-Control: no-store on every API response | El API no envía `Cache-Control: no-store` | GET /api/auth/me → `Cache-Control: no-store` | ✅ 2026-10-04 | -| `9045b8e` | fix(backoffice): allow-list the columns supplier and invoice edits may set | Backoffice: inyección SQL por los nombres de campo | PATCH proveedor QA con clave `"notes = (SELECT 1), name"` → claves ignoradas, solo cambia `name` | ✅ 2026-10-04 | -| `99bd9f7` | fix(backoffice): only fetch invoice PDFs from our own Cloudinary cloud | Backoffice: SSRF en la descarga de PDF | Factura con PDF en 169.254.169.254, dominio externo u otra cuenta de Cloudinary → 400 | ✅ 2026-10-04 | -| `5127df8` | fix(cloudinary): only delete files from the folder of the module asking | Blacklist: `DELETE /upload/:publicId` borra cualquier recurso de…
Backoffice: borrado arbitrario en Cloudinary | Factura con `public_id` fuera de `invoices/` (crear y validar) → 400 | ✅ 2026-10-04 | -| `60dba0a` | fix(backoffice): accept only integer year and month in batch payments | Backoffice: el pago en lote paga todos los meses con un objeto | Batch payment con year 2026.5 o "2026" → 400 (otra IA) | ✅ 2026-10-04 | -| `c043ff8` | fix(scheduling): stop constraint edits from setting their own status | — | PUT /scheduling/constraints/:id con `status: approved` → la clave se descarta (400 «al menos un campo»); PUT …/approve como recep → 403 | ✅ 2026-10-04 | -| `1c48a16` | docs(todo): close the scheduling self-approval entry, note the open decision | Scheduling: un recepcionista puede aprobarse sus propias solicitudes | no aplica | — | -| `9c42b09` | fix(parking): validate the booking update body against its columns | Parking: rutas de edición sin Zod | Reserva de parking con importe negativo o método de pago desconocido → 400 (otra IA) | ✅ 2026-10-04 | -| `f591165` | fix(scheduling): bound bulk edits and validate employee id lists | Scheduling: validación incompleta | Edición masiva de 32 días → 400 (otra IA) | ✅ 2026-10-04 | -| `765e271` | fix(scheduling): stop sending solver internals in generate errors | Scheduling: errores del solver y del arranque devueltos al cliente. | `tests/security/no-raw-error-messages.test.ts` | 🧪 2026-10-04 | -| `1c3878d` | fix(blacklist): accept only images uploaded through the blacklist upload | Blacklist: el array `images` acepta cualquier URL | POST /api/blacklist con imagen de otro dominio o de otra carpeta de Cloudinary → 400 «Cada imagen debe haberse subido desde la lista negra» | ✅ 2026-10-04 | -| `1282e33` | fix(cloudinary): sanitise new public ids, accept legacy ones when deleting | Maintenance: `public_id` con el nombre original sin sanear. | Avatar subido como `mi foto ñ (1)#x.png` → public_id `avatar_…_mi_foto____1__x` (borrado después) | ✅ 2026-10-04 | -| `8fe1514` | fix(cloudinary): check control characters without a control-character regex | — | `tests/uploads/cloudinary-url.test.ts` | 🧪 2026-10-04 | -| `54a5210` | chore(conciliation): drop the commented-out admin check in the monthly status | Conciliation: comentario de código muerto en el controlador mensual | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `758883b` | docs(tests): replace the outdated backend tests README | `backend/tests/README.md` está desfasado | no aplica | — | -| `e8157bf` | chore(db): keep a single copy of the Aiven CA certificate | Certificado de Aiven duplicado | El backend de preview conecta a Aiven con TLS verificado y `apply-migration.sh` usa la misma CA | ✅ 2026-10-04 | -| `4d65223` | chore(env): bring the .env.example files in line with the code | Actualizar los `.env.example` | El backend de preview arranca con las variables de producción; `.env.example` sin valores | ✅ 2026-10-04 | -| `6bf04e9` | docs(agents): note that the unused email service is gone | Corregir el `AGENTS.md` raíz sobre next-intl | no aplica | — | -| `9b83a28` | docs(cashier): stop presenting the daily totals trigger as reliable | Corregir el `AGENTS.md` de cashier | no aplica | — | -| `2551afb` | docs(logbook): document the input checks and close the stale entry | Corregir `backend/services/logbook/AGENTS.md` | no aplica | — | -| `8ba44f0` | docs(todo): close the module and parking AGENTS.md entries, already fixed | Corregir los `AGENTS.md` de módulo
Corregir los `AGENTS.md` de parking | no aplica | — | -| `4fea090` | docs(blacklist): describe the real blacklist_entries columns and soft delete | Corregir el `AGENTS.md` de blacklist | no aplica | — | -| `4f64a36` | docs(backoffice): describe assets, both PDFs and the unenforced lifecycle | Corregir el `AGENTS.md` de backoffice | no aplica | — | -| `f6d15f8` | docs(checklist): describe the param validation and the real table names | Corregir el `AGENTS.md` de checklist | no aplica | — | -| `d7b7c83` | refactor(api): drop param checks the route guards already make | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `404f99e` | refactor(maintenance): drop the per-handler id checks the route guard makes | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `31371bd` | refactor(parking): call isCalendarDate directly instead of a one-line wrapper | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `9a8d9e8` | chore(backend): remove the parking PDF placeholder and dead config code | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `55c5a18` | chore(backend): remove the unused conciliation logic and payment method repo | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `d3873b1` | refactor(backend): remove exports nothing uses | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `ac0c937` | docs(scheduling): fix PI and FO, the weights claim and the rotation emitter | Corregir los `AGENTS.md` de scheduling | no aplica | — | -| `a82c284` | chore(backend): remove the unused nodemailer dependency | — | Backend de preview arranca sin `nodemailer` | ✅ 2026-10-04 | -| `25f3334` | fix(messages): store message notifications under the messages module | Notificaciones de mensajes con `module: 'system'` | Mensaje urgente QA → aviso con `module: messages`. Fallaba («Data truncated»): faltaba la migración, aplicada con `5365d5c` | ✅ 2026-10-04 | -| `e2fdf9c` | fix(logbook): stop returning author and editor emails nobody displays | Emails de más en las respuestas de logbook | Listas de logbook: `author_name` sin campos de email | ✅ 2026-10-04 | -| `77d8f22` | fix(checklist): keep attachment public_id on the server | Checklist: respuestas con campos internos | `tests/checklist/attachment-response.test.ts` | 🧪 2026-10-04 | -| `967d65f` | fix(checklist): reject control characters in comments, escape them in reports | Checklist: `checklist-report.ts` imprime los comentarios sin filtrar… | Comentario de checklist con ESC y BEL → 400 «caracteres no válidos»; con salto de línea → 201 (borrado después) | ✅ 2026-10-04 | -| `7574d8c` | refactor(blacklist): remove the unused image delete endpoint and API methods | Blacklist: `DELETE /upload/:publicId` borra cualquier imagen de la…
Ids sin codificar en las URL de `blacklistApi.ts` | DELETE /api/blacklist/upload/:id → 404 «Ruta no encontrada» | ✅ 2026-10-04 | -| `c7a2703` | refactor(parking): remove the analytics API and occupancy query nobody calls | Parking: código sin uso | GET /api/parking/stats/analytics/{trends,comparison,performance} → 404 | ✅ 2026-10-04 | -| `b293e28` | chore(frontend): remove the unused auth routes and dependencies | Quitar código muerto | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `364ce12` | chore(frontend): remove the old backoffice tabs and their mock data | Backoffice: notas `IMPORTANT-PRODUCTION.MD` obsoletas | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `e276f80` | chore(frontend): remove 19 unimported files and 3 unused dependencies | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `0014b3a` | refactor(frontend): remove unused exports and fix the real lint errors | — | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `da5822d` | fix(reports): filter maintenance by the statuses the backend uses | Informes: la sección de mantenimiento filtra por estados que no… | Los 7 estados del filtro de informes → 200 en /api/maintenance; los antiguos `pending` y `resolved` → 400 | ✅ 2026-10-04 | -| `eca5507` | fix(blacklist): render search highlights as React nodes, not injected HTML | Blacklist: resaltado de búsqueda con `dangerouslySetInnerHTML` | Búsqueda "Mar" en blacklist → 6 `` en nombre y documento, solo con nodos de texto | ✅ 2026-10-04 | -| `ee73ff7` | fix(backoffice): neutralise spreadsheet formulas in the paid invoices CSV | Backoffice: inyección de fórmulas en la exportación CSV | CSV de pagadas con proveedor QA `=1+1 QA-Supplier-csv` → celda `"'=1+1 QA-Supplier-csv"` (blob interceptado, sin descarga) | ✅ 2026-10-04 | -| `425a194` | style(frontend): apply Prettier to the TypeScript and JavaScript files | — | no aplica | — | -| `5f04740` | refactor(logbook): drop the req.user.isAdmin check that never holds | Logbook: comprobación de `isAdmin` que nunca se cumple
`backend/scheduling-solver/AGENTS.md` cita `backend/debug-*.js` | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `032fb13` | fix(parking): read every occupying booking on the status map and wizard | Parking: el mapa de estado deja de ver reservas a partir de 50 | Mapa de estado de parking carga (20 plazas); el caso de más de 50 reservas lo cubre `tests/frontend/fetch-all-pages.test.ts` | 🧪 2026-10-04 | -| `79f8913` | refactor(blacklist): drop the created_by list filter and two unused actions | Blacklist: filtro `created_by` sin restricción | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `a64fc0d` | docs(blacklist): note that no screen restores a deleted entry | — | no aplica | — | -| `9f269c7` | chore(fnb): log uploads through the logger and fix the cache docs | Corregir el `AGENTS.md` de `fnb`
Fnb: `console.log` sin pasar por el logger del proyecto
Corregir el comentario de `canAccessFnb` | Recorrido de los 17 módulos como admin en preview: cargan y pintan sin peticiones fallidas (fuera de la barra de Vercel); CI verde | ✅ 2026-10-04 | -| `af9811b` | ci: lint, format, typecheck and DB-free tests on every push | CI mínimo | no aplica | — | -| `861d972` | fix(cashier): export the PDF when a text has an emoji or a non-Latin-1 letter | Cashier: la exportación a PDF puede romperse con emojis | `tests/frontend/pdf-text.test.ts`; mismo helper que el PDF de facturas probado en vivo con emoji (`a9f44fb`) | 🧪 2026-10-04 | -| `a9f44fb` | fix(pdf): keep free text drawable in the scheduling, invoice and editor PDFs | — | PDF de facturas pendientes con proveedor QA `🚀 Łódź ✓` → PDF de 2,9 KB sin error. Scheduling y editor: texto libre pasa por `encodableFor` (revisado) y `tests/frontend/pdf-text.test.ts` | ✅ 2026-10-04 | -| `7bebc39` | fix(logbook): stop solving, reopening, reading or editing deleted rows | Logbook: se pueden resolver, reabrir y marcar como leídas entradas…
Logbook: comentarios borrados que se pueden editar y volver a borrar | Entrada y comentario QA borrados: editar, borrar, resolver, reabrir y leer → 404 | ✅ 2026-10-04 | -| `acb24fa` | fix(messages): a removed participant can no longer edit or delete messages | Mensajería: un expulsado sigue editando y borrando sus mensajes | Participante expulsado de un grupo QA: editar y borrar su mensaje → 403 | ✅ 2026-10-04 | -| `0d2b9cf` | fix(maintenance): keep deleted reports out of non-admin lists and writes | Maintenance: partes borrados visibles y modificables | Parte QA borrado: fuera de las listas no admin; escrituras → 400 (status, priority y assign daban 500, arreglado en `25a3df3`) | ✅ 2026-10-04 | -| `66e5c4e` | fix(parking): keep the floor or number a booking edit leaves unchanged | Parking: cambiar solo la planta puede mover la reserva a otra plaza | Reserva QA en -3 #2: solo número 3 → -3 #3; solo planta -2 → -2 #3; ambos → -3 #4 | ✅ 2026-10-04 | -| `a77f94f` | fix(maintenance): only admins read a deleted report | — | Parte borrado: recep → 404, admin → 200 con `is_deleted` (otra IA y mía) | ✅ 2026-10-04 | -| `dbfd651` | fix(notifications): only admin and group-admin run the pending check | — | POST /api/notifications/check-pending como recep → 403 | ✅ 2026-10-04 | -| `ae68010` | docs(logbook): record that any user may re-route an entry by commenting | Logbook: un comentario cambia la prioridad o el departamento de una…
Cualquier rol lanza a mano la generación de notificaciones
Maintenance: el detalle, las fotos y el historial de un parte borrado… | no aplica | — | -| `7ef4e77` | fix(auth): leave the disabled demo-admin role out of the new rules | — | `tests/maintenance/deleted-reports.test.ts` (`isAdminRole` excluye `demo-admin`, desactivado) | 🧪 2026-10-04 | -| `05d58ff` | docs(todo): the demo user being disabled was not checked in the database | — | no aplica | — | -| `6dc2ba1` | fix(parking): free a deleted booking's days and validate list paging | Parking: borrar una reserva no libera sus días | Paginación inválida → 400 (otra IA). Reserva QA borrada: su plaza vuelve a salir libre en esas fechas (antes no) | ✅ 2026-10-04 | -| `3d8dd70` | fix(search): escape LIKE wildcards in every text search | — | /api/search?q=%% y q=__ → 0 resultados (q=ab → 5) | ✅ 2026-10-04 | -| `00cc932` | docs(release): plan the merge without squash and track the branch's fixes | — | no aplica | — | -| `e249dea` | docs(deploy): record the QA users created for the preview tests | — | no aplica | — | -| `25a3df3` | fix(maintenance): answer 400, not 500, when status, priority or assign hit a deleted report | — (hallado al probar `0d2b9cf`) | Parte QA borrado: PATCH /status, /priority y /assign → 400; el parte no cambia | ✅ 2026-10-04 | -| `5365d5c` | fix(db): add 'messages' to the notifications module ENUM | — (hallado al probar `25f3334`) | Migración aplicada en Aiven; mensaje urgente QA → aviso con `module: messages` | ✅ 2026-10-04 | -| `c0f61c1` | docs(db): add a migration runner and record the notifications ENUM as applied | — | `apply-migration.sh --dry-run` contra local y Aiven; segunda ejecución salta (idempotente) | ✅ 2026-10-04 | -| `0d923c1` | docs(verify): close every row of the branch's production checks | — | no aplica | — | -| `0b8b008` | fix(cashier): correct daily cash total inflated by trigger LEFT JOIN | Cashier: el total diario de efectivo se infla | Trigger en Aiven consultado: creado el 2026-09-29, sin el `LEFT JOIN` | ✅ 2026-10-04 | -| `f22d799` | fix(cashier): expected cash is fund + cash income - vouchers still out of the drawer | Cashier: el descuadre calculado en el servidor siempre es 0 | Día QA 2026-12-01: noche con vale 20 €, income 100 y recuento 270 → esperado 280, descuadre −10; mañana (vale arrastrado, uno de 5 € cancelado) y tarde → esperado 180; cierre que justifica el vale → 200. Editar income recalcula. Vale pendiente pasa al día siguiente (noche 2026-12-02: 15 €). Datos QA borrados después | ✅ 2026-10-04 | -| `56a2743` | docs(deploy): record the merge of PR #7 and how the branch continues | — | no aplica | — | -| `cbd338f` | docs(decisions): keep simple permissions in blacklist, F&B, maintenance and cashier | Blacklist, F&B (dos), mantenimiento y caja: permisos por acción (ADR-032, intencionado) | no aplica | — | -| `938fed1` | fix(fnb): reject negative, out-of-range and impossible-date manual entries | Fnb: `manualEntry` acepta cualquier número, incluido negativo o absurdamente grande | POST /api/fnb/entries con −50, 100.000.000, "12" (texto) o fecha 2026-02-31 → 400; no se escribió ninguna cifra | ✅ 2026-10-04 | -| `b578dbf` | fix(fnb): reject non-PDF uploads and bound PDF parsing time | Fnb: el PDF subido no se valida por contenido, solo por extensión o mimetype declarado | Subida de un texto como `.pdf` → 422 «El fichero no es un PDF»; PDF con cabecera y cuerpo roto → 422 «No se pudo leer el PDF». Límite de páginas y tiempo: `tests/fnb/pdf-upload-guard.test.ts` | ✅ 2026-10-04 | -| `9847ef7` | fix(backoffice): enforce invoice status transitions and add single payment revert | Backoffice: sin máquina de estados | Factura QA 36: pagar pendiente → 409; validar → 200; con validada, editar, validar, rechazar y subir PDF → 409; pagar → 200; recep en `/unpay` → 403; `/unpay` → 200 (`validated`, `paid_date` null), repetido → 409; id inexistente → 404; rechazar la pagada 35 → 409. Historial con el estado anterior real. Navegador: validadas sin Editar ni Subir PDF; botón «Revertir pago» en Pagadas revierte la 35 con confirmación. Datos QA restaurados | ✅ 2026-10-04 | -| `3de0012` | docs(decisions): keep parking payments editable in any booking state | Parking: importes cobrados editables siempre (ADR-032, intencionado; queda abierto el historial del cobro) | no aplica | — | -| `ebee351` | fix(scheduling): make constraint writes admin-only and keep locked cells consistent | Scheduling: cualquier rol edita o borra restricciones ajenas o ya aprobadas | recep: crear, editar y borrar restricción → 403; leer → 200. Admin, mes 114, restricción QA 54 (vacaciones de una empleada 9–10 de mayo, antes `L`): aprobar → celdas V bloqueadas; editar a 16–17 → 9–10 vuelven a `L` sin bloqueo y 16–17 quedan V bloqueadas; borrar → 16–17 vuelven a `L`. Historial: `manual_edit` «Restricción editada» y «Restricción eliminada». Celdas como estaban | ✅ 2026-10-04 | -| `005211e` | docs(decisions): keep the messaging user directory open to every role | Mensajería: directorio de emails y roles (ADR-032, intencionado) | no aplica | — | -| `c845875` | fix(parking): prevent double bookings and keep the availability calendar extended | Parking: doble reserva al cambiar fechas; carrera al crear reserva y en la entrada; el calendario se acaba el 2026-12-26 | Aiven tras el despliegue: calendario ampliado al arrancar hasta 2027-10-04. Plaza -3/10, marzo 2027: crear encima de otra → 400; mover solo fechas encima de otra → 400; mover a días libres → 200 y el calendario se recoloca; cancelar libera solo sus días (trigger migrado). Concurrencia: falló (una 201 y una 500), arreglado en `8db94f1` | ✅ 2026-10-04 (con `8db94f1`) | -| `8db94f1` | fix(parking): count overlapping bookings with a locking read | Parking: carrera al crear reserva | Dos rondas de 3 altas simultáneas en la misma plaza y fechas → una 201 y dos 400, sin 500. Reservas QA borradas y calendario limpio | ✅ 2026-10-04 | -| `567d9c4` | docs(todo): fold the backoffice pdf-download entry into retiring demo-admin | Backoffice: `/pdf-download` no exige `isRealAdmin` (se integra en «Retirar el rol demo-admin»; `demo` inactivo en Aiven) | no aplica | — | -| `93bb20d` | docs(todo): drop the cashier entry about writes to missing columns | Cashier: operaciones que fallan siempre en producción (ya resuelto por `210f445` y la migración 20260929; columnas comprobadas en Aiven) | no aplica | — | -| `16fc3bf` | fix(logbook): write each change and its history in one transaction | Logbook: el historial no es atómico | Aiven, entrada QA 141 con qa_recep: crear, editar, comentar, editar y borrar comentario, leído, no leído, resolver, reabrir y borrar → todo 200/201 y 11 filas en `logbook_history` en orden (la de borrado, después del borrado). Commit y rollback: `tests/logbook/atomic-history.test.ts` | ✅ 2026-10-04 | -| `4e2fabc` | refactor(notifications): type query params and catch clauses instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `47e1c0c` | refactor(messages): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `5f8670e` | refactor(checklist): type catch clause instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `e9cb75d` | refactor(conciliation): type catch clauses instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `bceb353` | refactor(blacklist): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `8ab6209` | refactor(group): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `916c378` | refactor(maintenance): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `2ab1bbc` | refactor(backoffice): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `af2f731` | refactor(cashier): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `5c282b3` | refactor(scheduling): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `2809745` | refactor(fnb): type catch clauses and query params instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `528c08a` | refactor(activity): type catch clauses instead of any | Backend: 341 usos de `any` (otra IA) | Sin cambio de comportamiento: typecheck, lint y 883 tests tras la fusión; humo en preview (abajo, `c1510e2`) | ✅ 2026-10-04 | -| `c1510e2` | Merge branch 'chore/backend-types' into claude/compassionate-planck-gh6aof | Backend: 341 usos de `any` | Preview tras desplegar: listados de mantenimiento, blacklist, facturas, caja, conciliación, grupos, F&B, notificaciones, mensajes, checklist, scheduling y parking → 200; logbook crear y borrar → 201/200 | ✅ 2026-10-04 | -| `c14ddcd` | fix(maintenance): keep NULL in the change history after the any cleanup | Regresión de la fusión anterior (historial con '' en vez de NULL; 5 tests) | Parte QA 041026-003 sin asignar → asignar a qa_mant: `maintenance_history` guarda `old_value` NULL en `assigned_to` y `assigned_type`. Parte borrado | ✅ 2026-10-04 | -| `26eedc1` | docs(todo): update the remaining any count after the backend typing | Backend: usos de `any` (quedan 55) | no aplica | — | -| `e72ac44` | docs(decisions): limit phase 1c to what must be fixed before publishing | Alcance de la fase 1c (ADR-033): TODO en «Antes de publicar» y «Después de publicar» | no aplica | — | -| `4748168` | fix(frontend): serve the design-system and fonts-test pages only in development | Proteger o quitar `/design-system` y `/fonts-test` | Navegador del dueño en preview: `/design-system` y `/fonts-test` → «HTTP ERROR 404» (la extensión no tiene permiso en ese dominio y curl solo ve el SSO de Vercel) | ✅ 2026-10-04 | -| `5b13897` | fix(auth): fail closed if Render's proxy chain changes | `trust proxy 3` depende de la red de Render | Preview: login fallido con `X-Forwarded-For` falso distinto cada vez → `ratelimit-remaining` 4, 3, 2 (la IP falsa se ignora y la cadena se reconoce como la esperada). El fallo cerrado ante una cadena distinta: `tests/auth/client-ip.test.ts` | ✅ 2026-10-04 | -| `1057c6d` | fix(scheduling): keep health data to admin and out of the logs | Scheduling: datos de salud expuestos | Preview: recep en `/months`, `/constraints/by-period` y `/shift-stats` → 403; admin → 200. Log de INFEASIBLE: `tests/scheduling/constraint-permissions.test.ts` | ✅ 2026-10-04 | -| `fd4ad23` | docs(verify): record the proxy chain and scheduling checks, move solver run retention to later | Retención de `solver_runs` (a «Después de publicar») | no aplica | — | -| `72a8224` | docs(roadmap): plan a public demo environment as phase 5 | Demo pública (fase 5) | no aplica | — | -| `40371d8` | feat(uploads): keep invoice PDFs, stamps and signatures private behind the API | Backoffice: PDFs de facturas, sello y firma públicos en Cloudinary | Preview tras migrar (`02b063d`): admin → PDF original y validado de las facturas 28, 29 y 34, sello 8 y firma 9 → 200 con su tipo; ZIP de 29 y 34 → 2 PDF; sin login → 401; recep en el sello → 403. Ninguna respuesta de facturas ni de sellos lleva `cloudinary.com`; `/pdf-url` → 404 | ✅ 2026-10-04 | -| `4d2bd3b` | feat(blacklist): serve guest photos as private files through the API | Fotos de huéspedes de blacklist públicas (mismo problema que backoffice) | Preview: ficha 11 guarda `/api/blacklist/images/…`; la foto → 200 `image/jpeg` para admin y recep; un nombre con `../` → 404; el detalle de la ficha no lleva `cloudinary.com` | ✅ 2026-10-04 | -| `86e76bc` | feat(uploads): serve maintenance photos and checklist attachments through the API | Maintenance: fotos públicas (la parte que queda pasa a «Después de publicar») | Preview: fotos 6 y 7 de los partes 060326-002 y 180526-002 → 200 `image/png` y `image/jpeg`; el detalle del parte no lleva `cloudinary.com`. Checklist sin adjuntos en Aiven: solo `tests/checklist/attachment-response.test.ts` | ✅ 2026-10-04 | -| `b67c3b2` | feat(frontend): load Google and Vercel Analytics only when configured | Cambio del dueño (analítica por variables de entorno) | Preview sin `NEXT_PUBLIC_GA_ID`: la CSP servida no incluye `googletagmanager.com` y el detector de `securitypolicyviolation` no registró ningún intento de cargarlo en todo el recorrido del 2026-10-04 (comprobación indirecta). Producción necesita `NEXT_PUBLIC_GA_ID` y `NEXT_PUBLIC_VERCEL_ANALYTICS` en Vercel para seguir igual que hoy | ✅ 2026-10-05 | -| `5c2b3a8` | chore: license the project under MIT | Licencia del proyecto | no aplica | — | -| `02b063d` | chore(uploads): script to make the already uploaded files private | Ficheros ya subidos siguen públicos | Aiven 2026-10-04: dry-run y `--apply`, 17 ficheros a `authenticated` (4 públicos: avatares y `main-sample`), filas de facturas, sellos, fotos y blacklist 11 reescritas; segunda pasada → 0 por migrar. Las 8 URL públicas de antes → 404 | ✅ 2026-10-04 | -| `db32a70` | docs(verify): record the private files migration and its checks in preview | Registro de la migración de ficheros privados | no aplica | — | -| `c1d01a6` | chore(db): drop private scripts and dumps, read local credentials from .env | Contraseñas de la BD escritas en ficheros versionados (parte del árbol; el historial, en la fase 2) | `git grep` de la contraseña de Aiven en el árbol → 0 ficheros; los 4 volcados siguen en disco y fuera del índice; `bash -n` de los 3 scripts locales; hash de `mock-data.sql` comprobado con bcrypt. Los scripts locales no se han ejecutado | ✅ 2026-10-04 | -| `76f9da1` | feat(frontend): add a Content-Security-Policy and COOP, drop X-Powered-By | Cabeceras del frontend en producción incompletas para L3 | Navegador en preview: la página trae CSP (origen de la API de preview y `vercel.live`), COOP `same-origin`, `X-Frame-Options` y sin `X-Powered-By`. Con un detector de `securitypolicyviolation`: panel, mantenimiento, logbook, parking, horarios, checklist, perfil, backoffice (visor y editor de PDF) y blacklist → 0 bloqueos. El editor dibuja el PDF sin `eval`. Sin hacer: HSTS `preload` (decisión del dueño; no recomendado) y SRI de Google Analytics (Google cambia el script) | ✅ 2026-10-04 | -| `1bc177c` | fix(uploads): let the frontend show private images from the API | Regresión de `40371d8`: el navegador bloqueaba las `` de ficheros privados (CORP `same-origin` de helmet) | Navegador en preview: foto de la ficha 11 de blacklist (800 px) y su descarga por `fetch` (200, 109 824 B); foto 7 del parte 180526-002; sello y firma en el editor de PDF y en la configuración de backoffice → cargan; 0 bloqueos de CSP. Editor cerrado sin guardar (facturas 28 y 36 siguen pendientes) | ✅ 2026-10-04 | -| `7328f13` | test(scheduling): anonymize employee names in solver corpus fixtures (OpenCode) | Nombres reales del personal en el repo (fixtures del solver) | Revisado: el diff solo cambia nombres por `EMP_01`…`EMP_07`; ningún nombre del personal en `tests/scheduling-corpus/` ni `scheduling-solver/`. Tras la fusión: vitest de scheduling 132 passed, pytest del solver 105 passed, 1 skipped | ✅ 2026-10-04 | -| `f2eabdc` | Merge branch 'chore/anonymize-solver-fixtures' into claude/compassionate-planck-gh6aof | (fusión) | Los mismos tests | ✅ 2026-10-04 | -| `4829a52` | chore: remove test requests and the planning importer with real staff data | Nombres reales del personal en el repo (parte del árbol) | `git grep` en el árbol: sin los 18 `.http` ni el importador; los comentarios de los dos SQL usan `uuid-employee-N`. Sin cambio de comportamiento | ✅ 2026-10-05 | -| `223e847` | fix(auth): give every failed login the same minimum response time | Login: el tiempo de respuesta puede delatar si un usuario existe | Preview, 18 intentos fallidos por tipo, antes del arreglo: mediana 364 ms real frente a 340 ms inexistente (Mann-Whitney p ≈ 0,048). Después: 666 frente a 663 ms (p ≈ 0,27; un intento de 63,8 s fue el arranque en frío de Render). `tests/auth/login-timing.test.ts` | ✅ 2026-10-05 | -| `a227edc` | docs(todo): fix the login timing entry and drop the done CSP entry | (documentación) | no aplica | — | -| `c1e213d` | chore(checklist): replace the real email distribution list with example contacts | Correos reales de personas en el checklist (hallado en el barrido del 2026-10-04) | `git grep` en el árbol de los dominios reales → 0; tests del checklist 28 passed. No probado en la interfaz | ✅ 2026-10-05 | -| `c337c2d` | docs(gitclean): add what the history analysis found | Análisis de la fase 2 | no aplica | — | -| `55efaeb` | docs(gitclean): note that the old Anthropic key is revoked | (documentación) | no aplica | — | -| `5794432` | Merge pull request #8 from bpstack/claude/compassionate-planck-gh6aof | Fase 1c en producción | Producción tras el despliegue (Vercel y Render): cabeceras con CSP (con Google Analytics), COOP y HSTS, sin `X-Powered-By`; la API sirve las rutas nuevas de ficheros; en el navegador, foto de la ficha 11 de blacklist (800 px), sello y firma en el editor de PDF (cerrado sin guardar), PDF de la factura 34 por la API (200, 266 575 B), Google Analytics y Vercel Analytics cargan; recorrido por panel, mantenimiento, logbook, parking y perfil. Única pega: la CSP bloquea 2 píxeles de Google Analytics a `www.google.es` (audiencias de Google Signals), sin efecto en la medición | ✅ 2026-10-05 | -| `42f6ac0` | chore: replace the remaining staff names with invented ones | Nombres reales del personal en el árbol (hallado al preparar las listas de la fase 2) | `git grep` en el árbol de los nombres del personal → solo el autor; ninguna fila del checklist en Aiven usa el id antiguo de la guía; tests de restricciones 37 passed. No probado en la interfaz | ✅ 2026-10-05 | -| `2fe93a0` | docs(gitclean): record the approved phase 2 lists | (documentación) | no aplica | — | -| `c50b09a` | test(fnb): replace the real Opera revenue report with a synthetic sample | Informe Opera real de ingresos en los fixtures (hallado en el barrido de OpenCode) | El texto extraído del PDF sintético tiene las mismas 344 líneas y estructura que el real (solo cambian cifras y nombre del hotel); backend 909 passed. No probado subiendo el PDF en la interfaz | ✅ 2026-10-05 | -| `0ad5967` | docs(gitclean): record the OpenCode sweep and the dry run | (documentación) | no aplica | — | -| `d12e117` | docs(gitclean): record the first real run and reword a line the rules touched | (documentación) | no aplica | — | -| `98e6fb9` | docs(verify): add the rows for 0ad5967 and d12e117 | (documentación) | no aplica | — | -| `2d1005a` | Merge pull request #9 from bpstack/claude/compassionate-planck-gh6aof | `main` con los arreglos previos a la limpieza | CI de la PR en verde (backend y frontend); árbol de `main` igual al de la rama. Despliegue de producción no revisado: solo cambian tests, documentación, un placeholder y el id de una guía | ✅ 2026-10-05 | -| `cd3b7fa` | docs(gitclean): record the final rewrite and its audit | (documentación) | no aplica | — | -| `b6f37ff` | feat(checklist): load the hotel's real content from a private repository at build time | (revertido en `a9e8842`) | no aplica | — | -| `ee07328` | chore(checklist): replace the hotel's procedures with example content | (revertido en `a9e8842`) | no aplica | — | -| `47ceb9b` | chore: parametrize the hotel name and the local paths | Nombre del hotel y rutas personales en el código | El pie impreso lee `NEXT_PUBLIC_HOTEL_NAME` con «Hotel PMS» por defecto; CI en verde. Pie impreso no revisado en el preview | ✅ 2026-10-05 | -| `27b6761` | chore(frontend): replace the screenshots that showed a staff user | Capturas con un usuario del personal | Revisado por el propietario en el preview | ✅ 2026-10-05 | -| `34630b8` | test(fnb): remove chain and partner names from the synthetic Opera sample | Nombres de la cadena y del partner en la muestra | CI en verde (backend) | ✅ 2026-10-05 | -| `c9b4323` | docs: describe the hotel's files without their names | (documentación) | no aplica | — | -| `a9e8842` | revert(checklist): keep the procedures in the repository instead of a private one | Contenido del checklist | CI en verde; ver `fa85339` | ✅ 2026-10-05 | -| `fa85339` | chore(checklist): generalize the hotel's procedures | Nombres de la cadena, sus sistemas y partners en el checklist | Las 22 referencias `guide:`/`reference:` de las tareas apuntan a ficheros existentes; tareas del backend idénticas a las del frontend; el propietario abrió los enlaces en el preview | ✅ 2026-10-05 | -| `563b5b1` | style(checklist): format the night audit tasks | (formato) | no aplica | — | -| `fb877ac` | docs: point commit references to the rewritten history | (documentación) | Los 176 hashes citados existen en el repositorio nuevo, con el mismo asunto que en el original (uno solo cambia en los hashes que cita) | ✅ 2026-10-05 | -| `2b84b0a` | build: pin the Node major version in .nvmrc | — | no aplica | — | -| `aab1848` | docs(todo): close the pre-publication section | (documentación) | no aplica | — | -| `2fd2f56` | docs(gitclean): record the third pass, its audit and the private publication | (documentación) | no aplica | — | -| `d197ad6` | docs(todo): drop the local path of the harness repository | (documentación) | no aplica | — | -| `5b216a0` | docs(roadmap): close phases 1c and 2 and make publication current | (documentación) | no aplica | — | -| `0f237d0` | docs(decisions): record the publication order and when to rotate | (documentación) | no aplica | — | -| `2009f39` | docs(verify): record the switch to the new repository | (documentación) | no aplica | — | -| `8194031` | docs(session): move the session to the publication phase | (documentación) | no aplica | — | -| `0bb4c1f` | fix(db): require the Aiven user and port instead of defaulting them | Puerto y usuario de Aiven por defecto en el código | Producción arranca y conecta tras el merge (PR #2) | ✅ 2026-10-05 | -| `a474007` | chore(db): rewrite the mock data without users and for the current schema | Datos ficticios para Aiven | Cargado en local y en Aiven; horarios intactos | ✅ 2026-10-05 | -| `a889999` | docs: plan the demo on the showcase site before going public | (documentación) | no aplica | — | -| `e332b8a` | docs: settle the demo design and the local install | (documentación) | no aplica | — | -| `5789e83` | feat(auth): restrict the public demo account with an is_demo flag | Demo: marca `is_demo` y bloqueos | Preview, sesión demo real: 18 rutas, lecturas 200 y bloqueadas 403 | ✅ 2026-10-05 | -| `a306d36` | docs(db): mark the is_demo migration as applied on Aiven | (documentación) | no aplica | — | -| `8553963` | fix(auth): answer 403 to blocked demo requests without a JSON body | — | Local: DELETE y subidas bloqueadas dan 403, no 500/401 | ✅ 2026-10-05 | -| `7507d58` | feat(auth): enter the public demo with one button, without a password | Demo: entrada con botón | Preview: botón visible, entra como Demo · Admin; login con contraseña de `demo` da 401 | ✅ 2026-10-05 | -| `d8c23af` | feat(auth): rate limit the demo account's writes and schedule generation | Demo: límites | `demo-limits.test.ts` (5/h por IP, 30/h en total, 60 escrituras/15 min) | 🧪 | -| `db8b356` | feat(demo): reset the demo data daily and from Settings | Demo: reinicio diario | Preview: primer reinicio del día registrado en Aiven (734 ms); local: base de horarios restaurada, fallo deshecho; pestaña Demo solo para admins | ✅ 2026-10-05 | -| `a2395a7` | docs(db): mark the demo reset log migration as applied on Aiven | (documentación) | no aplica | — | -| `7e7ed39` | test(auth): mock the demo reset in the login timing test | — | CI en verde | ✅ 2026-10-05 | -| `ee9cbc0` | fix(db): load the mock data on a database with a single admin | — | `setup:local` en un MySQL limpio carga el mock | ✅ 2026-10-05 | -| `a8882c4` | feat(db): install a local database from scratch with pnpm setup:local | Demo: instalación local | MySQL 8.0 limpio en Docker: estructura igual que Aiven (1161 de 1161) y mismos catálogos; la app entra con el admin | ✅ 2026-10-05 | -| `9336716` | feat(db): give the local install a documented admin password | — | `setup-local.test.ts` | 🧪 | -| `e3a642c` | fix(db): group the backoffice monthly summary view by its period | — (hallazgo del 2026-10-05) | `GET /api/backoffice/stats/monthly` en preview da 200; la vista coincide con un cálculo directo | ✅ 2026-10-05 | -| `9991d79` | test(security): check the CSRF defence and the session cookies by behaviour | Demo: CSRF | `csrf-origin.test.ts` (servidor Express real), `session-cookies.test.ts`; preview: origen ajeno da 403 | ✅ 2026-10-05 | -| `4449b7a` | fix(checklist): render unsafe links in step notes as plain text | Demo: XSS | `safe-href.test.ts` (11 URL maliciosas) | 🧪 | -| `143882d` | refactor(auth): retire the demo-admin role | Retirar `demo-admin` | Preview tras el push: la entrada demo, los bloqueos y horarios funcionan; rol borrado en Aiven (no tenía usuarios) | ✅ 2026-10-05 | diff --git a/docs/demo/README.md b/docs/demo/README.md new file mode 100644 index 00000000..e142222a --- /dev/null +++ b/docs/demo/README.md @@ -0,0 +1,139 @@ +# Public demo + +A demo account on the project site that anyone can enter with one button, to try +the application without installing it. Project overview in +[`../general/README.md`](../general/README.md). + +## What problem it solves + +The site is a showcase: a visitor should see the whole application working, not +a video of it. A real administrator account cannot be shared, and the production +database is the same one the site runs on. The demo gives every visitor an +administrator that can use almost everything, protects what could break the +site, and puts the data back every day. + +To see everything with no restriction, install it locally: `pnpm setup:local` +creates a full administrator (root [`README.md`](../../README.md)). + +## Who uses it + +- **Visitors**: the **«Prueba nuestra demo»** button on the login page signs + them in as the `demo` user, without a password. It shows only where the + frontend has `NEXT_PUBLIC_DEMO_MODE=true`. +- **A real administrator** (never the demo account): the **Settings → Demo** tab + shows the last reset, resets the data by hand and saves the scheduling base. + +## What it can do + +The demo user is an `admin`, so it sees every module and every screen. It can +create, edit and delete anything the daily reset puts back: logbook entries, +parking bookings, maintenance work orders, groups, cashier shifts, F&B revenue, +invoices, checklists, messages, and it can generate schedules with the solver. + +It **cannot**: + +| Blocked | Where | +| -------------------------------------------------------------------------- | ----------------------------------------------------------------------- | +| Create, edit or delete users | `/api/users` (any write) and `POST /api/auth/register` | +| Change its own profile, password or avatar | `/api/auth/me/profile`, `/me/password`, `/me/avatar` | +| Create, edit or delete departments | `/api/departments` (any write) | +| Change the scheduling configuration, shifts, rules, employees or contracts | `/api/scheduling/{config,shifts,rules,employees,contracts}` (any write) | +| Create backoffice categories | `POST /api/backoffice/categories` | +| Upload any file | every `multipart/form-data` request | +| See the demo activity log or reset the demo | `/api/demo-activity`, `/api/demo` | +| Sign in with a password | the `demo` account only enters through the button | + +A blocked request answers `403` with `demo: true`; the frontend shows a «Modo +Demo» notice. Every blocked attempt is stored in `demo_activity_log`. + +## What data it handles + +- **`users.is_demo = 1`** marks the demo account. The access token carries + `demo: true` only for that account, and a token refresh reads the flag again + from the database. The account is created with `pnpm demo:user` (local) or + `pnpm demo:user:aiven`, with a random password nobody uses. +- **Module data** comes from `backend/db-mysql/mock-data.sql`: fictitious + records with dates relative to the day of the reset, so the screens never look + stale. +- **The scheduling base** is a copy of every `scheduling_*` table in + `demo_snapshot_scheduling_*`: employees, contracts, rules, shifts, + configuration and the months already generated by the solver. +- **`demo_reset_log`** records each reset and each saved base: when, who (`auto` + or the administrator), whether it worked and how long it took. + +## What rules it follows + +### When the data resets + +**There is no fixed time.** The reset runs with **the first demo entry of each +day**, counting days in Madrid time: + +1. A visitor presses the demo button. +2. The backend looks for the last successful reset in `demo_reset_log`. +3. If it happened today (since 00:00 Madrid), the visitor goes straight in. + Otherwise the data is reset first, about a second, and then the visitor goes + in. + +So the first visitor of the day always finds clean data, however long the site +has been idle. A session is not reset while it is in use, unless it crosses +midnight and someone else enters the demo. + +**Why not a scheduled job at a fixed hour**: the API runs on Render's free plan, +which **puts the service to sleep after 15 minutes without traffic**. A job at, +say, 04:00 would almost always find the service asleep and never run. Resetting +on the first entry works no matter how long the service slept. + +An administrator can also reset at any moment from **Settings → Demo**. + +### What a reset does + +1. Reloads `mock-data.sql`, which empties the module tables and loads the + fictitious records again. Users, roles, departments and catalogs are not + touched. +2. Restores the scheduling base from `demo_snapshot_scheduling_*`, inside a + transaction. If no base was saved, scheduling is left as it is. + +Two resets never run at once: a database lock (`GET_LOCK`) makes the second one +answer «busy» (`409`). The reset runs on its own connection, so it does not hold +the application's pool. + +### Saving the scheduling base + +An administrator generates the months with the solver, as anyone would, and +presses **«Guardar horarios actuales como base»** in Settings → Demo. That +copies every scheduling table into the snapshot tables; from then on each reset +restores that state. Saving again replaces the previous base. + +### Limits + +Per IP unless stated, and only for the demo account: + +| What | Limit | +| -------------------------------------------- | --------------------------------------- | +| Entering the demo | 10 per 15 minutes | +| Writes (anything but `GET`/`HEAD`/`OPTIONS`) | 60 per 15 minutes | +| Generating a schedule with the solver | 5 per hour per IP, 30 per hour in total | + +## How information flows + +``` +Login page ── POST /api/auth/demo ──► is DEMO_MODE on? (404 if not) + │ + ├─ find the is_demo user (503 if none) + ├─ reset already done today (Madrid)? + │ no → reset: mock-data.sql + scheduling base + └─ session cookies, token with demo: true + │ +Each request ── authenticateToken ──► demo? multipart → 403 + ├─ write rate limit + └─ route guards (denyDemo / denyDemoWrites) → 403 +``` + +Configuration: + +| Variable | Where | Effect | +| ---------------------------- | -------- | -------------------------------------------------------------- | +| `DEMO_MODE=true` | backend | Enables `POST /api/auth/demo`, the daily reset and `/api/demo` | +| `NEXT_PUBLIC_DEMO_MODE=true` | frontend | Shows the demo button and the Settings → Demo tab | + +Both are off by default, so a local install has no demo button. diff --git a/docs/general/README.md b/docs/general/README.md index 04c49c3d..1b2c9bc6 100644 --- a/docs/general/README.md +++ b/docs/general/README.md @@ -25,11 +25,11 @@ Hotel staff, each with **a role**: receives their notices. - **`mantenimiento`**: only the maintenance module, plus the common functions (profile, messages, notifications, search). -- **Public demo account** (`users.is_demo = 1`, only where `DEMO_MODE=true`): - an `admin` entered with the demo button, without a password. It can do - everything the daily reset undoes, but not manage users, departments or the - scheduling configuration, upload files, or reset the demo. Each blocked - attempt is recorded. +- **Public demo account** (`users.is_demo = 1`, only where `DEMO_MODE=true`): an + `admin` entered with the demo button, without a password. It can do everything + the daily reset undoes, but not manage users, departments or the scheduling + configuration, upload files, or reset the demo. Each blocked attempt is + recorded. Details in [`../demo/README.md`](../demo/README.md). The side menu only hides backoffice and scheduling links from non-admins. **The backend is what actually decides**: each route checks the role and returns `403` @@ -197,13 +197,13 @@ like this is in ADR-030. `SameSite=Lax` cookies are not sent and the CORS list rejects it. Under `four-points.stackbp.es` both work without touching code. -**Test users** (created on 2026-10-04 to verify `docs/VERIFY.md` role by role) +**Test users** (created on 2026-10-04 to verify the pre-release fixes role by role) -| User | Role | Email | -| ----------- | --------------- | ------------------------ | -| `qa_recep` | `recepcionista` | `qa_recep@qa.invalid` | -| `qa_gadmin` | `group-admin` | `qa_gadmin@qa.invalid` | -| `qa_mant` | `mantenimiento` | `qa_mant@qa.invalid` | +| User | Role | Email | +| ----------- | --------------- | ---------------------- | +| `qa_recep` | `recepcionista` | `qa_recep@qa.invalid` | +| `qa_gadmin` | `group-admin` | `qa_gadmin@qa.invalid` | +| `qa_mant` | `mantenimiento` | `qa_mant@qa.invalid` | - They live in the production database, so production sees them too. - Their random passwords, and the `admin` one used for the tests, are only in @@ -215,8 +215,8 @@ like this is in ADR-030. - **Shared cookies**: both APIs set the same cookie names on `.four-points.stackbp.es` and sign them with the same `SECRET_JWT_KEY`. - Logging in on preview replaces the production session in that browser, and - the session is valid on both. + Logging in on preview replaces the production session in that browser, and the + session is valid on both. - **Scheduled tasks run twice** while the preview service is awake: both backends start the same `node-cron` jobs against the same database. Checklist reset, log purge and batch payment are idempotent; the 07:00 notices may be @@ -224,17 +224,17 @@ like this is in ADR-030. happens if preview is awake at that time. - **Vercel toolbar on preview**: Vercel injects its feedback toolbar (`feedback.js`) and the "Vercel Authentication" protection into preview - deployments only. They log `OPTIONS` 400 and `/.well-known/vercel/jwe` 503 - in the console and some `?_rsc=` prefetches answer 503. It is not the - app: production shows none of it. The toolbar can be turned off in Vercel → + deployments only. They log `OPTIONS` 400 and `/.well-known/vercel/jwe` 503 in + the console and some `?_rsc=` prefetches answer 503. It is not the app: + production shows none of it. The toolbar can be turned off in Vercel → Settings → General. - **Free limits**: the Render workspace has 750 instance hours a month shared by every free service (if they run out, production is suspended too) and 2 custom domains, both now in use. -**After a merge into `main`**: keep working on the same branch, so the -preview keeps its domain, variable and Render service. A merge commit has the -branch as a parent, so it only needs a fast-forward: +**After a merge into `main`**: keep working on the same branch, so the preview +keeps its domain, variable and Render service. A merge commit has the branch as +a parent, so it only needs a fast-forward: `git switch claude/compassionate-planck-gh6aof && git merge --ff-only origin/main`. PR #7 merged the first round on 2026-10-04 (`942fdfa`). @@ -257,8 +257,8 @@ cd frontend && pnpm dev # http://localhost:3000 Backend variables: `PORT` (4000 locally), `NODE_ENV`, `SECRET_JWT_KEY`, `SALT_ROUNDS`, `DB_ENVIRONMENT` (`local` or `aiven`), `LOCAL_DB_*` or -`AIVEN_DB_*` + `AIVEN_PASSWORD`, `CLOUDINARY_*`, `LOG_LEVEL`, `FRONTEND_URL` -and `DEMO_MODE` (`true` only where the public demo runs). Frontend: +`AIVEN_DB_*` + `AIVEN_PASSWORD`, `CLOUDINARY_*`, `LOG_LEVEL`, `FRONTEND_URL` and +`DEMO_MODE` (`true` only where the public demo runs). Frontend: `NEXT_PUBLIC_API_URL`, `NEXT_SERVER_API_URL` (optional), `NEXT_PUBLIC_APP_URL`, which the user creation route uses to accept the request origin, and `NEXT_PUBLIC_DEMO_MODE`, which shows the demo button on the login page. diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png new file mode 100644 index 00000000..c715f254 Binary files /dev/null and b/docs/images/dashboard.png differ diff --git a/docs/images/parking.png b/docs/images/parking.png new file mode 100644 index 00000000..703e59fc Binary files /dev/null and b/docs/images/parking.png differ diff --git a/docs/images/scheduling.png b/docs/images/scheduling.png new file mode 100644 index 00000000..10feaa7e Binary files /dev/null and b/docs/images/scheduling.png differ diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index be35bd7a..b21a9ecd 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -80,7 +80,7 @@ export default function Page() {
- for desktop y mobile + for desktop and mobile @@ -108,7 +108,7 @@ export default function Page() { className="group relative px-8 py-4 bg-gradient-to-r from-blue-600 to-purple-600 text-white font-semibold rounded-xl overflow-hidden transition-all hover:shadow-2xl hover:shadow-blue-500/50 dark:hover:shadow-blue-400/30 transform hover:scale-105" > - Get Started + Try the live demo - Live demo → + Source code → @@ -229,18 +229,21 @@ export default function Page() { {[ { icon: '⚡', - title: 'Lightning Fast', - description: 'Optimized performance for instant responses', + title: 'Built from real operations', + description: + 'Logbook, parking, cashier, groups, maintenance and more, from years at a front desk', }, { icon: '🔒', - title: 'Secure by Default', - description: 'Enterprise-grade security and data protection', + title: 'Shift scheduling solver', + description: + 'Monthly front-desk rosters generated with OR-Tools constraint programming', }, { icon: '🌍', - title: 'Works Everywhere', - description: 'Cloud-synced across all your devices', + title: 'Open source', + description: + 'MIT licensed, with a one-click demo and a local install with full admin access', }, ].map((feature, index) => (

- Including design methodology and implementation details. Private backend repository, - public frontend. + Frontend, backend and the scheduling solver, under the MIT licence, with instructions to + run it locally.

Source code available on GitHub @@ -290,7 +293,7 @@ export default function Page() { {/* Footer */}
diff --git a/frontend/public/customers/amy-burns.png b/frontend/public/customers/amy-burns.png deleted file mode 100644 index 7b29d725..00000000 Binary files a/frontend/public/customers/amy-burns.png and /dev/null differ diff --git a/frontend/public/customers/balazs-orban.png b/frontend/public/customers/balazs-orban.png deleted file mode 100644 index 7fbc0097..00000000 Binary files a/frontend/public/customers/balazs-orban.png and /dev/null differ diff --git a/frontend/public/customers/delba-de-oliveira.png b/frontend/public/customers/delba-de-oliveira.png deleted file mode 100644 index 08db1b8e..00000000 Binary files a/frontend/public/customers/delba-de-oliveira.png and /dev/null differ diff --git a/frontend/public/customers/evil-rabbit.png b/frontend/public/customers/evil-rabbit.png deleted file mode 100644 index fe7990ff..00000000 Binary files a/frontend/public/customers/evil-rabbit.png and /dev/null differ diff --git a/frontend/public/customers/lee-robinson.png b/frontend/public/customers/lee-robinson.png deleted file mode 100644 index 633ae985..00000000 Binary files a/frontend/public/customers/lee-robinson.png and /dev/null differ diff --git a/frontend/public/customers/michael-novotny.png b/frontend/public/customers/michael-novotny.png deleted file mode 100644 index 96a13a69..00000000 Binary files a/frontend/public/customers/michael-novotny.png and /dev/null differ