Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa88cf6665 | ||
|
|
b2ea8ec537 |
@@ -1,5 +1,41 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.45.4 (2026-10-03) — Anti-FOUC au chargement complet + morts nettoyés
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Flashs au chargement COMPLET** (F5, première visite, navigation servie par
|
||||
le service worker) : le masquage `x-cloak`/`opacity` de v7.45.3 ne couvrait
|
||||
que les swaps partiels fdLoad — un chargement de document peignait la page
|
||||
tant qu'Alpine n'avait pas initialisé `.app-layout`, donc sidebar brute (toutes
|
||||
sections ouvertes, menu utilisateur visible) **et** contenu de la zone dont la
|
||||
bande rouge « You are offline. Changes will sync when connection is
|
||||
restored. » (aucun `x-cloak` sur elle). Fix : `x-cloak` sur le nœud racine
|
||||
Alpine `.app-layout` (base.html) — tout l'app (sidebar + zone + bandeau) reste
|
||||
masqué tant que `appState()` n'est pas monté, puis Alpine retire l'attribut.
|
||||
Vérifié par le probe (3 pages × plein chargement) : 0 frame de contenu brut,
|
||||
`x-cloak` retiré et `display:flex` à la fin (donc pas de page blanche).
|
||||
|
||||
### Changed
|
||||
|
||||
- **`fd-navigating` supprimée** : classe posée/retirée à chaque `fdLoad` mais
|
||||
sans aucune règle CSS dans le dépôt (grep css/js/html → seulement app.js) —
|
||||
code mort.
|
||||
- **`e2e/probe_nav_perf.spec.js` étendu à 4 scénarios avec assertions** :
|
||||
navigations partielles (5 pages), **clic réel sur le bouton Home**, Home avec
|
||||
réseau coupé (SW actif), et plein chargement. Asserte : 0 frame de contenu
|
||||
brut peint, zone révélée (`opacity:1`) et montée (`0 [x-show]` non montés,
|
||||
`x-ignore` absent), `.app-layout` sans `x-cloak` et visible. Capture
|
||||
screenshot automatique si du contenu brut est peint + collecte console.
|
||||
|
||||
### Not reproduced
|
||||
|
||||
- Le symptôme rapporté (Home → rafale de menus/fenêtres + bande rouge) n'est
|
||||
**pas rejouable** dans un navigateur frais : clic Home en ligne comme hors
|
||||
ligne, nav partielle et plein chargement donnent 0 frame de contenu brut et 0
|
||||
erreur console. Reste à confirmer côté navigateur signalé (recharge dure
|
||||
après deploy, URL/instance testée).
|
||||
|
||||
## v7.45.3 (2026-10-03) — Navigation partielle : le contenu brut n'est plus peint
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -1005,6 +1005,8 @@ Détails livrés :
|
||||
- [x] **Fix SW + CSRF uploads** (v7.45.1) — logout ne sert plus la page « hors ligne » (le fetch event de navigation est en `redirect:'manual'` → 302 lue `opaqueredirect` → rejet ; rejoué en `redirect:'follow'` + **302 synthétique vers l'URL finale**, Chromium refusant les responses `redirected` servies à une navigation) et drag & drop de fichiers/dossiers réparé (`_doUpload` + `toggleFavorite` sans `X-CSRF-Token` = oublis A19, derniers appels mutants du front). `timeoutFetch` annule enfin ses requêtes. Gate `e2e/regression_logout_dnd.spec.js` (2 tests verts).
|
||||
- [x] **Navigation partielle Alpine** (v7.45.2) — les swaps fdLoad initialisaient `.main-wrapper` avant l'exécution des scripts de page (courses : « Undefined variable » sur l'éditeur, `reading 'has'` sur `$store.fdCtx`, `SyntaxError: 'LW' already declared` à la 2ᵉ visite). Fix maison dans `app.js` : `x-ignore` sur la zone swapée (nœud remplacé uniquement) → démontage unique à tous `<script src>` chargés via `Alpine.initTree` idempotent ; garde de fichier sur 5 scripts à `const` de haut niveau ; `if (window.Alpine)` sur 8 scripts + 7 templates inline ; polices Inter orphelines purgées d'`app.css`. Gates : `regression_editor_mount` + `regression_partial_nav` (7 pages × complet/partiel/2ᵉ = 0 erreur).
|
||||
- [x] **Flashs de navigation** (v7.45.3) — pendant la fenêtre `x-ignore` (swap fdLoad → `Alpine.initTree`), la zone `.main-wrapper` était peinte **non montée** : tous ses `[x-show]` visibles à leur valeur brute (menus/panneaux/états loading+empty+table superposés) puis masqués d'un coup au montage = la rafale de fenêtres qui flashent à chaque changement de page/section. Fix 6 lignes dans `app.js` : `opacity:0` + `pointer-events:none` posés dans `htmx:afterSwap` (même task que le swap → aucun paint intermédiaire), retirés **après** `initTree` sur les 3 chemins de démontage. Mesure avant : 19 éléments bruts/87 ms sur /library (CLS 0.149), 80 éléments/732 ms sur /settings — après : 0 frame de contenu brut. Gate maison `e2e/probe_nav_perf.spec.js` (marques htmx + fenêtre x-ignore + échantillon par frame + layout-shift).
|
||||
- Suivis restants (mesurés, non traités) : CLS résiduel **0.058** sur /library (états `lib-loading`/`lib-empty` → `lib-table` : le remplacement du spinner par les lignes décale le contenu — nécessiterait un squelette de lignes, pas un réglage CSS) · fenêtre `x-ignore` de **~150–300 ms** sur /settings (coût des scripts de page avant `initTree`, pas de gain sans changer l'ordre de montage) · la **topbar est dans `.main-wrapper`** (base.html) → re-swappée et re-montée à chaque navigation (avec le masquage elle disparaît/revient au lieu de flasher — la sortir demanderait de la swapper séparément).
|
||||
- Suite v7.45.4 : `x-cloak` posé sur `.app-layout` (racine Alpine) → les **chargements complets** (F5, SW, hors ligne) ne peignent plus la sidebar brute ni la bande rouge hors ligne — le masquage fdLoad ne couvrait que les swaps partiels. `fd-navigating` (classe sans règle CSS) supprimée. Probe étendu à 4 scénarios (nav / clic Home réel / Home hors ligne / plein chargement) avec assertions. **Symptôme Home non rejouable** en navigateur frais (0 frame brut, 0 erreur console) → à confirmer sur le navigateur signalé.
|
||||
|
||||
### v7.1.0 — Calendar sync + Meeting Notes ✅ (2026-09-28)
|
||||
> **Objectif** : calendrier bidirectionnel + transcription → agents (cf. Notion 07/2026 : Meeting Notes trigger Custom Agents).
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# WORKLOAD — FlowDeck Notion Clone
|
||||
|
||||
> **Début**: 2026-07-08 | **Version**: v7.45.3 (flashs de navigation = la zone `x-ignore` n'est plus peint non montée : `opacity:0` au swap, reveal après `initTree` — 0 frame de contenu brut au lieu de 19/87 ms puis 80/732 ms) | **Statut**: EN COURS 🔄
|
||||
> **Début**: 2026-07-08 | **Version**: v7.45.4 (anti-FOUC : `x-cloak` sur `.app-layout` = plus de sidebar brute ni de bande rouge hors ligne au chargement complet, classe morte `fd-navigating` supprimée, probe étendu à 4 scénarios avec assertions) | **Statut**: EN COURS 🔄
|
||||
> **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
|
||||
|
||||
+1
-1
@@ -185,7 +185,7 @@ async def lifespan(_app: FastAPI):
|
||||
|
||||
app = FastAPI(
|
||||
title="FlowDeck",
|
||||
version="7.45.3",
|
||||
version="7.45.4",
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
lifespan=lifespan,
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
<script src="/static/js/sortable.min.js" defer data-cfasync="false"></script>
|
||||
</head>
|
||||
<body hx-headers='{"X-CSRF-Token":{{ csrf_token() | tojson }}}'{% if embed_mode %} class="embed-mode"{% endif %}>
|
||||
<div class="app-layout" x-data="appState()">
|
||||
<div class="app-layout" x-data="appState()" x-cloak>
|
||||
|
||||
<!-- ═══════════ MOBILE SIDEBAR OVERLAY ═══════════ -->
|
||||
<div class="sidebar-overlay"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"openapi": "3.1.0",
|
||||
"info": {
|
||||
"title": "FlowDeck",
|
||||
"version": "7.45.3"
|
||||
"version": "7.45.4"
|
||||
},
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
|
||||
+180
-4
@@ -89,8 +89,10 @@ test('probe fluidité nav', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
await login(page);
|
||||
|
||||
const NAVS = ['/library', '/settings', '/workspaces', '/trash'];
|
||||
const NAVS = ['/library', '/settings', '/workspaces', '/trash', '/local-workspace'];
|
||||
const results = [];
|
||||
const SHOTS = require('path').join(__dirname, 'shots');
|
||||
require('fs').mkdirSync(SHOTS, { recursive: true });
|
||||
|
||||
for (const dest of NAVS) {
|
||||
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'load' });
|
||||
@@ -103,17 +105,34 @@ test('probe fluidité nav', async ({ page }) => {
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
}, dest);
|
||||
await page.waitForTimeout(3200);
|
||||
// Filet : si du contenu brut est peint, on capture pour voir CE qui flash
|
||||
let shot = null;
|
||||
const tEnd = Date.now() + 3200;
|
||||
while (Date.now() < tEnd && !shot) {
|
||||
const last = await page.evaluate(() => {
|
||||
const f = window.__probe.frames;
|
||||
return f.length ? f[f.length - 1] : null;
|
||||
});
|
||||
if (last && last[2] > 0) {
|
||||
shot = `navflash_${dest.replace(/\W+/g, '')}.png`;
|
||||
await page.screenshot({ path: require('path').join(SHOTS, shot) });
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(25);
|
||||
}
|
||||
const P = await page.evaluate(() => window.__probe);
|
||||
// État final : la zone doit être révélée (opacity>0) ET montée Alpine
|
||||
// État final : la zone doit être révélée (opacity>0) ET montée Alpine,
|
||||
// et .app-layout (x-cloak anti-FOUC) doit être retiré → pas de page blanche
|
||||
const final = await page.evaluate(() => {
|
||||
const z = document.querySelector('.main-wrapper');
|
||||
const a = document.querySelector('.app-layout');
|
||||
if (!z) return { opacity: 'NO-ZONE', unmounted: -1, xIgnore: null };
|
||||
return {
|
||||
opacity: getComputedStyle(z).opacity,
|
||||
pointerEvents: getComputedStyle(z).pointerEvents,
|
||||
unmounted: Array.from(z.querySelectorAll('[x-show]')).filter((e) => !e._x_marker).length,
|
||||
xIgnore: z.hasAttribute('x-ignore'),
|
||||
layout: a ? { cloak: a.hasAttribute('x-cloak'), display: getComputedStyle(a).display } : 'ABSENT',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -138,9 +157,166 @@ test('probe fluidité nav', async ({ page }) => {
|
||||
|
||||
// GATE : aucun contenu brut peint + zone révélée et montée à la fin
|
||||
const paintedRaw = results.filter((r) => r.rawVisibleFrames > 0);
|
||||
const notRevealed = results.filter((r) => parseFloat(r.final.opacity) !== 1 || r.final.unmounted !== 0 || r.final.xIgnore);
|
||||
const notRevealed = results.filter((r) => parseFloat(r.final.opacity) !== 1 || r.final.unmounted !== 0 || r.final.xIgnore
|
||||
|| !r.final.layout || r.final.layout.cloak === true || r.final.layout.display === 'none');
|
||||
console.log('SOMMAIRE ' + JSON.stringify(results.map((r) => ({ d: r.dest, raw: r.rawVisibleFrames, hidden: r.hiddenFrames, op: r.final.opacity, un: r.final.unmounted, shift: r.shifts.length }))));
|
||||
if (paintedRaw.length) throw new Error('contenu brut peint: ' + paintedRaw.map((r) => `${r.dest} (${r.rawVisibleFrames} frames, max ${r.rawMaxElements})`).join(' ; '));
|
||||
if (notRevealed.length) throw new Error('zone non révélée/montée: ' + JSON.stringify(notRevealed.map((r) => [r.dest, r.final])));
|
||||
console.log('GATE OK — 0 frame de contenu brut, zone révélée et montée sur ' + results.length + ' navigations');
|
||||
});
|
||||
|
||||
// Le geste réel du rapport : clic sur le bouton Home de la sidebar (et non un
|
||||
// ancre synthétique) — on mesure ce qui peint et on garde une capture si flash.
|
||||
test('probe bouton Home', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
const SHOTS = require('path').join(__dirname, 'shots');
|
||||
require('fs').mkdirSync(SHOTS, { recursive: true });
|
||||
await login(page);
|
||||
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'load' });
|
||||
await page.waitForTimeout(1200);
|
||||
await page.evaluate(INSTALL);
|
||||
|
||||
const console_ = [];
|
||||
page.on('console', (m) => { if (m.type() !== 'log') console_.push(m.type() + ' | ' + m.text().slice(0, 180).replace(/\s+/g, ' ')); });
|
||||
page.on('pageerror', (e) => console_.push('PAGEERROR | ' + String(e).slice(0, 180)));
|
||||
|
||||
const before = await page.evaluate(() => ({ onLine: navigator.onLine, home: !!document.querySelector('.home-btn'), href: (document.querySelector('.home-btn') || {}).href }));
|
||||
console.log('HOME avant clic ' + JSON.stringify(before));
|
||||
|
||||
await page.click('.home-btn');
|
||||
let shot = null;
|
||||
const tEnd = Date.now() + 4000;
|
||||
while (Date.now() < tEnd && !shot) {
|
||||
const last = await page.evaluate(() => {
|
||||
const f = window.__probe.frames;
|
||||
return f.length ? f[f.length - 1] : null;
|
||||
});
|
||||
if (last && last[2] > 0) {
|
||||
shot = 'home_flash.png';
|
||||
await page.screenshot({ path: require('path').join(SHOTS, shot) });
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(25);
|
||||
}
|
||||
await page.waitForTimeout(2000);
|
||||
const P = await page.evaluate(() => window.__probe);
|
||||
const rawFrames = P.frames.filter((f) => f[2] > 0);
|
||||
const after = await page.evaluate(() => {
|
||||
const b = document.querySelector('.offline-banner');
|
||||
const z = document.querySelector('.main-wrapper');
|
||||
return {
|
||||
url: location.pathname + location.search,
|
||||
onLine: navigator.onLine,
|
||||
banner: b ? { cloak: b.hasAttribute('x-cloak'), display: getComputedStyle(b).display, mounted: !!b._x_marker } : 'absente',
|
||||
zone: z ? { op: getComputedStyle(z).opacity, un: Array.from(z.querySelectorAll('[x-show]')).filter((e) => !e._x_marker).length, xIgnore: z.hasAttribute('x-ignore') } : null,
|
||||
mounts: typeof window.__fdLocalWorkspaceScriptsLoaded !== 'undefined' ? window.__fdLocalWorkspaceScriptsLoaded : 'n/a',
|
||||
};
|
||||
});
|
||||
const res = { marks: Object.fromEntries(P.marks), rawVisibleFrames: rawFrames.length, rawFirst: rawFrames[0] || null, rawMax: P.rawMax, shot, shifts: P.shifts, console: console_, after };
|
||||
console.log('HOME résultat => ' + JSON.stringify(res));
|
||||
if (res.rawVisibleFrames > 0) throw new Error(`contenu brut peint au clic Home: ${res.rawVisibleFrames} frames (max ${res.rawMax})`);
|
||||
console.log('GATE HOME OK — 0 frame de contenu brut');
|
||||
});
|
||||
|
||||
// Scénario du rapport : la bande rouge dit « hors ligne » → on rejoue le clic
|
||||
// Home AVEC le réseau coupé (SW actif) pour voir ce qui peint.
|
||||
test('probe bouton Home hors ligne', async ({ page, context }) => {
|
||||
test.setTimeout(120000);
|
||||
const SHOTS = require('path').join(__dirname, 'shots');
|
||||
require('fs').mkdirSync(SHOTS, { recursive: true });
|
||||
await login(page);
|
||||
await page.addInitScript(INSTALL);
|
||||
await page.goto(`${FD_BASE}/workspaces`, { waitUntil: 'load' });
|
||||
await page.waitForTimeout(1500); // laisse le SW s'installer et cacher les pages
|
||||
await page.evaluate(() => { window.__probe = null; window.__probe = undefined; });
|
||||
await page.evaluate(INSTALL);
|
||||
|
||||
await context.setOffline(true);
|
||||
await page.waitForTimeout(200);
|
||||
const onLine = await page.evaluate(() => navigator.onLine);
|
||||
|
||||
await page.click('.home-btn');
|
||||
const shots = [];
|
||||
for (const [delay, name] of [[700, 't700'], [2500, 't2500']]) {
|
||||
await page.waitForTimeout(delay === 700 ? 700 : 1800);
|
||||
const p = `home_offline_${name}.png`;
|
||||
await page.screenshot({ path: require('path').join(SHOTS, p) }).catch(() => {});
|
||||
shots.push(p);
|
||||
}
|
||||
const P = await page.evaluate(() => window.__probe || null).catch(() => null);
|
||||
const after = await page.evaluate(() => {
|
||||
const b = document.querySelector('.offline-banner');
|
||||
const z = document.querySelector('.main-wrapper');
|
||||
const visible = (sel) => Array.from(document.querySelectorAll(sel)).filter((e) => e.getClientRects().length && getComputedStyle(e).display !== 'none').map((e) => (e.className || '').toString().split(' ')[0]).slice(0, 12);
|
||||
return {
|
||||
url: location.pathname + location.search,
|
||||
onLine: navigator.onLine,
|
||||
banner: b ? { display: getComputedStyle(b).display, mounted: !!b._x_marker } : 'absente',
|
||||
zone: z ? { op: getComputedStyle(z).opacity, un: Array.from(z.querySelectorAll('[x-show]')).filter((e) => !e._x_marker).length, xIgnore: z.hasAttribute('x-ignore') } : null,
|
||||
overlays: visible('.modal-overlay, .ctx-menu, .context-menu, .user-menu-dropdown, .dropdown-menu, .popover'),
|
||||
};
|
||||
}).catch((e) => ({ err: String(e) }));
|
||||
const rawFrames = P ? P.frames.filter((f) => f[2] > 0) : [];
|
||||
const res = { onLineBefore: onLine, marks: P ? Object.fromEntries(P.marks) : null, rawVisibleFrames: rawFrames.length, rawFirst: rawFrames[0] || null, rawMax: P ? P.rawMax : null, shots, shifts: P ? P.shifts : null, after };
|
||||
console.log('HOME OFFLINE => ' + JSON.stringify(res));
|
||||
});
|
||||
|
||||
// Charge COMPLÈTE (F5 / premier visit / SW) : le masquage fdLoad ne s'applique
|
||||
// pas là, seul x-cloak protège — on mesure ce qui peint avant Alpine.start().
|
||||
test('probe fluidité plein chargement', async ({ page }) => {
|
||||
test.setTimeout(120000);
|
||||
const SHOTS = require('path').join(__dirname, 'shots');
|
||||
require('fs').mkdirSync(SHOTS, { recursive: true });
|
||||
await login(page);
|
||||
await page.addInitScript(INSTALL);
|
||||
|
||||
const OUT = [];
|
||||
for (const dest of ['/local-workspace', '/library', '/workspaces']) {
|
||||
await page.goto(FD_BASE + dest, { waitUntil: 'commit' });
|
||||
let shot = null;
|
||||
const tEnd = Date.now() + 4000;
|
||||
while (Date.now() < tEnd && !shot) {
|
||||
const last = await page.evaluate(() => {
|
||||
const f = window.__probe && window.__probe.frames;
|
||||
return f && f.length ? f[f.length - 1] : null;
|
||||
}).catch(() => null);
|
||||
if (last && last[2] > 0) {
|
||||
shot = `fullflash_${dest.replace(/\W+/g, '')}.png`;
|
||||
await page.screenshot({ path: require('path').join(SHOTS, shot) });
|
||||
break;
|
||||
}
|
||||
await page.waitForTimeout(25);
|
||||
}
|
||||
await page.waitForTimeout(1500);
|
||||
const P = await page.evaluate(() => window.__probe);
|
||||
const rawFrames = P.frames.filter((f) => f[2] > 0);
|
||||
const res = {
|
||||
dest,
|
||||
rawVisibleFrames: rawFrames.length,
|
||||
rawFirst: rawFrames[0] || null,
|
||||
rawLast: rawFrames[rawFrames.length - 1] || null,
|
||||
rawMax: P.rawMax,
|
||||
shot,
|
||||
shifts: P.shifts,
|
||||
final: await page.evaluate(() => {
|
||||
const z = document.querySelector('.main-wrapper');
|
||||
const a = document.querySelector('.app-layout');
|
||||
return z ? {
|
||||
op: getComputedStyle(z).opacity,
|
||||
un: Array.from(z.querySelectorAll('[x-show]')).filter((e) => !e._x_marker).length,
|
||||
layout: a ? { cloak: a.hasAttribute('x-cloak'), display: getComputedStyle(a).display } : 'ABSENT',
|
||||
} : null;
|
||||
}),
|
||||
};
|
||||
OUT.push(res);
|
||||
console.log('FULL ' + dest + ' => ' + JSON.stringify(res));
|
||||
}
|
||||
console.log('SOMMAIRE_FULL ' + JSON.stringify(OUT.map((r) => [r.dest, r.rawVisibleFrames, r.rawMax, r.shot])));
|
||||
const notRevealed = OUT.filter((r) => !r.final || parseFloat(r.final.op) !== 1 || r.final.un !== 0
|
||||
|| !r.final.layout || r.final.layout.cloak === true || r.final.layout.display === 'none');
|
||||
if (notRevealed.length) throw new Error('page blanche/non montée au chargement: ' + JSON.stringify(notRevealed.map((r) => [r.dest, r.final])));
|
||||
if (OUT.some((r) => r.rawVisibleFrames > 0)) {
|
||||
throw new Error('contenu brut peint au plein chargement: ' + OUT.filter((r) => r.rawVisibleFrames > 0).map((r) => `${r.dest} (${r.rawVisibleFrames} frames, max ${r.rawMax})`).join(' ; '));
|
||||
}
|
||||
console.log('GATE FULL OK — 0 frame de contenu brut au chargement complet');
|
||||
});
|
||||
|
||||
@@ -283,10 +283,8 @@
|
||||
if (!window.htmx) { window.location.href = url; return; }
|
||||
if (loading) return;
|
||||
loading = true;
|
||||
document.documentElement.classList.add('fd-navigating');
|
||||
function done() {
|
||||
loading = false;
|
||||
document.documentElement.classList.remove('fd-navigating');
|
||||
}
|
||||
var p;
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user