Add CI, a pre-commit hook, and an accessibility pass on the panel's own views

CI (.github/workflows/tests.yml) reuses docker-compose.test.yml verbatim for
the Postgres job rather than re-describing that environment in YAML, so the
Docker image stays the single source of truth for it.

Fixes a real mismatch found while wiring PHP versions into the workflow:
composer.json declared "php": "^8.3", but the locked Symfony packages require
>=8.4.1 — installing on 8.3 would fail at `composer install`, exactly what
the Dockerfile's own comment already warned about. Bumped the constraint and
resynced composer.lock's content-hash (no package versions changed).

The pre-commit hook (.githooks/pre-commit, activated via
`git config core.hooksPath` in post-autoload-dump) runs `pint --dirty --test`
and refuses to commit unformatted files rather than silently rewriting them.

The accessibility pass only touches this app's own hand-written Blade views,
not Filament's components: aria-hidden on icons that repeat adjacent text,
an aria-label on the one icon that is the only signal of a health check's
severity, rel="noopener" and a labelled announcement on preview links that
open in a new tab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-20 08:35:17 +02:00
parent 3897113313
commit 618d3a56ad
11 changed files with 217 additions and 20 deletions

View File

@@ -56,6 +56,15 @@ php artisan key:generate
php artisan migrate --seed # crea roles, permisos y contenido de ejemplo
```
`composer install` activa también [`.githooks/pre-commit`](.githooks/pre-commit)
(`git config core.hooksPath .githooks`, dentro de `post-autoload-dump`): antes
de cada commit corre `pint --dirty --test` sobre lo que has tocado y lo
rechaza si algo no está formateado. Solo avisa, nunca reescribe por su cuenta
un fichero que estás a punto de comitear — el arreglo es `pint --dirty` a
mano, ver el diff, y comitear otra vez. Nada de esto sustituye a la CI: el
hook protege el minuto a minuto de quien escribe, la CI protege lo que llega
al remoto por si alguien comitea con `--no-verify`.
### 1. Registrar la aplicación en el servidor de identidad
En `https://oauth.pireon.dev` crea un cliente OAuth con:
@@ -1340,6 +1349,14 @@ php artisan site:maintenance --up
php artisan test # 391 tests, 1121 aserciones
```
En cada push y cada pull request, [`.github/workflows/tests.yml`](.github/workflows/tests.yml)
corre tres cosas: `pint --test`, esta suite contra SQLite (la vuelta rápida) y
`docker compose -f docker-compose.test.yml run --rm test` — el mismo comando de
más abajo, no una reimplementación en YAML — contra Postgres (la que de verdad
importa). Reutiliza el fichero de Docker en vez de describir el entorno dos
veces: un cambio en las extensiones de PHP o en la versión de Postgres se
corrige en un solo sitio.
Cubren el acceso con email y contraseña (credenciales incorrectas
indistinguibles de una dirección desconocida, cuenta desactivada, pendiente de
aprobación o sin rol, límite de intentos y rotación de sesión), el flujo PKCE
@@ -1357,19 +1374,18 @@ detectan, los permisos que ocultan resultados y los códigos de salida), las
redirecciones (que se crean solas, que no encadenan ni dan vueltas y que la web
las recibe de una vez), los 404 que reporta el frontend (que el endpoint no se
abre sin secreto, que cuenta en vez de acumular filas, que normaliza igual que
una redirección, que tira los escaneos y que crear la regla cierra el aviso), el
la revisión (que se puede pedir sin permiso de publicar, a
quién avisa, que devolver exige motivo, que publicar la cierra y programar no),
las ediciones simultáneas (que el segundo guardado se rechaza, que guardarse a
uno mismo no cuenta como conflicto y que la presencia se olvida sola),
las cifras de lectura (que suman en una fila por día, que cada idioma cuenta
aparte y que en la tabla no cabe nada de un visitante), el contrato de la API
contra su especificación campo a campo y los tipos generados de ella, las
traducciones que se quedaron atrás (que el slug no cuenta, que una vacía no
cuenta y que ponerse al día lo limpia), las ediciones simultáneas y sus
acciones en lote, las sesiones activas (que no se puede cerrar la propia) y el
aviso de títulos parecidos,
el historial (quién publicó, el diff y restaurar), la
una redirección, que tira los escaneos y que crear la regla cierra el aviso), la
revisión (que se puede pedir sin permiso de publicar, a quién avisa, que
devolver exige motivo, que publicar la cierra y programar no) y sus acciones en
lote, las ediciones simultáneas (que el segundo guardado se rechaza, que
guardarse a uno mismo no cuenta como conflicto y que la presencia se olvida
sola), las cifras de lectura (que suman en una fila por día, que cada idioma
cuenta aparte y que en la tabla no cabe nada de un visitante), el contrato de
la API contra su especificación campo a campo y los tipos generados de ella,
las traducciones que se quedaron atrás (que el slug no cuenta, que una vacía
no cuenta y que ponerse al día lo limpia), las sesiones activas (que no se
puede cerrar la propia) y el aviso de títulos parecidos, el historial (quién
publicó, el diff y restaurar), la
limpieza (qué se conserva siempre y qué nunca se borra sin pedirlo), las
ventanas de publicación (que se respetan sin nada corriendo, que preview las
salta y que el cierre de convocatorias es idempotente), la sincronización con
@@ -1394,6 +1410,33 @@ inyecta `Accept-Language: en-us` por defecto, y la API respeta la negociación d
idioma. Si un test espera castellano tiene que pedirlo explícitamente
(`Accept-Language: es` o `?lang=es`), como hace `PublicApiTest`.
## Accesibilidad del propio panel
El texto alternativo de la web pública tiene su sección propia; esto es sobre
la herramienta, no sobre lo que publica. Filament ya se ocupa de lo suyo —
contraste, roles ARIA, navegación por teclado entre pestañas— y no se ha
tocado nada de eso. Lo que se ha revisado son las **pocas vistas Blade
escritas a mano** en `resources/views/filament/`, donde nada garantiza esas
cosas por defecto.
Dos reglas, aplicadas donde tocaba:
**Un icono junto a un texto que ya dice lo mismo lleva `aria-hidden="true"`.**
El aviso de mantenimiento, el de salud del sitio: el icono de aviso al lado de
«La web pública está en mantenimiento» no añade nada que un lector de
pantalla necesite oír dos veces.
**Un icono que es la única señal de algo lleva su propio `aria-label`.** El de
cada comprobación de salud es el único caso: la gravedad —fallo o aviso— no
está escrita en ningún sitio, solo se ve en la forma y el color del icono.
Ocultarlo habría sido perder esa información, no limpiarla.
De paso, los enlaces de vista previa que abren en pestaña nueva llevan
`rel="noopener"` (la página de destino, en otro origen, no debe quedarse con
una referencia a esta) y el campo de solo lectura que copia la URL tiene su
propio `aria-label`, porque el texto que lo identifica visualmente es un
párrafo aparte sin relación programática con él.
## Estructura
```