diff --git a/CHANGELOG.md b/CHANGELOG.md index e85c57c..9aae79f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,41 @@ # Changelog - FlowDeck +## v7.69.4 (2026-10-08) — Le close de Settings ferme vraiment + +### Fixed + +- **Le bouton close de Settings devait être cliqué deux fois** — symptomatique + depuis le menu utilisateur ; depuis `/workspaces` ça marchait. +- Cause : `closeSettings()` faisait `history.back()` **puis** + `window.location.reload()` après un délai **fixe de 100 ms**. Le retour est + une navigation asynchrone : quand elle n'est pas terminée en 100 ms, le + `reload()` retombe sur `/settings` et le panneau « se ferme puis revient ». + La profondeur d'historique diffère selon le parcours, d'où le caractère + intermittant et le fait que `/workspaces` échappait au symptôme. +- → `location.replace()` vers la page d'origine : **une seule navigation + déterministe**, ni `history.back()`, ni `popstate`, ni rechargement différé. + Plus aucun chemin ne peut se terminer sans bouger. Effet secondaire + souhaitable : « Retour » ne rouvre plus Settings. +- La branche `window.opener` ne fait plus de `return` inconditionnel : + `window.close()` échoue en silence quand l'onglet n'a pas été ouvert par du + script, et l'ancien code abandonnait alors sans navigation de repli. + +### Tests + +- `e2e/v7694_settings_close.spec.js` — **contrôle négatif fait** : le même + spec a été exécuté contre deux builds. + - ancien `closeSettings()` → le parcours menu-utilisateur **échoue** + (`Received: "…/settings"`), `/workspaces` passe ; + - correctif → **les deux passent**. + Le test verrouille aussi que le panneau `position:fixed` n'est plus peint + (`.settings-overlay` absent), qui est le symptôme exact rapporté. + +### Correctifs de diagnostic + +- Le diagnostic posé en v7.69.3 (« Alpine ne lie pas le composant ») était + **faux** — un artefact de mon banc d'essai `target="_blank"`, pas le parcours + réel. Corrigé dans `ROADMAP.md`. + ## v7.69.3 (2026-10-08) — La section Aide est positionnée comme Settings ### Fixed @@ -21,11 +57,9 @@ surcharge inline et l'identité de balise avec Settings ; `e2e/v7693_help_positioning.spec.js` compare les géométries calculées. -> **Non traité dans ce lot** : le double-clic sur le bouton close de Settings. -> Cause racine identifiée mais correctif NON appliqué — Alpine ne lie pas le -> composant `x-data="settingsInit()"` dans certains contextes, donc -> `@click="closeSettings()"` est inerte au premier clic. Détail et prochaine -> étape dans `ROADMAP.md § v7.69.3`. +> **Corrigé en v7.69.4** : le double-clic sur le bouton close de Settings. +> Le diagnostic porté ici (« Alpine ne lie pas le composant ») était **faux**, +> voir `CHANGELOG.md § v7.69.4`. ## v7.69.2 (2026-10-08) — Le favicon est enfin le vrai logo diff --git a/ROADMAP.md b/ROADMAP.md index 1c1e3cb..09daf4e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1593,37 +1593,37 @@ près produisaient deux rendus. Surcharges retirées ; e2e compare les géométries calculées : **identiques des deux côtés** (overlay `fixed` avec fond, panneau 1050×765 centré, close à 13/13 px du coin). -**Non livré — double-clic sur le bouton close de Settings.** Cause racine -trouvée, correctif NON appliqué (non vérifié sur le parcours réel). +**Terminé en v7.69.4** — double-clic sur le bouton close de Settings. -Relevé d'investigation, à ne pas refaire : +Le diagnostic posé en v7.69.3 était **FAUX** : je pensais qu'Alpine ne liait +pas le composant (`_x_dataStack` absent), donc que le handler était inerte. +La question à l'utilisateur a tranché : *« après le premier clic la page est +réactive »* → Alpine lie bien le composant, le handler s'exécute. Le scénario +`target="_blank"` qui m'avait donné `_x_dataStack` absent était un artefact de +mon propre banc d'essai, pas le parcours réel. **Ne pas relire ce diagnostic.** -- **Cause racine** : Alpine ne LIE PAS le composant `x-data="settingsInit()"` - dans certains contextes. Mesure sur un onglet où Settings a été ouvert via - `target="_blank"` : - `window.Alpine` existe, mais `button.settings-close._x_dataStack` est absent - → `@click="closeSettings()"` est **inerte**. Le premier clic ne fait rien du - tout — ce qui correspond au symptôme rapporté. Ce n'est PAS la logique de - navigation qui est en cause. -- **Le bug se reproduit en e2e** : ouvrir `/settings` depuis un onglet distinct - (`target="_blank"` avec `rel="opener"`), cliquer `button.settings-close` → - l'URL reste `/settings`. Un test l'a confirmé (`APRES 1 clic= /settings`), - sans `pageerror` ni message console : le handler ne s'exécute simplement pas. -- **Ce qu'il ne faut PAS faire** : réécrire `closeSettings()`. L'ancienne - implémentation (`window.opener` → `window.close()`, sinon `history.back()` + - `reload()` après 100 ms) a été modifiée puis **annulée** : elle n'avait aucun - effet sur le cas reproduit. Le `return` inconditionnel de la branche - `window.opener` reste un défaut réel (`window.close()` échoue en silence si - l'onglet n'a pas été ouvert par du script), mais il n'est pas la cause - rapportée. -- **Thèse de la course réfutée** : un retour plus lent que 100 ms ne provoque - pas le symptôme (testé avec 400 ms de latence, un seul clic suffit). -- Parcours de référence qui, lui, **fonctionne en un clic** : `/local-workspace` - → menu avatar → Settings (chargement de page complet). +Cause réelle : `closeSettings()` faisait `history.back()` puis +`window.location.reload()` après un délai **fixe de 100 ms**. Entre les deux, +le retour est une navigation asynchrone ; quand elle n'est pas terminée en +100 ms, le `reload()` retombe sur `/settings` — le panneau « se ferme puis +revient ». Depuis `/workspaces`, la profondeur d'historique diffère et le +retour arrive à temps : d'où l'absence de symptôme sur ce seul parcours. -**Prochaine étape concrète** : déterminer pourquoi `settingsInit()` échoue à -s'initialiser — `settings.js` est-il chargé dans ce contexte ? Vérifier -`x-data` au chargement et la présence d'une erreur CSP/nonce sur le script. +Correctif : `location.replace()` vers la page d'origine. Une seule navigation +déterministe, ni `history.back()`, ni `popstate`, ni rechargement différé. +Effet secondaire assumé et souhaitable : « Retour » ne rouvre plus Settings. + +**Preuve avant/après (contrôle négatif fait)** : même spec, deux builds. +- ancien `closeSettings()` → le test du parcours menu-utilisateur **ÉCHOUE** + (`Received: "…/settings"`), celui de `/workspaces` passe ; +- correctif → **les deux passent**. + +**Piège relevé** : ma « réfutation » de la thèse de course était fausse. J'avais +interposé 400 ms via `page.route()`, qui intercepte les requêtes de sous-ressources +— pas la navigation du document lors d'un `history.back()`, servie depuis le +bfcache. Le délai n'a donc jamais été appliqué. Un contrôle négatif (rebuild +avec l'ancien code) aurait tranché en un tour ; il a fallu plusieurs tours pour +le faire. --- diff --git a/VERSION b/VERSION index c652fec..39e69d2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.69.3 +7.69.4 diff --git a/WORKLOAD.md b/WORKLOAD.md index b3994fd..bad5eb6 100644 --- a/WORKLOAD.md +++ b/WORKLOAD.md @@ -1,6 +1,6 @@ # WORKLOAD — FlowDeck Notion Clone -> **Début**: 2026-07-08 | **Version**: v7.69.3 (sélection multi-blocs fonctionnelle ; identité visuelle logo/bannière/favicon ; Aide alignée sur Settings — ⏳ double-clic close Settings non reproduit) +> **Début**: 2026-07-08 | **Version**: v7.69.4 (sélection multi-blocs fonctionnelle ; identité visuelle logo/bannière/favicon ; Aide alignée sur Settings ; close Settings réparé) > **Cible**: parité Notion + intégration forge · **Follow-ups v7.3 livrés**: sidebar teamspaces, notif `page.updated`, charts `number` + dashboards multi-DB, unfurl forge, UI Settings → Audit — voir `ROADMAP.md § v7.3.0` ## Avancement Global diff --git a/app/main.py b/app/main.py index f67de8b..a011390 100644 --- a/app/main.py +++ b/app/main.py @@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI): app = FastAPI( title="FlowDeck", - version="7.69.3", + version="7.69.4", docs_url="/docs", redoc_url="/redoc", lifespan=lifespan, diff --git a/docs/openapi-v2.json b/docs/openapi-v2.json index fda384f..5cfc0be 100644 --- a/docs/openapi-v2.json +++ b/docs/openapi-v2.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "FlowDeck", - "version": "7.69.2" + "version": "7.69.4" }, "paths": { "/auth/register": { diff --git a/e2e/v7694_settings_close.spec.js b/e2e/v7694_settings_close.spec.js new file mode 100644 index 0000000..2e45304 --- /dev/null +++ b/e2e/v7694_settings_close.spec.js @@ -0,0 +1,72 @@ +// v7.69.4 — le bouton close de Settings doit fermer le panneau, y compris +// quand Settings est ouvert depuis le MENU UTILISATEUR (navigation partielle). +// +// `shouldIntercept()` accepte `/settings`, donc ce clic passe par fdLoad() +// (htmx + history.pushState) SANS rechargement de page. L'ancien closeSettings() +// faisait `history.back()` → popstate → swap PARTIEL de .main-wrapper, et le +// panneau `position:fixed` restait peint : « ça ferme mais ça reste ouvert ». +// Depuis /workspaces, c'est un chargement complet → pas de symptôme. +const { test, expect } = require('@playwright/test'); +const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8081'; +const USER = process.env.FD_USER || 'e2e@flowdeck.local'; +const PASS = process.env.FD_PASS || 'e2e-secret-123'; + +async function login(page) { + await page.goto(`${FD_BASE}/auth/login?provider=local`, { waitUntil: 'domcontentloaded' }); + await page.fill('#email', USER); + await page.fill('#password', PASS); + await page.click('.btn-primary'); + await page.waitForURL('**/workspaces', { timeout: 8000 }).catch(() => {}); +} +const csrfOf = async (page) => { + const c = await page.context().cookies(); + return (c.find((x) => x.name === 'csrf_token') || {}).value || ''; +}; +async function makeWorkspace(page) { + const r = await page.request.post(`${FD_BASE}/api/workspaces`, { + headers: { 'X-CSRF-Token': await csrfOf(page) }, + data: { name: `e2e-cls-${Date.now()}-${Math.random().toString(16).slice(2, 6)}` }, + }); + const j = await r.json(); + return j.id || (j.workspace && j.workspace.id); +} + +// ouvre Settings depuis le menu utilisateur — le chemin qui provoque le bug +async function openSettingsFromUserMenu(page) { + await page.locator('.workspace-avatar, .user-avatar').first().click(); + await page.locator('.um-item[href="/settings"]').first().click(); + await page.waitForSelector('button.settings-close', { timeout: 8000 }); +} + +test('close ferme Settings ouvert depuis le menu utilisateur (nav partielle)', async ({ page }) => { + await login(page); + const id = await makeWorkspace(page); + await page.goto(`${FD_BASE}/local-workspace?ws=${id}`, { waitUntil: 'domcontentloaded' }); + + await openSettingsFromUserMenu(page); + expect(page.url()).toContain('/settings'); + + await page.locator('button.settings-close').click(); + await page.waitForTimeout(900); + + // plus sur /settings… + expect(page.url()).not.toContain('/settings'); + expect(page.url()).toContain('/local-workspace'); + // …et surtout : le panneau `position:fixed` ne doit PLUS être peint. C'est + // le symptôme exact rapporté (« ça ferme mais ça reste ouvert »). + expect(await page.locator('.settings-overlay').count()).toBe(0); + await expect(page.locator('button.settings-close')).toHaveCount(0); +}); + +test('close ferme Settings ouvert par chargement complet depuis /workspaces', async ({ page }) => { + await login(page); + await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'domcontentloaded' }); + await openSettingsFromUserMenu(page); + expect(page.url()).toContain('/settings'); + + await page.locator('button.settings-close').click(); + await page.waitForTimeout(900); + + expect(page.url()).not.toContain('/settings'); + expect(await page.locator('.settings-overlay').count()).toBe(0); +}); diff --git a/static/js/settings.js b/static/js/settings.js index 7bea536..6667515 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -1215,26 +1215,27 @@ function settingsInit() { finally { this.backupRunning = false; } }, closeSettings() { - if (window.opener && !window.opener.closed) { - window.opener.location.reload(); - window.close(); - return; - } - // If navigated directly to settings, redirect to workspaces (forces refresh) - if (!document.referrer || document.referrer === window.location.href) { - window.location.href = '/workspaces?ts=' + Date.now(); - return; - } - // Go back and force refresh + // v7.69.4 — « ça ferme mais ça reste ouvert » quand Settings est ouvert + // depuis le menu utilisateur : `shouldIntercept()` accepte `/settings`, + // donc ce clic passe par fdLoad() (htmx + history.pushState) SANS + // rechargement de page. L'ancien history.back() déclenchait alors un + // popstate → swap PARTIEL de .main-wrapper, et le panneau `position: + // fixed` restait peint au-dessus. Depuis /workspaces c'est un + // chargement complet, d'où l'absence de symptôme. + // → navigation explicite et déterministe : une seule requête, tout le + // document est détruit, aucun chemin ne peut se terminer sans bouger. var prev = document.referrer; - if (prev && prev.includes('/settings')) { - // Came from another settings tab — go back two steps then refresh - history.go(-2); - setTimeout(function() { window.location.reload(); }, 100); - } else { - history.back(); - setTimeout(function() { window.location.reload(); }, 100); + if (window.opener && !window.opener.closed) { + // fermeture au mieux — on ne peut PAS vérifier qu'elle a réussi, + // donc on enchaîne toujours sur la navigation ci-dessous. + try { window.opener.location.reload(); } catch (e) { /* opener cross-origin */ } + try { window.close(); } catch (e) { /* ignoré */ } } + var back = '/workspaces'; + if (prev && prev.indexOf(location.origin) === 0 && prev.indexOf('/settings') < 0) { + back = prev.split('#')[0]; + } + location.replace(back + (back.indexOf('?') >= 0 ? '&' : '?') + 'ts=' + Date.now()); }, }; }