- Réaction sur texte : bouton 🙂+ (« React to selected text ») dans la toolbar
de sélection → sélecteur d'émoji EXISTANT en mode réaction (en-tête = texte
sélectionné + ✋, onglets masqués, ligne Recent au-dessus de la grille
People, Filter… et barre de catégories avec + custom inchangés)
- Backend : migration 37 text_reactions (UNIQUE plage × emoji × utilisateur →
POST idempotent) + GET/POST /api/pages/{id}/reactions (GET groupé par plage
emoji/count/mine, POST toggle, 400 sur plage vide)
- Affichage : plage commentée/réagie peinte en jaune (_paintAnchors après
render/loadComments/loadReactions), pastille 😀+compte après la plage, clic
pastille = rouvrir le sélecteur, clic ancre = ouvrir et centrer le fil
- Pastilles = annotations : gtTok/gtMd les ignorent (y compris le chemin
rapide innerText) et les offsets _srcOffset/_srcNodeLen les excluent → le
texte sauvé est identique (vérifié dans le e2e)
- Tiroir refondu : carte de fil (avatar, nom, horodatage, ✓ toggle
resolve/reopen, ⋯ → Copy link #cmt-<id>/Delete), saisie « Add a comment... »
avec @ et envoi ↑/Entrée ; #cmt-<id> ouvre et centre le fil au chargement ;
loadComments + loadReactions à l'ouverture (commentCount enfin renseigné)
- Fixes : resolveComment ne pouvait pas rouvrir (toujours resolved:true) ;
showCommentBtn ignorait les sélections à ancre élément (bouton 💬 jamais
affiché)
- Tests : tests/test_v764_comments_reactions.py (8, dont toggle API réel),
e2e/v764_comments_reactions.spec.js (8 étapes) — 3/3 specs e2e verts,
suite complète 1383 passed / 0 failed, ruff OK, OpenAPI 7.64.0 (526 chemins)
- Conversion `async def` → `def` de TOUTES les routes dont le corps ne contient
ni `await`, ni `async with`, ni `async for`, ni `asyncio` (scan automatique
corps par corps sur app/ : 352 converties, 0 dangereuses, vérifié
`asyncio`/`run_coroutine`/`.result()` absents). FastAPI exécute ces handlers
dans son threadpool → tout leur SQLite (`get_conn()` + `conn.execute`) quitte
l'event loop, sans changer une ligne de logique.
- Répartition : api_v2 60, dashboard 40, collections 25, board 23,
workspace 19, wiki 17, permissions 14, api 14, main.py 6, + 35 fichiers.
- Les 4 routers prioritaires de l'audit sont couverts par ce lot :
api_v2 60 + dashboard 40 + collections 25 + board 23 = 148 conversions
(le reste de leurs routes attend la phase 2 : elles ont de vrais `await`).
- Reste (phase 2) : les 311 routes avec de vrais `await` → enrouler les blocs
DB dans `await anyio.to_thread.run_sync(...)` ; pas de wrapper partagé livré
(rien ne l'appellerait — YAGNI jusqu'au premier usage).
suite **1037/1037** (233 s) · `ruff check app tests` OK · docs à jour
- A25 — 84 `except Exception: pass/…` → `logger.exception("<fonction>")`
(19 fichiers : api_v2 30, dashboard 10, board 7, sites 5, workspace 5,
api_v2_helpers 5, …) ; `logger` ajouté là où il manquait (api_v2_helpers,
sites + `import logging`)
- A25 critique — les `try` autour de `materialize_properties` supprimés dans
`create_collection_v2` ET `apply_db_template_v2` : un échec interrompt la
transaction au lieu de commiter une collection sans schéma
- test `test_collection_rollback_when_materialize_fails` (Bearer v2, monkeypatch
qui lève, assertions : RuntimeError + 0 ligne commitée)
- A21 partiel — `PRAGMA busy_timeout=5000` dans `get_conn()` (point d'entrée
unique) ; commentaire `ponytail:` : le wrapper async + les 510 call sites
restent à migrer module par module
- suite **1028/1028** · `ruff check app tests` OK
tests/conftest.py: mutate the settings singleton (instead of rebinding) so DB + backup dir are isolated per test -> pytest-xdist safe.
Real backup tests (snapshot/prune/admin API) and OAuth mock tests (Gitea/GitHub/link) replace the previous skips.
init_db() now also creates webhook_subscriptions (full schema without the FastAPI lifespan).
ruff check is clean; .eslintrc.json migrated to eslint.config.mjs (flat config).
CI: lint job (ruff + eslint), parallel tests (-n auto), run on every branch push.
VERSION 5.11.1.