6 Dev environment
Claude Agent edited this page 2026-09-23 23:25:33 +01:00

Environnement de test local

Une instance Nextcloud jetable avec l'ExApp branchée dessus, en une commande.

make up

Puis http://localhost:8080 — identifiants admin / admin.

L'entrée « Organisateur Familial » apparaît dans la barre de navigation.

Un second compte, alice / alice-dev-password, est créé sans aucune famille. Deux comportements ne s'observent que depuis un tel compte : l'assistant de première utilisation, et le 404 opposé à un non-membre. admin a une famille dès le premier essai et n'en a plus jamais zéro.

L'assistant de bienvenue de Nextcloud (firstrunwizard) est désactivé : son carrousel modal se posait par-dessus l'application et interceptait les clics.

Ce que ça monte

navigateur ──▶ nextcloud:80  (publié sur localhost:8080)
                   │  proxy AppAPI
                   ▼
                exapp:9030 ──▶ nextcloud:80  (appels OCS)
                   │
                   └─▶ db:5432 (PostgreSQL, pour Nextcloud)
Service Image Rôle
db postgres:16-alpine Base de Nextcloud
nextcloud nextcloud:34-apache L'instance. AppAPI est fourni d'office depuis NC 30.0.1
exapp dev/Dockerfile.dev Le backend, en rechargement à chaud (tsx watch)

Le mode de déploiement est manual-install : l'ExApp tourne dans son propre conteneur et on l'enregistre à la main. Pas de socket Docker exposé à Nextcloud, pas de HaRP. C'est le chemin le plus léger pour boucler vite ; la validation HaRP est un exercice séparé (voir plus bas).

Boucle de développement

Ce que tu changes Ce qu'il faut faire
Backend (ex_app/lib/src/) Rien — tsx watch recharge tout seul
Frontend (ex_app/src/) make reload
appinfo/info.xml (routes, env vars) make reload
package.json du backend docker compose up -d --build exapp

Commandes utiles :

make logs-exapp             # logs du backend, en continu
make logs-nc                # logs de Nextcloud
make occ ARGS='app_api:app:list'
make shell                  # shell dans le conteneur ExApp
make stop                   # arrêt, données conservées
make reset                  # tout supprimer, volumes compris

Validate application behavior through the AppAPI proxy. Direct access to port 9030 is useful only for process-level diagnostics such as /heartbeat; it bypasses the proxy and cannot validate browser access, route declarations, caching or language forwarding.

curl -u admin:admin http://localhost:8080/apps/app_api/proxy/organisateur_familial/api/hello

Use the Playwright suite for browser behavior. For repeated requests and test seeding, use the app-password setup described below rather than the account password.

Demo data

make seed

Gives admin a family named « Famille Démo » with lists, tasks, recipes, a week of meals and a few events, every date computed from today. Re-running it deletes that family and builds it again, so the dates move back to around today; other families of admin are left alone.

It goes through the application, as the e2e suite does: it makes sure the service account exists and is configured (with the suite's password, so the suite can still log in as it), creates the family through the API, deletes the two empty starter lists (D41), then imports a document in the export format (D36, D37) built by dev/seed-data.mjs. Adding data means adding to that document.

Nothing runs it but you. make up does not call it, CI does not call it, and the production image never copies dev/. The e2e suite deletes it, like every family of admin (D29) — run make seed again after a run.

Traductions

Les catalogues sont des fichiers générés et versionnés (D38). Après avoir ajouté ou reformulé une phrase visible par un membre :

make l10n-build     # extrait les sources, régénère ex_app/l10n/{fr.js,fr.json}
make l10n-check     # régénère dans un temporaire, compare, et vérifie la couverture

l10n-check refuse une source sans français, une entrée vide ou fuzzy, un message compté auquel il manque une forme, et une traduction qui a perdu un {placeholder} ou son %n. Il a besoin de Docker — l'outil officiel est un phar PHP épinglé, exécuté dans le conteneur dev/Dockerfile.l10n — et c'est pour cela qu'il forme un job de CI à part, sur le runner hôte.

Deux copies existent sur la stack locale, et make reload s'occupe des deux :

  • le backend lit ex_app/l10n/, monté en lecture seule sur /assets/l10n ;
  • Nextcloud lit <writable-apps>/organisateur_familial/l10n/, où dev/setup.sh installe les catalogues — une installation manuelle n'est jamais dépaquetée par AppAPI, donc rien ne les y met tout seul. Ne jamais y copier appinfo/info.xml : Nextcloud prendrait le dossier pour une app PHP.

Le script que Nextcloud injecte pour le navigateur ne porte pas de paramètre de version : après une mise à jour des catalogues, un navigateur peut servir l'ancien depuis son cache jusqu'à expiration. Un rechargement forcé le règle.

La suite end-to-end lit du français et l'impose (imposeFrench dans e2e/tests/instance.ts, plus le locale de Playwright) ; tests/i18n.spec.ts tient l'autre bout avec un compte anglais.

Comment l'enregistrement fonctionne

dev/setup.sh est idempotent et fait trois choses :

  1. active app_api ;
  2. enregistre un deploy daemon manual_install dont l'hôte est exapp — le nom de service compose, c'est ainsi que Nextcloud atteint le conteneur ;
  3. enregistre l'ExApp avec --json-info.

Le JSON est généré depuis appinfo/info.xml par dev/json-info.mjs, pour ne pas maintenir deux déclarations de routes. Deux raisons de ne pas passer --info-xml directement :

  • AppAPI générerait un secret et un port aléatoires, alors que le conteneur a déjà un APP_SECRET figé — tout retournerait 401 ;
  • en --json-info, access_level doit être un entier (PUBLIC=0, USER=1, ADMIN=2). La conversion depuis le nom n'a lieu que dans la branche --info-xml d'AppAPI. Le générateur s'en charge.

Réenregistrer est la façon normale de rejouer /init et /enabled contre le nouveau code.

Pièges rencontrés

Ne pas rm -rf ex_app/src/dist pendant que la stack tourne. Compose bind-monte ex_app/src/dist/js ; supprimer le répertoire laisse le montage sur un inode mort et l'ExApp répond 404 sur ses assets jusqu'à un docker compose up -d --force-recreate exapp. make clean vide le contenu sans supprimer les répertoires.

Le bundle frontend doit s'appeler .js, pas .mjs. AppAPI enregistre un script fictif puis réécrit le src en concaténant .js en dur au chemin enregistré. Corollaire : le bundle est chargé en <script> classique, sans type="module" — il doit rester auto-suffisant (IIFE). C'est configuré dans ex_app/src/vite.config.js.

Un daemon ne peut pas être réenregistré tant qu'il a des ExApps attachées. setup.sh l'ignore et continue, c'est sans conséquence.

Valider HaRP

L'environnement ci-dessus n'exerce pas le chemin HaRP (socket Unix, FRP), qui est pourtant celui de la production sur NC 32+. Le backend gère les deux modes et le mode socket a été vérifié en local hors Docker, mais le tunnel FRP complet reste à éprouver.

Pour le faire, il faut ajouter le conteneur HaRP à la stack et enregistrer un daemon docker-install --harp, ce qui implique de laisser AppAPI déployer l'image de production plutôt que d'utiliser manual-install. À traiter quand la question D3 (polling vs WebSocket) sera tranchée — voir Architecture.

Continuous integration runs on the same kind of stack

Written in English per the language rule in AGENTS.md.

CI is a forgejo-runner on the machine that hosts the forge. It brings the stack of this page up for real — make up, dev/setup.sh, the whole thing — so everything above applies to it, plus what follows. See Workflow for what the two jobs run.

Its labels are mapped by hand, and only one is a container:

labels:
  - self-hosted:host
  - ubuntu-latest:host
  - linux_arm64:docker://node:22-bookworm

ubuntu-latest is host execution here, not a container — the name is misleading and cost a wrong assumption once. checks therefore runs on linux_arm64, which isolates it from whatever the machine has installed; the end-to-end job runs on self-hosted, and must.

The e2e suite has to run on the host, because it drives docker compose itself. e2e/tests/instance.ts shells out to docker compose exec from inside the Playwright process, with the repository as its working directory: that is how psql() and occ() reach the stack. So Nextcloud cannot be a workflow services: — the tests would not find it — and a containerised job would need the Docker socket, the CLI, and the checkout at the same path inside and out, since the bind mounts in compose.yaml resolve on the host. The last condition fails silently: an empty directory mounted over the sources raises nothing, it just produces a suite testing a bundle that is not there.

A containerised job cannot reach the forge without help. Measured three ways: from the default bridge to the public address, fine; from a user-defined network — which is what the runner creates per job — refused in 200 ms; from a user-defined network to the host gateway, fine. Hence, in the runner's config.yml:

container:
  options: --add-host=git.lozach.eu:host-gateway

Without it actions/checkout fails before anything else runs.

The runner's systemd unit is hardened, and two of its options reach into the jobs. SystemCallFilter= has no SystemCallErrorNumber=, so a filtered call kills the process rather than failing: npm ci died instantly on Bad system call, which is SIGSYS, until a drop-in set SystemCallErrorNumber=EPERM. Which set was responsible was never identified, and the drop-in makes it moot. And ProtectSystem=full keeps /usr read-only, so a job cannot apt-get: Chromium's system libraries are installed once on the host by hand, and the workflow calls playwright install chromium without --with-deps.

Two things the workflows deliberately do not use. No cache: npm — the runner's cache server announces itself on an address a job network cannot reach, which cost one run 4 min 38 of timeout for a cache that never restored. And no actions/upload-artifact, whose v4 refuses any server that is not github.com; forgejo/upload-artifact@v4 is the fork without that check.

The machine is not reliable, and this is unresolved. One run had Chromium's renderers collapse progressively — 8 tests passed, 27 failed on Page crashed — and a cleanup and a reboot made it go away without the cause ever being found. Another timed out a single test on a click whose element was visible, enabled and stable. Vitest's worker pool is capped at maxWorkers: '50%' for the same reason: an unconstrained container is told about every core of the host, including the ones already serving the forge, and starving workers blew a 5 s test timeout on assertions that take under a millisecond. Expect the suite to go red occasionally without the code being at fault, and check a red run against a second one before believing it.

Repartir de zéro

make reset && make up

Supprime les trois volumes (instance Nextcloud, base, données de l'ExApp) et reconstruit tout. Compter deux à trois minutes pour la réinstallation de Nextcloud.