Compare commits

...
4 Commits
Author SHA1 Message Date
bruno 7468e497c6 fix: le bouton close de Settings ferme en un clic (v7.69.4)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 15m40s
FlowDeck CI / docker (push) Skipped
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 intermittent et le fait que /workspaces
échappait au symptôme.

Correctif : location.replace() vers la page d'origine. Une seule navigation
déterministe — ni history.back(), ni popstate, ni rechargement différé. Aucun
chemin ne peut plus se terminer sans navigation. Effet secondaire assumé et
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 — le bouton semblait mort.

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é.

Correction de diagnostic : le diagnostic posé en v7.69.3 (« Alpine ne lie pas
le composant x-data="settingsInit()" ») était FAUX — un artefact de mon banc
d'essai target="_blank", pas le parcours réel. La page étant réactive après le
premier clic, le handler s'exécutait bien. ROADMAP.md et CHANGELOG.md § v7.69.3
corrigés pour ne pas laisser une piste fausse en héritage.

pytest 1421 passed / 0 failed · ruff OK · OpenAPI 526 chemins / 7.69.4
e2e 18/18 verts sur l'instance déployée
2026-10-08 22:02:08 -04:00
bruno 162eb257c7 fix: la section Aide est positionnée comme Settings (v7.69.3)
FlowDeck CI / lint (push) Successful in 1m32s
FlowDeck CI / test (push) Failing after 16m17s
FlowDeck CI / docker (push) Skipped
La section Aide et la section Settings partagent le même gabarit
(`.settings-overlay` / `.settings-panel`) et la même feuille de style
(`static/css/settings.css`), mais affichaient deux rendus différents.

Cause : `help.html` neutralisait son overlay via un style INLINE sur la balise
— `position:static`, `background:none`, `backdrop-filter:none`, `padding:0` —
et imposait par-dessus un panneau `width:100%;max-width:1050px;
height:calc(100vh - 120px);margin:0 auto`. Settings, lui, laissait
`settings.css` faire son travail : overlay `fixed;inset:0` avec fond flouté,
panneau `85vh` centré par `margin:auto`.

Résultat perçu : Aide s'affichait comme un bloc posé dans la page, Settings
comme une modale centrée floutant l'arrière-plan — malgré un code quasi
identique.

Correctif : les surcharges inline sont retirées de `help.html`. La géométrie
vient désormais entièrement de `settings.css`, commun aux deux pages. Aucune
valeur n'est dupliquée ; les deux balises sont strictement identiques.

Vérifié en e2e en comparant les géométries CALCULÉES des deux pages (pas des
valeurs de code) : overlay `fixed` avec fond opère des deux côtés, panneau
1050x765 centré à top:68px, bouton close à 13/13px du coin du panneau — les
deux jeux de mesures sont identiques.

Tests : `tests/test_v7693_help_positioning.py` (5) verrouille l'absence de
surcharges inline et l'identité de balise avec Settings ;
`e2e/v7693_help_positioning.spec.js` compare les géométries calculées.

NON INCLUS — double-clic sur le bouton close de Settings :
le bug a été REPRODUIT en e2e et sa cause racine identifiée, mais le correctif
n'est volontairement PAS dans ce commit. Voir ROADMAP.md § v7.69.3 pour le
relevé complet et la prochaine étape.

pytest 1421 passed / 0 failed · ruff OK · e2e 15/15 verts sur l'instance déployée
2026-10-08 21:07:17 -04:00
bruno b9a776b626 fix: le favicon est enfin le vrai logo, plus un placeholder (v7.69.2)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 3h6m20s
FlowDeck CI / docker (push) Skipped
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
2026-10-08 19:41:37 -04:00
bruno 674baf6954 fix: og:image et twitter:image en URL absolue (v7.69.1)
FlowDeck CI / lint (push) Successful in 1m34s
FlowDeck CI / test (push) Failing after 3h12m7s
FlowDeck CI / docker (push) Skipped
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
2026-10-08 19:00:13 -04:00
21 changed files with 415 additions and 39 deletions
+98
View File
@@ -1,5 +1,103 @@
# 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
- **Aide s'affichait autrement que Settings** alors qu'ils partagent le même
gabarit (`settings-overlay` / `settings-panel`) et le même CSS. `help.html`
neutralisait son overlay **en style inline** : `position:static`,
`background:none`, `backdrop-filter:none`, `padding:0`, et forçait un panneau
`100% / max-width:1050px / height:calc(100vh - 120px)`. Résultat : Aide
était un bloc posé dans la page, Settings une modale centrée floutant le fond.
- → les surcharges inline sont retirées ; la géométrie vient entièrement de
`settings.css`, commun aux deux pages. Vérifié en e2e : **mêmes valeurs
mesurées** des deux côtés (overlay `fixed` avec fond, panneau 1050×765
centré à `top:68px`, bouton close à 13/13 px du coin du panneau).
### Tests
- `tests/test_v7693_help_positioning.py` (5) verrouillent l'absence de
surcharge inline et l'identité de balise avec Settings ;
`e2e/v7693_help_positioning.spec.js` compare les géométries calculées.
> **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
### Fixed
- **Le favicon était un placeholder** : un carré bleu `#2383E2` avec les lettres
« FD » écrites en `<text>` SVG — génériques, sans lien avec la marque, et
rendus avec la police du visiteur.
- → `static/favicon.ico` généré depuis `assets/logo.jpg` : 16/32/48 px dans un
seul fichier (un `<link>` pour tous les navigateurs), 4,1 Ko.
**Pourquoi un fond sombre** : le logo est blanc à ~82 % (mesuré : 50,7 % de
blanc pur + 32 % de blancs proches). Posé nu, il disposerait sur un onglet clair
— cas nominal de Firefox et Safari en thème clair. 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 une identité cohérente. Contraste mesuré à 16 px : σ = 106 (contre
σ = 49 pour le logo seul — le fond sombre double la lisibilité).
Mise à jour des **5 templates** qui référençaient 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.
### Tests
- `test_favicon_is_the_real_logo` (tailles ICO, absence du placeholder) +
`test_every_template_points_at_the_real_favicon` (5 templates + `sw.js`).
## v7.69.1 (2026-10-08) — `og:image` en URL absolue
### Fixed
- **`og:image` / `twitter:image` étaient des URL relatives** — les crawlers
(LinkedIn, Slack, Discord, iMessage…) ignorent une URL relative : la carte de
partage était vide. → global Jinja `app_base_url()` construit à partir de
`settings.app_base_url`, **la même source que les liens de partage de pages**
(`collaboration.py`) : si elle est mal renseignée, ces liens l'étaient déjà.
Lecture paresseuse, comme `_plugin_enabled`, pour éviter l'import circulaire.
## v7.69.0 (2026-10-08) — La sélection multi-blocs fonctionne enfin (+ identité visuelle)
### Fixed
+45
View File
@@ -1581,6 +1581,51 @@ le marqueur de troncature du message d'erreur, **pas** un bug produit — une he
passée à chercher une fuite inexistante. Vérifier `JSON.stringify()` de la valeur
réelle avant de croire un affichage tronqué.
---
### v7.69.3 — Aide alignée sur Settings ✅ · double-clic close ⏳ ouvert (2026-10-08)
**Livré** : la section Aide partage maintenant le positionnement exact de la
section Settings. `help.html` neutralisait son overlay en style inline
(`position:static`, fond transparent, sans blur, sans padding) et forçait un
panneau `1050px / 100vh-120px` — les deux pages pourtant identiques au CSS
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).
**Terminé en v7.69.4** — double-clic sur le bouton close de Settings.
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 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.
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.
---
## ✅ Fonctionnalités livrées hors roadmap (bonus détectés dans le code)
+1 -1
View File
@@ -1 +1 @@
7.69.0
7.69.4
+1 -1
View File
@@ -1,6 +1,6 @@
# WORKLOAD — FlowDeck Notion Clone
> **Début**: 2026-07-08 | **Version**: v7.69.0 (sélection multi-blocs fonctionnelle : drag souris + Maj+↓, clic droit qui la préserve) | **Statut**: EN COURS 🔄
> **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
+1 -1
View File
@@ -186,7 +186,7 @@ async def lifespan(_app: FastAPI):
app = FastAPI(
title="FlowDeck",
version="7.69.0",
version="7.69.4",
docs_url="/docs",
redoc_url="/redoc",
lifespan=lifespan,
+3 -3
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlowDeck — {% block title_prefix %}Home{% endblock %}</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="/static/favicon.ico" sizes="48x48">
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#191919">
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
@@ -14,11 +14,11 @@
<meta property="og:site_name" content="FlowDeck">
<meta property="og:title" content="FlowDeck — votre espace Notion auto-hébergé">
<meta property="og:description" content="Éditeur de blocs, Kanban, bases de données et intégration Gitea/GitHub, sur votre propre serveur.">
<meta property="og:image" content="/static/img/logo-512.jpg">
<meta property="og:image" content="{{ app_base_url() }}/static/img/logo-512.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="FlowDeck — votre espace Notion auto-hébergé">
<meta name="twitter:description" content="Éditeur de blocs, Kanban, bases de données et intégration Gitea/GitHub.">
<meta name="twitter:image" content="/static/img/logo-512.jpg">
<meta name="twitter:image" content="{{ app_base_url() }}/static/img/logo-512.jpg">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<link rel="stylesheet" href="/static/css/app.css?v={{ asset_version }}">
+5 -2
View File
@@ -30,8 +30,11 @@
.help-badge.sso{background:rgba(217,115,13,.18);color:#E0952B;}
</style>
<div class="settings-overlay" x-data="helpInit()" style="background:none;backdrop-filter:none;position:static;z-index:1;padding:0;">
<div class="settings-panel" style="width:100%;max-width:1050px;height:calc(100vh - 120px);margin:0 auto;" @keydown.escape="navOpen = false">
<!-- v7.69.3 : positionnement IDENTIQUE à la section Settings — les surcharges
inline (position:static / background:none / panneau 1050px) faisaient
d'Aide un bloc posé dans la page au lieu de l'overlay centré de Settings. -->
<div class="settings-overlay" x-data="helpInit()">
<div class="settings-panel" @keydown.escape="navOpen = false">
<button class="settings-menu-btn" @click="navOpen = !navOpen" title="Sections" aria-label="Open sections menu">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Importer — FlowDeck</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="/static/favicon.ico" sizes="48x48">
<script src="/static/js/alpine.csp.min.js" defer data-cfasync="false"></script>
<style>
:root{--bg:#191919;--bg2:#1F1F1F;--bg3:#2D2D2D;--border:#333;--text:#fff;--dim:#9B9A97;--accent:#2383E2;--accent-h:#1a6bc0;--success:#0F7B6C;--danger:#E03E3E;--warn:#D9730D;}
+3 -3
View File
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
{% from '_icons.html' import fd_icon %}
<title>FlowDeck — All-in-one workspace</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="/static/favicon.ico" sizes="48x48">
<link rel="manifest" href="/static/manifest.json">
<meta name="theme-color" content="#191919">
<link rel="apple-touch-icon" href="/static/icons/apple-touch-icon.png">
@@ -17,11 +17,11 @@
<meta property="og:site_name" content="FlowDeck">
<meta property="og:title" content="FlowDeck — votre espace Notion auto-hébergé">
<meta property="og:description" content="Éditeur de blocs, Kanban, bases de données et intégration Gitea/GitHub, sur votre propre serveur.">
<meta property="og:image" content="/static/img/logo-512.jpg">
<meta property="og:image" content="{{ app_base_url() }}/static/img/logo-512.jpg">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="FlowDeck — votre espace Notion auto-hébergé">
<meta name="twitter:description" content="Éditeur de blocs, Kanban, bases de données et intégration Gitea/GitHub.">
<meta name="twitter:image" content="/static/img/logo-512.jpg">
<meta name="twitter:image" content="{{ app_base_url() }}/static/img/logo-512.jpg">
<style>
:root {
--bg: #191919;
+1 -1
View File
@@ -5,7 +5,7 @@
{% from '_icons.html' import fd_icon %}
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ title }} — FlowDeck</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="/static/favicon.ico" sizes="48x48">
<style>
:root {
--bg: #191919;
+1 -1
View File
@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bienvenue sur FlowDeck</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<link rel="icon" href="/static/favicon.ico" sizes="48x48">
<script src="/static/js/alpine.csp.min.js" defer data-cfasync="false"></script>
<style>
:root{--bg:#191919;--bg2:#1F1F1F;--bg3:#2D2D2D;--border:#333;--text:#fff;--dim:#9B9A97;--accent:#2383E2;--accent-h:#1a6bc0;--success:#0F7B6C;--danger:#E03E3E;}
+12
View File
@@ -79,6 +79,18 @@ ENV.globals["csp_nonce"] = lambda: CSP_NONCE.get()
ENV.globals["csrf_token"] = lambda: CSRF_TOKEN.get()
# Plugins activés : conditionne les blocs de UI rendus côté serveur.
ENV.globals["plugin_enabled"] = lambda slug: _plugin_enabled(slug)
# v7.69 : URL de base absolue pour og:image / twitter:image — les crawlers
# ignorent les URL relatives. Lecture paresseuse comme _plugin_enabled
# (évite l'import circulaire au chargement du module). Même source que les
# liens de partage de pages (collaboration.py) : si elle est mal renseignée,
# ces liens le sont déjà.
ENV.globals["app_base_url"] = lambda: _app_base_url()
def _app_base_url() -> str:
from app.config import settings
return (settings.app_base_url or "").rstrip("/")
def _plugin_enabled(slug: str) -> bool:
+1 -1
View File
@@ -2,7 +2,7 @@
"openapi": "3.1.0",
"info": {
"title": "FlowDeck",
"version": "7.69.0"
"version": "7.69.4"
},
"paths": {
"/auth/register": {
+62
View File
@@ -0,0 +1,62 @@
// v7.69.3 — la section Aide doit avoir le MÊME positionnement que Settings
const { test, expect } = require('@playwright/test');
const FD_BASE = process.env.FD_BASE_URL || 'http://localhost:8081';
const USER = process.env.FD_USER || '[email protected]';
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(() => {});
}
// géométrie de l'overlay + du panneau, telle que perçue à l'écran
const geo = (page) =>
page.evaluate(() => {
const ov = document.querySelector('.settings-overlay');
const pn = document.querySelector('.settings-panel');
const o = getComputedStyle(ov);
const p = getComputedStyle(pn);
const r = pn.getBoundingClientRect();
return {
ovPosition: o.position,
ovBgSet: o.backgroundColor !== 'rgba(0, 0, 0, 0)',
pnWidth: Math.round(r.width),
pnHeight: Math.round(r.height),
pnCentered: Math.abs(r.left - (window.innerWidth - r.width) / 2) < 24,
pnTop: Math.round(r.top),
closeTop: Math.round(document.querySelector('.settings-close').getBoundingClientRect().top - r.top),
closeRight: Math.round(r.right - document.querySelector('.settings-close').getBoundingClientRect().right),
};
});
test('Aide et Settings partagent le même positionnement', async ({ page }) => {
await login(page);
await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(400);
const h = await geo(page);
await page.goto(`${FD_BASE}/settings`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(400);
const s = await geo(page);
console.log('HELP =', JSON.stringify(h));
console.log('SETTING=', JSON.stringify(s));
// l'overlay de Aide n'est plus désactivé en inline
expect(h.ovPosition).toBe(s.ovPosition);
expect(h.ovPosition).toBe('fixed');
expect(h.ovBgSet).toBe(true);
// panneau centré et de la même taille que Settings
expect(h.pnCentered).toBe(true);
expect(h.pnWidth).toBe(s.pnWidth);
expect(h.pnHeight).toBe(s.pnHeight);
// le bouton close occupe la même place relative au panneau
expect(h.closeTop).toBe(s.closeTop);
expect(h.closeRight).toBe(s.closeRight);
await page.goto(`${FD_BASE}/help`, { waitUntil: 'domcontentloaded' });
await page.waitForTimeout(300);
await page.screenshot({ path: 'shots/v7693-help-overlay.png' });
});
+72
View File
@@ -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 || '[email protected]';
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);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

-4
View File
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#2383E2"/>
<text x="16" y="22" text-anchor="middle" font-family="Arial,sans-serif" font-size="16" font-weight="bold" fill="#fff">FD</text>
</svg>

Before

Width:  |  Height:  |  Size: 253 B

+19 -18
View File
@@ -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());
},
};
}
+1 -1
View File
@@ -30,7 +30,7 @@ const PRECACHE_URLS = [
'/static/js/katex.min.js?v=0.16.11',
'/static/js/prism.min.js',
// Icônes & métadonnées
'/static/favicon.svg',
'/static/favicon.ico',
'/static/manifest.json',
'/static/icons/icon-192x192.png',
'/static/icons/icon-512x512.png',
+53
View File
@@ -0,0 +1,53 @@
"""v7.69.3 — la section Aide doit être positionnée comme la section Settings.
Auparavant `help.html` neutralisait l'overlay en inline
(`position:static;background:none;backdrop-filter:none`) et forçait un panneau
`1050px / 100vh-120px` : Aide s'affichait comme un bloc posé dans la page
tandis que Settings était une modale centrée. Le même gabarit partagé
(`.settings-overlay` / `.settings-panel`) produisait donc deux rendus.
"""
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SETTINGS = (ROOT / "app" / "templates" / "settings.html").read_text(encoding="utf-8")
HELP = (ROOT / "app" / "templates" / "help.html").read_text(encoding="utf-8")
def _opening_tag(src: str, css_class: str) -> str:
"""La balise d'ouverture qui porte cette classe, sans ses attributs."""
line = next(row for row in src.splitlines() if f'class="{css_class}"' in row)
# coupe avant x-data ET avant n'importe quel handler @keydown(.escape)
return line.split(" x-data=")[0].split(" @keydown")[0].strip()
class TestHelpMatchesSettingsPositioning:
def test_help_overlay_no_longer_disables_itself(self):
# l'overlay d'Aide EST celui de Settings : aucune surcharge inline ne
# peut subsister sur cette balise (le positionnement vient du CSS
# partagé). On asserte sur la BALISE, pas sur le fichier entier — le
# commentaire de tête du template mentionne ces valeurs à l'infinitif.
assert _opening_tag(HELP, "settings-overlay") == '<div class="settings-overlay"'
def test_overlay_markup_is_identical(self):
assert _opening_tag(HELP, "settings-overlay") == _opening_tag(
SETTINGS, "settings-overlay"
)
def test_panel_carries_no_inline_geometry(self):
# toute la géométrie vient de settings.css, partagé avec Settings
assert _opening_tag(HELP, "settings-panel") == '<div class="settings-panel"'
assert "max-width:1050px" not in HELP
assert "calc(100vh - 120px)" not in HELP
def test_both_pages_use_the_shared_stylesheet(self):
# le CSS qui pose l'overlay centré est bien chargé par les deux
base = (ROOT / "app" / "templates" / "base.html").read_text(encoding="utf-8")
assert "/static/css/settings.css" in base
def test_close_button_keeps_the_same_slot(self):
# même classe que Settings -> même position absolue dans le panneau
for src, name in ((HELP, "help"), (SETTINGS, "settings")):
assert 'class="settings-close"' in src, name
+35 -1
View File
@@ -123,10 +123,44 @@ class TestBranding:
def test_base_head_declares_open_graph(self):
base = (ROOT / "app" / "templates" / "base.html").read_text(encoding="utf-8")
assert 'property="og:image" content="/static/img/logo-512.jpg"' in base
# URL ABSOLUE : les crawlers ignorent les URL relatives
assert 'content="{{ app_base_url() }}/static/img/logo-512.jpg"' in base
assert 'name="twitter:card" content="summary_large_image"' in base
assert '<meta name="description"' in base
def test_og_image_url_is_absolute_via_base_url(self):
# le global expose la même source que les liens de partage de pages
templating = (ROOT / "app" / "templating.py").read_text(encoding="utf-8")
assert 'ENV.globals["app_base_url"]' in templating
for tpl in ("base.html", "landing.html"):
src = (ROOT / "app" / "templates" / tpl).read_text(encoding="utf-8")
assert 'content="{{ app_base_url() }}/static/img/logo-512.jpg"' in src, tpl
# forme RELATIVE interdite (le guillemet avant la barre est ce qui
# distingue : la bonne valeur contient l'ancienne en sous-chaîne)
assert 'content="/static/img/' not in src, tpl
def test_favicon_is_the_real_logo(self):
"""L'ancien favicon était un placeholder « FD » en SVG — pas la marque."""
ico = ROOT / "static" / "favicon.ico"
assert ico.exists()
assert ico.stat().st_size < 20_000 # un favicon pèse des Ko, pas des Mo
# 16/32/48 : la taille de l'onglet ET le repli rétro
from PIL import Image
assert Image.open(ico).info.get("sizes") >= {(16, 16), (32, 32), (48, 48)}
# le placeholder SVG doit avoir disparu
assert not (ROOT / "static" / "favicon.svg").exists()
def test_every_template_points_at_the_real_favicon(self):
for tpl in ("base.html", "import.html", "landing.html", "public_page.html", "welcome.html"):
src = (ROOT / "app" / "templates" / tpl).read_text(encoding="utf-8")
assert 'rel="icon" href="/static/favicon.ico"' in src, tpl
assert "favicon.svg" not in src, tpl
# le service worker pré-met en cache la bonne URL
sw = (ROOT / "static" / "sw.js").read_text(encoding="utf-8")
assert "'/static/favicon.ico'," in sw
assert "favicon.svg" not in sw
def test_landing_shows_logo_and_banner(self):
landing = (ROOT / "app" / "templates" / "landing.html").read_text(encoding="utf-8")
# le logo remplace l'icône générique dans la barre de navigation