Le favicon était un carré bleu #2383E2 avec les lettres « FD » écrites en
<text> SVG : génériques, sans aucun lien avec la marque, et rendues avec la
police installée chez le visiteur (donc instables).
static/favicon.ico est désormais généré depuis assets/logo.jpg — la même
source que la bannière et les icônes PWA. 16/32/48 px dans un seul fichier,
un seul <link> pour tous les navigateurs, 4,1 Ko.
Pourquoi un fond sombre et pas le logo nu :
le logo est blanc à ~82 % (mesuré via Pillow — 50,7 % de blanc pur, plus
32 % de blancs proches). Posé nu, il se dissoudrait sur un onglet clair,
qui est le cas nominal de Firefox et de Safari. Il est donc posé sur un
carré #191919 arrondi, LE MÊME fond que les icônes PWA générées en
v7.69.0, ce qui garde l'identité cohérente entre onglet, écran d'accueil
et partage.
Contraste mesuré à 16 px : σ = 106 avec le fond sombre, contre σ = 49 pour
le logo seul — le fond sombre double littéralement la lisibilité à cette
taille. Le test statique verrouille les tailles embarquées dans l'ICO.
Mise à jour des 5 templates qui pointaient vers l'ancien SVG (base, import,
landing, public_page, welcome) + la liste de pré-cache du service worker.
static/favicon.svg SUPPRIMÉ : plus aucune référence dans le dépôt, le test
le vérifie pour éviter qu'il revienne.
pytest 1416 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.2
- app.js : x-ignore sur .main-wrapper au swap fdLoad (nœud remplacé
uniquement), démontage unique quand tous les <script src> ont exécuté
(load/error + filet 4s) via Alpine.initTree idempotent (_x_marker) —
fini les cascades « Undefined variable » (éditeur) et « reading 'has' »
($store.fdCtx) : la zone s'initialisait avant composants et stores.
- 5 scripts de page gardés contre la ré-exécution (SyntaxError
« Identifier 'LW' has already been declared » à la 2e visite
partielle) : local_workspace, board, settings, database_table,
page_editor_realtime.
- if (window.Alpine) → Alpine.data immédiat sinon listener alpine:init
(déjà passé sur swap) : 8 scripts + 7 templates inline.
- app.css : purge des 7 blocs @font-face Inter orphelins (fichiers
inexistants → 302 HTML → « Failed to decode downloaded font »).
- Gates : e2e/regression_editor_mount + e2e/regression_partial_nav
(7 pages x complet/1er/2e passage) + regression_logout_dnd + smoke
= 6/6 verts sur l'image rebuildée · pytest 1094 passed · ruff OK ·
OpenAPI 7.45.2.
- ContentSecurityPolicyMiddleware : nonce aléatoire par requête dans la
ContextVar `CSP_NONCE` (posée avant `call_next` → visible des templates),
`script-src 'self' 'unsafe-eval' 'nonce-…'` — plus aucun script inline
sans nonce ne tourne (fin des XSS injectés en JS)
- 38 tags `<script>` des templates : `nonce="{{ csp_nonce() }}"` (passage
scripté, vérifié : 0 restant) ; `LOCAL_LOGIN_HTML` (constante de module) :
helper `_with_nonce()` au rendu ; collections.py : 3 scripts Python
(chart/form/map) noncés
- `<meta name="htmx-config" content='{"inlineScriptNonce": …}'>` dans base.html
: htmx ré-injecte les <script> des réponses boostées avec le bon nonce
- `script-src-attr 'unsafe-inline'` : les 74 handlers `onclick=` inline
restent couverts (le nonce les aurait désactivés aussi)
- chart.js (cdn.jsdelivr.net) et leaflet (unpkg) ajoutés à script-src/style-src
: vues chart/map déjà BLOQUÉES par la CSP depuis toujours
(commentaire ponytail: upgrade = vendoriser puis retirer les hôtes)
- reste d'A20 : unsafe-eval (Alpine x-data → @alpinejs/csp), externalisation
JS (A27), resserrer img-src/connect-src
test : test_csp_nonce_per_request (page base.html + page hors template,
nonce unique par requête)
suite **1037/1037** · `ruff check app tests` OK · docs à jour
- Scan de tous les appels `fetch` non-GET du front par préfixe : 12 préfixes
n'ont AUCUN call site sans `X-CSRF-Token` → sortis de `EXCLUDED_PATHS`
(`/db/`, `/workspace`, `/workspace/automations`, `/board/api/favorites`,
`/api/pages`, `/api/recents`, `/api/notifications`, `/api/comments`,
`/api/automations`, `/api/github`, `/api/admin`, `/api/onboarding`)
- `welcome.html` : les 2 POST `/api/onboarding/*` reçoivent le header
(`adminFetch` prouve que `/api/admin` était déjà couvert)
- reste A19 (front à équiper) : `/api/workspace` (+`/api/workspaces`),
`/api/local-workspace`, `/api/settings`, `/api/gitea`, `/api/agent` — 49 fetch
- tests : helper `anon_csrf()` (anonyme + CSRF valide) pour isoler les 401 de
route du 403 middleware — 4 tests d'anonymat ajustés
- suite **1026/1026** · `ruff check app tests` OK
Rocket Loader (Cloudflare) reecrit les balises script inline en type=...-text/javascript et les execute de facon differee, ce qui casse l'enregistrement des composants Alpine et l'init des pages lors d'un acces direct via le tunnel (ex. /settings ne se chargeait pas au complet). Ajout de data-cfasync=false sur les scripts inline des pages completes et des partiels du shell (settings, accounts, trash, workspace, board, gitea_workspace, welcome, import, page_editor_collection/embed, _database_table_scripts, _notification_bell, public_page, local_workspace). Les fragments charges via HTMX restent inchanges.