diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 0000000..a5c686a --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +############################################################################### +# Catches a formatting slip before it becomes a commit, rather than after — +# by which point it is a CI failure and a second commit to fix it. +# +# Only checks files with uncommitted changes (`--dirty`), not the whole tree: +# a hook that took as long as the full lint would be a hook people learn to +# skip. Read-only on purpose too — it reports what Pint would change and +# leaves fixing it to you, rather than silently rewriting a file you are +# about to commit without your having seen the diff. +# +# Activated by `composer run setup` (`git config core.hooksPath .githooks`), +# not by being physically inside .git/hooks/ — that directory is not tracked, +# so a hook living only there is never shared with anyone else who clones +# this repository. +############################################################################### + +if ! ./vendor/bin/pint --dirty --test; then + echo >&2 + echo "Pint found formatting issues in the files you are committing." >&2 + echo "Run ./vendor/bin/pint --dirty to fix them, then stage and commit again." >&2 + exit 1 +fi diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..674fc2e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,81 @@ +name: Tests + +# Every push and every pull request, on any branch — a failure is cheapest to +# find here, before it is a review comment or, worse, a deploy. +on: + push: + pull_request: + +# A second push to the same branch cancels whatever this workflow was still +# running for the first one: nobody is waiting on results for code that no +# longer exists. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + # Real PHP requirement per composer.lock, not the ^8.3 composer.json used to + # claim — the locked Symfony packages require >=8.4.1, so 8.3 fails at + # `composer install`, and this pins the version the Dockerfile already + # settled on rather than re-deriving it. + PHP_VERSION: "8.4" + # A throwaway key, checked into no secret store because it protects nothing + # here: sessions and signed URLs in a CI run that is discarded on exit. + # docker-compose.test.yml uses this exact value for the same reason. + APP_KEY: "base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=" + +jobs: + # Fast and dependency-free: catches a formatting slip before anything as + # slow as booting the framework runs at all. + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.PHP_VERSION }} + tools: composer:v2 + coverage: none + + - uses: ramsey/composer-install@v3 + + - run: ./vendor/bin/pint --test + + # SQLite, in memory: the fast loop, for feedback in the couple of minutes it + # actually takes. Postgres is what production runs on, which is what the + # job below is for. + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ env.PHP_VERSION }} + extensions: pdo_sqlite, pdo_pgsql, gd, exif, intl, zip, pcntl, opcache + tools: composer:v2 + coverage: none + + - uses: ramsey/composer-install@v3 + + - run: php artisan test + + # Fails if public/api-types.d.ts was committed out of step with + # openapi.yaml — the one artefact in this repo that is generated and + # checked in rather than built at deploy time, so nothing else would + # ever notice it drifting. + - run: php artisan api:types --check + + # The one this app's own README insists on before trusting a change: this + # project stores translations and image variants in JSON columns and + # queries them by path in about forty places, and SQLite and Postgres do + # not always agree on what that means. Reuses docker-compose.test.yml + # verbatim rather than re-describing the same environment in YAML a second + # time — one place to keep it correct. + test-postgres: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - run: docker compose -f docker-compose.test.yml run --rm test diff --git a/README.md b/README.md index e9d930d..10f6e99 100644 --- a/README.md +++ b/README.md @@ -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 ``` diff --git a/composer.json b/composer.json index df28908..28de4de 100644 --- a/composer.json +++ b/composer.json @@ -6,7 +6,7 @@ "keywords": ["laravel", "framework"], "license": "MIT", "require": { - "php": "^8.3", + "php": "^8.4", "filament/filament": "^5.7", "laravel/framework": "^13.17", "laravel/tinker": "^3.0", @@ -57,7 +57,8 @@ "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi", - "@php artisan filament:upgrade" + "@php artisan filament:upgrade", + "@php -r \"is_dir('.git') && exec('git config core.hooksPath .githooks');\"" ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" diff --git a/composer.lock b/composer.lock index 35fff3c..e854e19 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2c4a3ac0f2adf4cac823692240e8ca81", + "content-hash": "e9bbf5ad6f9f32f9f506caf56db81710", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -12591,7 +12591,7 @@ "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": "^8.3" + "php": "^8.4" }, "platform-dev": {}, "plugin-api-version": "2.6.0" diff --git a/lang/ca/panel.php b/lang/ca/panel.php index 9aad37a..708aa34 100644 --- a/lang/ca/panel.php +++ b/lang/ca/panel.php @@ -196,6 +196,8 @@ return [ 'heading' => 'Enllaços de vista prèvia', 'hint' => 'Es poden compartir i caduquen en :hours hores. Cadascun serveix només per a aquest registre.', 'open' => 'Obrir', + 'opens_new_tab' => '(s\'obre en una pestanya nova)', + 'link_for' => 'Enllaç de vista prèvia en :language', ], 'trash' => [ diff --git a/lang/en/panel.php b/lang/en/panel.php index f1aa18b..de57b47 100644 --- a/lang/en/panel.php +++ b/lang/en/panel.php @@ -196,6 +196,8 @@ return [ 'heading' => 'Preview links', 'hint' => 'Shareable, and they expire in :hours hours. Each one only works for this record.', 'open' => 'Open', + 'opens_new_tab' => '(opens in a new tab)', + 'link_for' => 'Preview link for :language', ], 'trash' => [ diff --git a/lang/es/panel.php b/lang/es/panel.php index 2b7befe..10c1774 100644 --- a/lang/es/panel.php +++ b/lang/es/panel.php @@ -196,6 +196,8 @@ return [ 'heading' => 'Enlaces de vista previa', 'hint' => 'Se pueden compartir y caducan en :hours horas. Cada uno sirve solo para este registro.', 'open' => 'Abrir', + 'opens_new_tab' => '(se abre en una pestaña nueva)', + 'link_for' => 'Enlace de vista previa en :language', ], 'trash' => [ diff --git a/resources/views/filament/pages/site-health.blade.php b/resources/views/filament/pages/site-health.blade.php index 1e0c77d..4234cdf 100644 --- a/resources/views/filament/pages/site-health.blade.php +++ b/resources/views/filament/pages/site-health.blade.php @@ -1,11 +1,18 @@ {{-- The headline. Whether anything is actually wrong is the one thing - somebody opening this page came to find out. --}} + somebody opening this page came to find out. + + Most of the icons below are `aria-hidden`: colour and shape repeating + what the text beside them already says. The one on each check's own + heading is the exception — severity there is never written out in + words, only implied by an icon and a badge colour — so that one gets + a spoken name instead of being hidden. --}} @if ($this->problems()->isEmpty())