Second Brain documentation

ADR 0034 – Application installable (PWA)

Accepté (2026-10-16, mode autonome)

← Toutes les décisions

ADR 0034 – Application installable (PWA)

Statut : Accepté (2026-10-16, mode autonome)

La ligne PWA du jalon 2 demande un manifeste, un service worker de la coquille et l’installation sur le téléphone. Le 2026-10-15, le périmètre a été précisé : icônes « Hexa facetté », coquille hors ligne, partage vers l’inbox (texte, lien, image, PDF), photo depuis « + Capturer » sur mobile, raccourcis du manifeste (Capturer, Inbox, Aujourd’hui), et Ctrl+N, Ctrl+W, Ctrl+Tab dans l’application installée (ADR 0031).

Décision

Manifeste (public/manifest.webmanifest)

  • display: standalone, start_url et scope à /, id: /, langue fr, theme_color du chrome (#F0F0F0).
  • Icônes : icone.svg (any), icone-192.png et icone-512.png existants, et deux icônes maskable (192, 512) tirées de icone-maskable.svg. Ce SVG a un fond plein jusqu’aux bords, et l’hexagone y est réduit à 80 % pour tenir dans la zone sûre que les lanceurs Android découpent. Les PNG ont été générés une fois avec Chrome sans interface et sont versionnés, comme ceux de l’ADR 0021.
  • Raccourcis (appui long sur l’icône, clic droit dans la barre des tâches), chacun avec une icône de 96 px dessinée dans la charte (raccourci-*.svg et .png) :
    • « Capturer » ouvre /inbox?capturer=1, dont le champ de capture a déjà le focus ;
    • « Inbox » ouvre /inbox ;
    • « Aujourd’hui » ouvre /. L’accueil deviendra le dashboard par défaut (ligne « Dashboards » du backlog) : le raccourci n’aura pas à changer.
  • Partage (share_target) : POST /share-target en multipart/form-data, avec title, text, url et des fichiers files (image/*, PDF).
  • index.html le déclare avec crossorigin="use-credentials" : derrière la connexion OIDC, le manifeste est alors demandé avec la session.

Service worker : celui d’Angular, précédé d’un écouteur de partage

  • @angular/service-worker (ngsw-config.json, provideServiceWorker), désactivé en développement et enregistré quand l’application est stable (30 s au plus).
  • Coquille hors ligne : index.html, le manifeste, tous les JS et CSS du build (chunks différés compris) et les polices woff2 sont préchargés à l’installation. Les images sont mises en cache à la première lecture. L’API n’est pas mise en cache : hors ligne, la coquille s’affiche et la barre d’état indique « Hors ligne · les modifications ne seront pas enregistrées ». Lire et écrire hors ligne n’est pas prévu ici.
  • Navigation : le worker ne répond jamais par la coquille pour /api, /actuator, /oauth2, /login, /logout, /swagger-ui, /v3 ni /share-target. La connexion OIDC passe donc toujours par le serveur. Une session expirée se voit au premier appel d’API (401), et l’intercepteur existant renvoie vers la connexion.
  • public/sw.js est le script enregistré. Il déclare d’abord son écouteur fetch pour POST /share-target, puis importe ngsw-worker.js. Le worker d’Angular répond à toutes les requêtes : l’écouteur de partage doit passer avant lui et arrêter la propagation.

Partage vers l’inbox

  • Où le recevoir. Le POST du système n’a ni jeton CSRF ni en-tête de l’application. Le recevoir côté serveur aurait demandé une exception CSRF sur un endpoint qui écrit. Le worker range donc le formulaire tel quel dans le cache sb-share (/share-target/<horodatage>-<uuid>), puis redirige (303) vers /inbox?partage=1.
  • Rien n’est écrit sans un clic. N’importe quel site peut soumettre un formulaire vers /share-target, et le worker ne peut pas distinguer ce POST de celui du système. Créer les éléments automatiquement reviendrait à l’exception CSRF qu’on voulait éviter, déplacée côté client (relevé en revue). L’inbox affiche donc un bandeau « Contenu partagé reçu » avec le titre et les noms des fichiers, et deux choix : « Ajouter à l’inbox » ou « Ignorer ».
  • Écriture, par l’API habituelle, avec la session et le CSRF de la page :
    • une note pour le texte et le lien (sharedNote) : le titre partagé, sinon la première ligne du texte, sinon le lien. Le corps reprend le texte et le lien, sans répéter le titre ni un lien déjà présent dans le texte ;
    • un élément par fichier (FileUploader.createNodes, comme un dépôt), avec le récapitulatif d’envoi habituel.
  • Jamais perdu, jamais en double. L’entrée du cache n’est effacée qu’une fois tout écrit, ou sur « Ignorer ». Hors ligne, en cas d’erreur ou de session expirée, le bandeau reste, et il revient à la prochaine ouverture de l’inbox. Si la note est écrite mais qu’un fichier échoue, seuls les fichiers restants sont proposés à nouveau.
  • Filtrage. Seuls les images et les PDF sont gardés, comme le manifeste les annonce (son filtre ne s’applique qu’à la feuille de partage du système), et au plus 10 fichiers.
  • Purge. Une entrée de plus de 7 jours, ou d’une autre forme, est effacée à la lecture suivante. Un appareil partagé ne garde donc pas indéfiniment un contenu jamais traité.
  • Sans worker actif (juste après l’installation, ou après une désinscription), le POST arrive au serveur. ShareTargetController répond 303 vers /inbox?partage=echec sans rien lire, et l’inbox demande de partager à nouveau. Cette seule adresse est exemptée du CSRF, puisqu’elle n’écrit rien.

Photo depuis « + Capturer »

Sur un écran tactile (pointer: coarse) ou étroit, le menu « + Capturer » propose « Prendre une photo » sous la capture de texte : input type=file accept=image/* capture=environment. La photo part dans l’inbox comme un fichier déposé, et le résultat est annoncé dans le menu (role=status). FileImportButton gagne les entrées accept et capture.

Ctrl+N, Ctrl+W, Ctrl+Tab

Ils sont déjà reconnus en mode standalone (ADR 0031). Le manifeste en display: standalone les active. Dans une fenêtre d’application, Chrome ne réserve plus ces touches au navigateur : la page les reçoit et les garde par preventDefault. Ce comportement de Chrome ne se vérifie pas par un test automatique. Il reste à confirmer sur l’application installée.

Serveur

  • Manifeste et icônes publics en mode oidc (/manifest.webmanifest, /favicon.*, /icone*, /raccourci-*.png). Pour proposer l’installation, le navigateur les lit parfois sans cookie. Ce sont des fichiers publics du front. Le reste, sw.js compris, demande toujours une session.
  • ManifestController sert le manifeste avec le type application/manifest+json, que Tomcat ne connaît pas pour .webmanifest, et avec Cache-Control: no-cache. Un changement d’icône ou de raccourci arrive ainsi à l’application installée. Les autres fichiers gardent les en-têtes par défaut de Spring Security (no-cache, no-store) : une nouvelle version du worker est vue au contrôle suivant du navigateur.
  • Correction trouvée en route : SpaForwardController envoyait index.html pour /media/*.woff2, le dossier des polices du build Angular. Dans l’image complète, les icônes Material et les polices de la charte ne se chargeaient pas. media est maintenant exclu, et un test d’intégration le vérifie. Les exclusions portent désormais sur le segment entier : une future route /mediatheque recevra bien la page.

Vérification

Avec Chrome sans interface, sur le build de production servi par Spring Boot :

  • le manifeste est lu sans erreur et Chrome ne remonte aucune erreur d’installabilité ;
  • le worker est actif et contrôle la page après rechargement ;
  • un partage simulé (POST multipart avec titre, texte, lien et image) est proposé dans l’inbox, puis crée une note et un élément fichier après le clic ;
  • hors ligne, /inbox affiche la coquille avec ses polices et l’indication « Hors ligne ».

Reporté

  • Lecture et écriture hors ligne (file d’attente des captures, données en cache).
  • Notifications push, synchronisation en arrière-plan, launch_handler, file_handlers.
  • Invitation à installer dans l’interface (le navigateur la propose lui-même), annonce d’une nouvelle version (SwUpdate).
  • Bascule clair/sombre de theme_color dans le manifeste (une seule valeur possible).

Conséquences

  • Toute nouvelle entrée de données externe (partage, gestionnaire de fichiers) passe par une confirmation avant d’écrire.
  • Toute nouvelle route serveur hors /api qui ne doit pas recevoir la coquille s’ajoute aux navigationUrls de ngsw-config.json et à l’exclusion de SpaForwardController.
  • Les icônes PNG et de raccourcis sont à régénérer si icone.svg change.
  • Hors du développement, un changement du front n’est visible qu’après la mise à jour du worker, au plus tard au deuxième chargement.