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
Les meta `og:image` / `twitter:image` ajoutés en v7.69.0 pointaient vers une
URL relative (`/static/img/logo-512.jpg`). Les crawlers — LinkedIn, Slack,
Discord, iMessage — ignorent une URL relative : la carte de partage de FlowDeck
était donc vide partout où un lien est collé.
Correction via le motif déjà en place dans `app/templating.py` : un global
Jinja `app_base_url()` construit à partir de `settings.app_base_url`, qui est
LA MÊME source que les liens de partage de pages (`collaboration.py:32`).
Si cette valeur est mal renseignée, ces liens l'étaient déjà — comportement
cohérent plutôt qu'une deuxième source de vérité à maintenir.
Lecture paresseuse de `settings` dans la fonction, à l'image de
`_plugin_enabled` : un import au chargement du module créerait un import
circulaire.
Rendu vérifié sur l'instance déployée :
og:image" content="http://localhost:8080/static/img/logo-512.jpg
Tests : `test_og_image_url_is_absolute_via_base_url` vérifie le global ET
l'absence de la forme relative dans les deux templates. Piège évité au passage
— la forme absolue CONTIENT l'ancienne en sous-chaîne, donc l'assertion
négative porte sur `content="/static/img/` (le guillemet avant la barre) et
non sur l'URL complète.
pytest 1413 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.1
e2e 14/14 verts sur l'instance redéployée
Trois bugs d'éditeur, chacun avec sa cause racine prouvée par trace
d'événements — le mécanisme Clipboard livré en v7.68 était inutilisable.
1) Ctrl+C / Ctrl+X multi-blocs ne faisaient RIEN.
Cause : le navigateur ne sait pas sérialiser une sélection qui traverse
deux `contenteditable` (chaque bloc en a un). `getSelection().toString()`
renvoie `""` et Chromium n'émet AUCUN événement `copy` — les handlers
`ct.addEventListener('copy')` n'étaient donc jamais atteints.
→ le raccourci est intercepté au keydown (capture sur document, garde
`closest('#_blocksCt')` pour ne pas voler Ctrl+C ailleurs) et le markdown
est écrit dans le presse-papier par nos soins. Sélection mono-bloc =
comportement natif inchangé.
2) Glisser la souris d'un bloc à l'autre ne sélectionnait qu'un bloc.
Cause : `mouseup` (on reconstruit la plage) → `click` → `focusin` → le
navigateur efface la sélection. La plage était reconstruite puis détruite
dans la foulée.
→ la plage reconstruite est mise en attente (`_mdSelRange`) et restaurée
au tour de boucle suivant si le focus l'a effectivement effacée. Chaque
nouveau geste (mousedown) remet l'attente à zéro, donc un clic pour
placer le curseur reste normal.
3) Le clic droit effaçait le surlignage.
Cause : `.bm-search` prenait le focus 10 ms après l'ouverture du menu.
→ le menu ne vole plus le focus quand un texte est sélectionné. Les
actions Clipboard utilisaient déjà la sélection figée à l'ouverture
(`d._clipSt`) ; le `preventDefault` du clic droit est remonté sur
`mousedown` (sur `contextmenu` il arrive trop tard).
Identité visuelle (bannière + logo du projet ajoutés à la racine) :
- assets optimisés servis : `static/img/banner.webp` 22,5 Ko (l'original
fait 1,6 Mo) + JPEG de secours, `logo.webp` 5,3 Ko + `logo-512.jpg` ;
originaux conservés dans `assets/` pour régénération.
- icônes PWA régénérées à partir du vrai logo (72 → 512 px, marge
maskable sur 192/512) + apple-touch-icon.
- landing : logo dans la barre de navigation, bannière en <picture>
(WebP + JPEG) avec dimensions déclarées (pas de décalage de mise en page).
- `og:image`, `twitter:card` et `meta description` ajoutés sur la landing
ET dans base.html — la landing a son propre <head>.
Tests : `tests/test_v769_selection_persistence.py` (19 statiques) +
`e2e/v769_selection_persistence.spec.js` (5, vrais gestes : drag souris,
Ctrl+C/Ctrl+X au clavier, clic droit). `e2e/v768_columns_clipboard.spec.js`
corrigé : les gestes simulés (`execCommand`, plage fabriquée en evaluate)
sont remplacés par de vrais gestes, et le token statique obsolète
`si>=ei)return null;` aligné sur la garde `isCollapsed` réelle.
pytest 1413 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.0
- 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