Les 3 confirmations destructives de la poubelle (suppression définitive
unitaire, suppression groupée, Empty Trash) remplacent le confirm() du
navigateur par une fenêtre FlowDeck :
- réutilise les classes flowdeck-modal-overlay / .flowdeck-modal déjà
présentes dans base.html (variables --bg-secondary / --border / --accent,
rayon 12 px, boutons Cancel + confirmation coloré) → aucun nouveau CSS ;
- overlay cliquable pour annuler, Échap pour annuler, × pour annuler ;
- bouton de confirmation porteur du libellé exact (Delete / Delete all /
Empty Trash) et message reprenant le nom de la page ou le nombre de pages ;
- role="alertdialog" aria-modal + aria-labelledby ;
- composant dans trashData() : askConfirm / cancelConfirm / acceptConfirm,
mutations de propriétés individuelles (pitfall réactivité Alpine) ;
- id #fd-trash-confirm (la classe flowdeck-modal-overlay sert déjà à
#fd-global-folder-modal → locator unique côté test) ;
- aucun confirm() natif ne subsiste dans trash.html.
Gates : e2e/trash_ui.spec.js étendu — ouverture + titre + message de la
fenêtre, ANNULATION vérifiée (page toujours présente), confirmation
(suppression réelle : plus dans la poubelle + contenu effacé), fenêtre Empty
Trash ouverte/annulée quel que soit le contenu, purge réelle seulement si la
poubelle ne contient que des pages E2E (instance partagée) ; le handler
dialog du test échoue si un dialog natif revient (NATIVE_DIALOG). Suite e2e
11 passed · pytest 1099 passed · ruff OK · OpenAPI 510 chemins / 7.45.6.
Page /trash alignée sur ce que fait ce type de section :
- sélection multiple (Select all porté au filtre courant) + barre d'actions
groupées Restore / Delete / Clear ;
- Empty Trash via POST /board/api/trash/empty (purge en masse, 1 requête) ;
- tri Recently deleted / Oldest first / Name et filtre emplacement RÉELS —
remplacent les deux boutons décoratifs « Last edited by ▾ » et « In ▾ » qui
ne faisaient rien ;
- date de suppression + jours restants par élément (rétention 30 j alignée sur
app/services/trash.py, horodatages UTC/ISO gérés) ;
- compteur de résultats, états vides distincts (vide vs filtre sans résultat +
Clear filters), toasts sur chaque action.
Bugs trouvés en route et corrigés :
1. Restore/Delete de la page ne marchaient JAMAIS : getCsrf() renvoie la chaîne
du jeton mais le code faisait csrf?.[1] → 2e caractère → 403 CSRF silencieux
avalé par if (r.ok).
2. « Move to Trash » de l'éditeur = 404 permanent : route appelée
/board/api/pages/{id}/trash (inexistante) alors que la route réelle est
/api/pages/{id}/trash, et le .then() naviguait quand même → la page partait
à l'accueil SANS être mise à la poubelle. URL corrigée + r.ok vérifié.
3. Page restaurée invisible en Library/Recents/Private : la suppression éditeur
réécrivait parent_section='Trash' et la restauration ne le remettait pas,
alors que tous les listings filtrent parent_section != 'Trash'. Écriture
retirée (deleted_at = source de vérité unique) + réparation idempotente au
boot dans db.init_db + requête sidebar /trash alignée sur deleted_at.
4. Routes trash sans aucune authentification (le CSRF ne protège pas : cookie
lisible + en-tête forgé) : 401 ajouté sur list/restore/delete/empty et sur
POST /api/pages/{id}/trash — vérifié anonyme → 401.
Gates : tests/test_trash_api.py (5) · e2e/trash_ui.spec.js (1, avec garde « on
ne vide jamais la poubelle d'autrui ») · pytest 1099 passed · ruff OK ·
e2e probe_nav_perf + partial_nav + editor_mount + logout_dnd + smoke = 10 passed ·
OpenAPI 510 chemins / 7.45.5.
- Symptôme rapporté : clic sur Home → rafale de menus/fenêtres + bande rouge
« You are offline. Changes will sync when connection is restored. ».
- Constat : le masquage de v7.45.3 ne couvrait QUE les swaps partiels fdLoad.
Un chargement COMPLET de document (F5, première visite, navigation servie par
le service worker) peignait toute l'app tant qu'Alpine n'avait pas initialisé
.app-layout : sidebar brute (sections ouvertes, menu utilisateur) + zone brute
dont la bande hors ligne (aucun x-cloak dessus). Le symptôme Home n'est par
ailleurs PAS rejouable en navigateur frais (nav, clic Home en ligne/hors
ligne, plein chargement → 0 frame de contenu brut, 0 erreur console).
- Fix : x-cloak sur le nœud racine Alpine .app-layout (base.html) — sidebar +
zone + bandeau masqués jusqu'au montage d'appState(), retrait par Alpine à
l'init. Le probe asserte que l'attribut est bien retiré et que le node est
visible en fin de parcours (garde-fou contre la page blanche).
- Suppression de la classe fd-navigating (posée à chaque fdLoad, aucune règle
CSS dans le dépôt → code mort).
- e2e/probe_nav_perf.spec.js : 4 scénarios avec assertions (nav 5 pages, clic
Home réel, Home hors ligne avec SW, plein chargement 3 pages) + capture
screenshot auto si contenu brut + collecte console.
Gates : probe_nav_perf 4/4 · regression_partial_nav + regression_editor_mount
+ regression_logout_dnd + smoke = 6 passed (errs=0 sur 7 pages × 3 passages) ·
pytest 1094 passed · ruff OK · OpenAPI 7.45.4.
- Symptôme : à chaque changement de page/section, une rafale de fenêtres /
menus / états s'affiche une fraction de seconde avant la page voulue.
- Cause : pendant la fenêtre x-ignore (swap fdLoad → Alpine.initTree, mise en
place en v7.45.2), .main-wrapper était peint NON montée : tous ses [x-show]
visibles à leur valeur brute, puis masqués d'un coup au montage. Mesure
(e2e/probe_nav_perf.spec.js sur l'instance locale) : 19 éléments bruts
visibles 87 ms sur /library (CLS 0.149, sources lib-loading/lib-empty/
lib-table), 80 éléments pendant 732 ms sur /settings, 7 sur /workspaces.
- Fix (app.js, handler htmx:afterSwap) : opacity:0 + pointer-events:none posés
dans la même task que le swap (aucun paint intermédiaire possible), retirés
APRÈS Alpine.initTree sur les 3 chemins de démontage (scripts chargés, zéro
script, filet 4 s). reveal() est appelé avant les early-returns de done() →
impossible de rester bloqué invisible.
- Après : 0 frame de contenu brut peint sur les 4 navigations du probe,
1–2 frames masquées, zone révélée (opacity:1, pointer-events:auto) et
montée (0 [x-show] non montés, x-ignore absent) à chaque fois.
- Gate : e2e/probe_nav_perf.spec.js — marques htmx, fenêtre x-ignore,
échantillon par frame des [x-show] bruts peints, layout-shift + assertion
(0 brut / zone révélée et montée), exécutable en CI.
- Reste (non traité, voir ROADMAP) : CLS résiduel 0.057 sur /library (états
loading → table), fenêtre x-ignore de ~250 ms sur /settings (coût des
scripts de page), topbar incluse dans .main-wrapper re-swappée à chaque nav.
Gates : probe_nav_perf + regression_partial_nav + regression_editor_mount +
regression_logout_dnd + smoke · pytest 1094 passed · ruff OK ·
OpenAPI 7.45.3.
- 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.
- SW : le fetch event de navigation arrive en redirect:'manual' → toute 302
du serveur (logout → /auth/login) se lisait opaqueredirect (status 0) →
« bad status » → page hors ligne. networkFirst rejoue la requête en
redirect:'follow' et sert une 302 synthétique vers l'URL finale (Chromium
refuse une response 'redirected' servie à une navigation → ERR_FAILED).
timeoutFetch annule désormais réellement (AbortController branchée).
- Local workspace : _doUpload (upload/upload-folder) et toggleFavorite
n'envoyaient pas X-CSRF-Token → 403 systématique depuis A19 (derniers
appels mutants du front, balayage complet refait).
- Porte : e2e/regression_logout_dnd.spec.js (2 tests verts, joués sur
l'image rebuildée) · pytest 1094 passed · ruff OK · OpenAPI 7.45.1.
Cause racine : {% set right_actions = '…' ~ fd_icon(…) ~ '…' %} —
fd_icon est une macro → Markup, et Markup.__radd__/__add__ ÉCHAPPE ses
arguments str → tous les segments littéraux sortent entité-és ("/<),
et le |safe de _header:141 est no-op sur un Markup déjà échappé.
Régression probable depuis A10 (activation d'autoescape).
Fix (5 templates, forme idiomatique) : conversion en block-set
{% set right_actions %}…{{ fd_icon(…) }}…{% endset %} — source brute,
interpolation Markup brute : gitea_workspace, page_editor,
page_editor_collection, workspace, workspaces. (Piège script : regex
greedy multi-lignes avalait le set suivant → matcher sur UNE ligne.)
Tests :
- NOUVEAU tests/test_topbar_right_actions.py (permanent) : /workspaces
doit servir class="topbar-btn" parsé et ZÉRIE entité "
- gate éditeur CSP : assertion .star-btn RÉTABLIE (les boutons rendent)
- debug temporaires (DBGCLS/DBGVAL) retirés
suite **1094/1094** (+1 nouveau test) · ruff OK · E2E **7/7** (5
csp_preview + 2 smoke) · docs à jour (CHANGELOG détail, ROADMAP bug →
CORRIGÉ)
Ajout — e2e/smoke.spec.js (2 gates verts contre l'instance de test) :
- gate A39 : bascule de vues d'une collection (clic onglet Calendar →
?view_type=calendar, grille .calendar + .cal-header rendue ; collection
créée puis SUPPRIMÉE = répétable)
- gate A20 : palette Ctrl+K (ouverture Alpine .open, recherche GET rend
.cmd-palette-item, fermeture Échap)
- filet console : 0 erreur JS/CSP (bruit Failed to load resource 401/403
filtré)
- Service Workers bloqués : /sw.js sert sa page « hors ligne » sur les
navigations redirigées (redirect:'manual') — pwa_offline.spec.js couvre
le SW
- bootstrap autonome : login OU création du compte e2e documenté (jamais
de mot de passe deviné), workspace si absent
- commande : cd e2e && node node_modules/@playwright/test/cli.js test smoke.spec.js
Fixed — trouvés par les gates :
1. Bascule de vues standalone JAMAIS fonctionnelle : les onglets
émettaient ?view=… mais la route lit `view_type` (FastAPI) → l'onglet
restait sur Table quel que soit le clic (bug pré-existant, A28 n'y est
pour rien). Onglets → ?view_type= ; test_all_view_tabs_present adapté +
assertion comportementale (GET ?view_type=calendar rend .calendar).
2. Inter bloqué par la CSP depuis v7.27 : app.css importait encore
Google Fonts (@import raté par le grep de la passe v7.27) → violation
style-src sur chaque page + police en fallback. Inter auto-hébergé :
2 faces variables (100-900, latin + latin-ext) dans static/fonts/,
@import supprimé (8 fichiers dupliqués dédupliqués → 2).
suite **1093/1093** · ruff OK · E2E **2/2** · docs à jour
Template-created pages (weekly report, project doc, meeting notes, to-do
list, ...) were persisted without block ids. The editor assigned ids
client-side, but the realtime room loaded the raw id-less content and sent
it back on 'sync'; applySync then merged id-less server blocks with local
blocks, producing data-bid='undefined' collisions and duplicated/shuffled
lines as soon as the user edited. Editing an empty page was unaffected
because the server state was empty.
Fixes:
- board.use_page_template: materialize unique block ids (recursively) when
instantiating built-in or user templates.
- realtime_server: unique block_id() (uuid) + ensure_block_ids() on room
load and on insert ops.
- editor: recursive ensureBlockIds() in init; gtTok() now restores
[[fddate:...]] tokens (date chips survived as labels before).
- realtime client: applySync() normalizes ids, no longer appends unknown
local blocks (duplication), and keeps local content when server is empty.
Tests: pytest (templates + realtime) and Playwright e2e covering to-do
list, weekly report date chip, Enter ordering and legacy id-less repair.