fix: side peek des bases repasse en vanilla JS + largeur 1100px standard (v7.49.0)
- Panneau peek: les bindings Alpine (x-data absent du conteneur) rendaient loovverture et le redimensionnement inoperants -> cblage direct sur le document. - Helper unique window.fdWirePeekResize (app.js): pointer capture, 300px-90vw, clic=fermer, largeur persiste fd_peek_width partagee entre les 4 peeks. - database-table-container margin:0 (tableau colle a gauche, marge Library). - .lib-container remonte dans app.css (trash etait pleine largeur), .db-index 1100px. - ObsiGate verifie sans code: creation .xlsx OK (openpyxl, #186).
This commit is contained in:
+566
@@ -1,5 +1,571 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.49.0 (2026-10-05) — Side peek : l'éditeur plein écran, et un calendrier complet
|
||||
|
||||
### Added
|
||||
|
||||
- **Calendrier My Tasks : cinq vues** — Mois, Année, Semaine, Jour et Planning,
|
||||
avec une navigation propre à chaque granularité (`←` / `→` et « Aujourd'hui »
|
||||
se recalculent : par exemple 5 – 11 octobre → 12 – 18 octobre en semaine, ou
|
||||
un jour en année). L'Année affiche les douze mini-mois de l'année courante et
|
||||
le Planning une ligne par jour avec ses tâches réparties par statut.
|
||||
- **Icône d'ouverture sur chaque tâche du calendrier** — même bouton « Open in
|
||||
side peek » que le tableau, révélé au survol. Elle porte le même attribut de
|
||||
ligne (`data-open-row`), donc un seul gestionnaire de clic pour toutes les vues :
|
||||
le titre est transmis par `data-open-title`, disponible là où il n'y a pas de
|
||||
ligne `.mt-row` (vues Jour, Semaine et Année).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Impossible d'ajuster la largeur du side peek des bases** — le panneau avait
|
||||
été réécrit en bindings Alpine (`:class="dbPeekOpen"`, `@click.outside`) alors
|
||||
que le conteneur n'a aucun `x-data` : bindings inertes, `openPageInSidePeek`
|
||||
passait par `container.__x.$data` (undefined), le panneau ne s'ouvrait pas et
|
||||
la poignée de redimensionnement ne réagissait pas. Repassé en vanilla JS
|
||||
(câblage direct sur le document, repli discret pour les collections embarquées
|
||||
inline). Au passage : les quatre implémentations dupliquées du drag de
|
||||
largeur (Library, workspace local, My Tasks, bases) sont remplacées par un
|
||||
helper unique `window.fdWirePeekResize` (`app.js`) — pointer capture, borne
|
||||
300 px–90 vw, clic = fermer, largeur persistée (`fd_peek_width`) et commune
|
||||
à tous les peeks, restaurée au chargement. My Tasks ne capturait ni le
|
||||
pointeur ni la largeur : son drag était erratique et non mémorisé.
|
||||
- **Largeur des pages standardisée sur celle de Library (1100 px)** — le
|
||||
conteneur des bases (`.database-table-container`) passe à `margin: 0` : le
|
||||
tableau est collé à gauche, au ras de la marge, au lieu d'être centré dans la
|
||||
page. `/trash` utilisait la classe `.lib-container` dont la règle vivait dans
|
||||
le `<style>` de library.html : rendue hors Library elle était pleine largeur —
|
||||
la règle est remontée dans `app.css` (globale). La page `/db` (`.db-index`)
|
||||
était à 1200 px, alignée à 1100 px.
|
||||
- **Le side peek de Library n'affichait plus le contenu du document** — deux
|
||||
défauts cumulés. L'`<iframe>` vivait dans un `<template x-if="peekItem">` et
|
||||
portait un binding `:src` : le build CSP d'Alpine n'évalue pas ce type
|
||||
d'expression, si bien que le `src` restait vide et que le panneau affichait un
|
||||
document blanc. L'iframe est désormais un élément permanent, hors du template,
|
||||
dont le `src` est posé en JavaScript impératif — le motif déjà employé par les
|
||||
aperçus du workspace local.
|
||||
- **Ouvrir un autre document dans le peek laissait le précédent affiché** —
|
||||
comme l'URL ne changeait pas, le navigateur ne rechargeait pas l'iframe. Le
|
||||
peek passe maintenant par `about:blank` avant de charger le nouveau document,
|
||||
et `closePeek()` décharge l'éditeur : il ne reste plus en mémoire, son autosave
|
||||
ne tourne plus, alors qu'il est invisible.
|
||||
- **Le side peek affichait une version allégée de l'éditeur** — il chargeait
|
||||
`/pages/{id}?embed=1`, qui masque sidebar, barre d'outils et actions. Il charge
|
||||
désormais la même page que l'ouverture plein écran, dans les quatre peek du
|
||||
site (Library, My Tasks, bases de données, workspace local).
|
||||
- **Le peek des bases pouvait ouvrir un document sans rapport** — quand la
|
||||
résolution de la ligne échouait, `resolveRowContentPage` retombait sur
|
||||
`return rowId` : un id de ligne de base n'est PAS un id de page, la page
|
||||
ouverte portait donc simplement le même identifiant — exactement le piège que
|
||||
le commentaire du fichier décrivait deux lignes plus haut. L'échec est
|
||||
désormais signalé, et le message est rendu **dans le panneau** (et non via
|
||||
`toast()`) : sous le build CSP d'Alpine, aucun composant n'est initialisé et
|
||||
`window.showToast` reste sans effet.
|
||||
- **Une iframe `src=""` se résout en l'URL courante** — sur une page de base, le
|
||||
navigateur chargeait une **copie complète de la page** dans l'iframe cachée, et
|
||||
la fermeture du peek (`iframe.src = ''`) rechargeait la page entière dans le
|
||||
panneau. Le gabarit et la fermeture utilisent désormais `about:blank`.
|
||||
|
||||
### Known issues (préexistants, hors périmètre)
|
||||
|
||||
- Sous le build CSP d'Alpine (`alpine.csp.min.js`, v3.17.4), les expressions
|
||||
`x-data` inline ne sont pas évaluées : `Alpine.$data(el)` ne renvoie aucune
|
||||
clé sur les cinq éléments `[x-data]` d'une page. Conséquence directe : les
|
||||
**toasts ne s'affichent nulle part**, car `window.showToast` (`base.html`) lit
|
||||
`el.__x`, propriété du build de développement. `refreshSidebarFromEvent` et
|
||||
le repli de la sidebar (`app.js`) relèvent du même défaut. Constaté en
|
||||
vérifiant le message d'erreur du peek ; non traité ici.
|
||||
|
||||
## v7.48.0 (2026-10-05) — My Tasks aligné sur Library, side peek, navigation SPA fiable, Ctrl+S
|
||||
|
||||
### Added
|
||||
|
||||
- **Side peek sur My Tasks** — le bouton « Open » de chaque tâche ouvre le
|
||||
document dans le **panneau latéral** de la page Library (`/pages/{id}` dans une
|
||||
iframe), au lieu de quitter le tableau de bord. Panneau redimensionnable,
|
||||
fermeture par `✕` ou `Escape`, bouton « Ouvrir dans une page entière ».
|
||||
- **Ctrl/Cmd + S dans l'éditeur** — sauvegarde explicite du document en cours
|
||||
d'édition (même `save()` que l'autosave, donc même file hors-ligne). Sans lui,
|
||||
le navigateur ouvrait sa boîte « Enregistrer la page ». Inactif sur une page
|
||||
verrouillée (`read-only`), et seulement quand l'éditeur est réellement monté :
|
||||
`window.E` survit aux navigations partielles, la touche aurait donc pu
|
||||
sauvegarder un éditeur détaché depuis une autre page.
|
||||
|
||||
### Changed
|
||||
|
||||
- **My Tasks adopts le gabarit de Library** — conteneur centré 1100 px, en-tête
|
||||
`h1` 700 + compteur à droite, onglets et outils de part et d'autre, lignes en
|
||||
`div` (et non `<table>`), hauteur 38 px, survol `var(--bg-hover)`, et palette
|
||||
entièrement prise sur les tokens du thème.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Bouton d'ouverture visible seulement au survol** — il reprend exactement le
|
||||
`.btn-open` / `.hover-only` de Library (même SVG, même libellé « Open », même
|
||||
révélation `opacity`), et reste visible sur écran tactile (`@media (hover: none)`)
|
||||
où il n'y a pas de survol.
|
||||
- **My Tasks vide ou a composants blancs au retour de navigation** — trois
|
||||
défauts distincts, tous dans le chemin de navigation partielle :
|
||||
- `fdLoad` **ignorait silencieusement** un clic quand une navigation était déjà
|
||||
en vol (`if (loading) return;`) : ni l'URL ni le contenu ne changeaient. Si la
|
||||
promesse htmx ne se résolvait jamais, `loading` restait à `true` **définitivement**
|
||||
et plus aucune navigation ne repartait — page figée jusqu'au hard refresh. Le clic
|
||||
abandonne maintenant la navigation en vol par un chargement complet, et une
|
||||
sentinelle de 15 s garantit que `loading` ne peut plus se bloquer.
|
||||
- Le compteur de scripts de la zone swappée écoutait `load` **en capture sur
|
||||
`document`**, donc voit tous les `<script>` de la page : le premier script
|
||||
sans rapport suffisait à déclencher le montage Alpine **avant** que les scripts
|
||||
de la page n'aient tourné (composants affichés à l'état brut). Il ne compte
|
||||
plus que les nœuds réellement attendus.
|
||||
- `my_tasks.css` était liée **dans** `.main-wrapper` : un `<link>` dans la zone
|
||||
swappée peut être retiré par htmx, et la page revenait alors sans aucune feuille
|
||||
de style — d'où les `<select>` / `<input type="date">` blancs en thème sombre.
|
||||
La feuille est désormais liée par `base.html` (le shell, jamais swappé) ; les
|
||||
deux injections par page ont été retirées.
|
||||
- **My Tasks se remonte s'il est resté vide** — une coquille montée mais vide est
|
||||
le symptôme d'un swap interrompu : le tableau de bord se remonte plutôt que
|
||||
d'afficher du vide. Les délégations d'événements ne sont posées qu'une fois par
|
||||
nœud (`__fdWired`), donc aucun double gestionnaire.
|
||||
|
||||
## v7.47.0 (2026-10-04) — My Tasks façon Notion : 3 vues, sources opt-in, mapping explicite
|
||||
|
||||
> My Tasks devient un poste de travail **transverse** (comme l'onglet *Home*
|
||||
> de Notion) : toutes vos tâches, **tous workspaces confondus**. Une base
|
||||
> n'y alimente plus qu'après **conversion explicite** (« Convertir en base de
|
||||
> tâches ») avec le mapping de ses trois colonnes requises — et il ne peut y
|
||||
> en avoir plus de 10.
|
||||
|
||||
### Added
|
||||
|
||||
- **Sources opt-in avec mapping explicite** — migration 30 : `collections`
|
||||
porte `task_assignee_prop` / `task_status_prop` / `task_due_prop`. La
|
||||
conversion est explicite (`POST /db/{id}/task-db/api`) : chaque rôle est
|
||||
**lié** à une colonne existante du bon type, ou **créé** avec le type
|
||||
attendu (`person` / `status` / `date`). Une base connectée au mapping
|
||||
incomplet est listée mais **n'émet aucune tâche** — l'UI le signale au lieu
|
||||
de deviner une colonne. Suppression du mapping à la déconnexion (plus de
|
||||
références mortes), et la limite de **10 bases** est refusée côté serveur.
|
||||
- **Trois vues** — tableau (colonnes redimensionnables non, mais statut et
|
||||
échéance éditables au clic), **kanban** glisser-déposer (les colonnes sont
|
||||
les options de statut de la base d'origine) et **calendrier** mensuel.
|
||||
- **Filtres et tris globaux** — échéance (toutes / aujourd'hui / en retard /
|
||||
cette semaine, borné à J+7), « masquer les terminées », tri par échéance,
|
||||
statut, source ou création.
|
||||
- **Étiquette de source** sur chaque tâche (base d'origine + workspace),
|
||||
cliquable vers la base.
|
||||
- **Modification en direct** — `PUT /my-tasks/api/task/{id}` écrit le champ
|
||||
demandé (`status`, `due`, `title`, `assignee`) dans **la colonne mappée de
|
||||
la base source**, puis relit et renvoie la tâche normalisée. Désassigner
|
||||
une tâche la fait sortir de My Tasks (`left_mytasks`).
|
||||
- **Ajout rapide multi-destinations** — `POST /my-tasks/api/task` crée la
|
||||
tâche dans la base choisie dans la modale, **auto-assignée** (sans colonne
|
||||
« Assigné à » renseignée, elle n'apparaîtrait jamais dans « mes tâches »).
|
||||
- **Modale de conversion** dans la barre de la base (bouton `✓`) :
|
||||
`static/js/task_db_link.js` + `static/css/my_tasks.css`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Périmètre transverse** — `flowdeck_workspace` ne cadre plus My Tasks
|
||||
(il reste lu par la sidebar). Le payload `/my-tasks/api` expose `tasks`,
|
||||
`sources`, `sources_total`, `max_sources` et `filters` ; **plus de
|
||||
`workspace_id`** (le contract précédent est modifié, pas additionsnel).
|
||||
- **`PUT /db/{id}/toggle-task/api`** reste rétrocompatible mais délègue à la
|
||||
conversion complète : il ne pose plus `is_task=1` seul, ce qui créait une
|
||||
source « connectée » incapable d'émettre quoi que ce soit.
|
||||
- **Rendu client** — la page HTML porte la configuration (`data-config`) et
|
||||
charge `static/js/my_tasks.js` ; sans aucune source connectée, le serveur
|
||||
rend l'invite à convertir une base et ne charge pas le script.
|
||||
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`/db` était un dump JSON de debug** — la page renvoyait un `<pre>` hors
|
||||
layout, sans session requise : « Ouvrir mes bases » (bouton de l'état vide
|
||||
My Tasks) menait à un écran vide. `app/routers/collections/index_page.py`
|
||||
la remplace par une vraie page dans le layout de l'app : les bases **du
|
||||
workspace courant**, leur état (« Base de tâches » / « À configurer »), le
|
||||
nombre de lignes, et ce qu'il reste à faire. Les bases à la corbeille n'y
|
||||
sont pas proposées.
|
||||
- **Bouton de conversion introuvable** — c'était un `✓` nu, sans libellé.
|
||||
Il porte maintenant le libellé de son état réel : « Convertir en base de
|
||||
tâches », « ⚠ Configurer » ou « ✓ Base de tâches ».
|
||||
- **L'invite My Tasks renvoyait vers un bouton « … » qui n'existe pas** — le
|
||||
message indiquait le menu `…` de la page alors que le bouton est dans la
|
||||
barre de la base, à gauche de `New`. Texte corrigé, et un bouton
|
||||
« Configurer » apparaît sur chaque source mal configurée.
|
||||
|
||||
- **Kanban et calendrier vidaient la page** — un clic sur l'onglet rendait la
|
||||
vue, puis l'intercepteur de liens de `app.js` (`shouldIntercept`) traitait le
|
||||
**même** clic et relançait un chargement complet : le DOM rendu était
|
||||
remplacé par un `#my-tasks-app` vide et l'écran devenait blanc. Le clic est
|
||||
désormais marqué `data-no-nav` sur les onglets, et `shouldIntercept` ignore
|
||||
tout événement déjà traité (`defaultPrevented`) — plus aucun composant ne
|
||||
peut plus être intercepté par la navigation SPA.
|
||||
- **`/db` n'avait aucune feuille de style** — `my_tasks.css` (qui porte les
|
||||
règles `.db-*`) n'était liée que par `_database_table_scripts.html`, jamais
|
||||
par la page `/db` : les cartes, sections et états vides s'affichaient sans
|
||||
aucun style. La feuille est maintenant liée explicitement.
|
||||
- **My Tasks ignorait le thème sombre** — le CSS utilisait `--bg-card`, une
|
||||
variable **inexistante** dans `app.css` : le repli `#fff` rendait les cartes,
|
||||
onglets et panneaux blancs en mode sombre. Remplacée par `--bg-primary`, qui
|
||||
suit le thème (mesuré : `rgb(25,25,25)` en sombre, `rgb(255,255,255)` en
|
||||
clair).
|
||||
- **États vides non centrés** — l'invite « aucune base connectée » et les états
|
||||
vides de `/db` étaient un bloc de texte serré à gauche, en styles inline.
|
||||
Gabarit centré commun (icône ronde, étapes numérotées, bouton), sans style
|
||||
inline, donc aligné sur le thème ; `/db` recentre son en-tête et sa section
|
||||
« Autres workspaces ».
|
||||
- **My Tasks « non connectée » n'avait aucune feuille de style** — `_render_shell`
|
||||
construisait le bloc `<link>` + `<script>` dans le même mini-template, or
|
||||
seul le script était conditionné à `app_js`. C'est précisément le cas
|
||||
« aucune base connectée » : la page perdait `my_tasks.css` et l'invite
|
||||
retombait en texte brut aligné à gauche, sans les états vides centrés. La
|
||||
feuille est désormais liée **toujours**, seul le script reste conditionnel.
|
||||
- **« file Tache 1 » / « file Tache 2 » sous la colonne Tâche** — ce n'était
|
||||
pas un libellé mais `collection_pages.icon`, valeur sentinelle historique
|
||||
signifiant « pas d'icône » (le même filtrage existait déjà dans
|
||||
`database_table.js`). `rowIcon()` la remplace désormais par `📄` dans le
|
||||
tableau et le kanban.
|
||||
- **Cellule « Tâche » collée au bord gauche** — `.mt-td-title` est un flex
|
||||
et ne recevait pas le `padding` des autres cellules ; le titre commençait à
|
||||
1 px de la bordure du tableau (mesuré : 13 px après correction).
|
||||
- **Pas de bouton pour voir le contenu d'une tâche** — chaque ligne du
|
||||
tableau porte désormais un bouton qui résout la page de contenu via
|
||||
`GET /db/pages/{id}/open/api` (un id de ligne n'est pas un id de page, la
|
||||
page est créée à la volée) puis ouvre l'éditeur. Si la résolution échoue,
|
||||
on reste sur le tableau de bord et on remonte l'erreur : un repli vers
|
||||
`/pages/{rowId}` ouvrirait une page **sans rapport** (piège déjà documenté
|
||||
dans `database_table.js`).
|
||||
- **My Tasks vide après navigation** — le script se gardait d'un drapeau
|
||||
global (`window.__fdMyTasksLoaded`) : la navigation interne de `app.js`
|
||||
remplace `.main-wrapper` par htmx **sans recharger le document**, le `<script>`
|
||||
est rejoué mais sortait aussitôt du IIFE, et la nouvelle coquille restait
|
||||
vide jusqu'au hard refresh. Le montage est désormais une fonction
|
||||
rejouable, protégée par un drapeau **porté par l'élément**, rejouée sur
|
||||
`htmx:afterSwap` et `popstate` (vérifié : deux aller-retours consécutifs
|
||||
restituent tableau, barre d'outils et onglets).
|
||||
- **Test dépendant de l'heure** — `test_today_follows_the_user_timezone`
|
||||
supposait que le jour UTC soit toujours différent du jour en UTC-7, ce qui
|
||||
n'est vrai que de 00h à 10h UTC : il échouait 13 heures sur 24. Le cas est
|
||||
maintenant traité explicitement au lieu d'être supposé faux.
|
||||
- **Bases portées par un document invisibles (`workspace_id` NULL)** — une base
|
||||
née d'une page (page convertie en base, base inline, base liée, modèle
|
||||
importé) n'a pas de `collections.workspace_id` : elle n'appartient qu'à travers
|
||||
sa page hôte. Le filtre `c.workspace_id = ?` et le `JOIN workspaces` de
|
||||
`list_task_sources` ne la trouvaient jamais, donc `/db` l'ignorait **et** elle
|
||||
n'alimentait pas My Tasks — la base était visible dans la sidebar mais
|
||||
introuvable et non convertible. Le rattachement passe désormais par le
|
||||
workspace **effectif**, `COALESCE(workspace_id, workspace de la page hôte)`
|
||||
(`collection_lifecycle.effective_workspace_sql`), utilisé par
|
||||
`live_collection_ids`, `list_task_sources` et la page `/db`.
|
||||
- **Fuite de bases entre utilisateurs sur `/db`** — la section « Autres
|
||||
workspaces » faisait `JOIN workspaces` **sans filtre de propriétaire** : elle
|
||||
exposait les bases de tous les utilisateurs de l'instance. Le périmètre est
|
||||
maintenant `user_workspace_ids` (propriétaire **ou** membre). Une base sans
|
||||
workspace rattachable n'appartient à personne et n'est plus listée nulle part.
|
||||
- **Base à la corbeille qui alimentait encore My Tasks** — le filtre de
|
||||
vivacité (`live_collection_ids`) était appliqué aux lignes mais pas aux
|
||||
bases dans le nouveau service : une base dont la page hôte avait disparu
|
||||
émettait toujours ses tâches (le bug v7.46.1, reintroduit par la réécriture).
|
||||
- **`is_done` ne reconnaissait pas « Terminée »** — la liste de libellés
|
||||
exacts ne couvrait ni le féminin, ni les accents, ni la casse. La
|
||||
comparaison est faite sur le **premier mot normalisé** (minuscules, sans
|
||||
accents) : « Terminée », « Complete », « Closed », « Cancelled » sont
|
||||
reconnus ; « Clôture » ne l'est pas (un préfixe « clos » l'aurait fait).
|
||||
- **« Cette semaine » n'était pas borné** — le filtre incluait toute tâche
|
||||
datée, y compris dans six mois.
|
||||
- **« Aujourd'hui » suivait UTC** — le projet stocke déjà `users.timezone`
|
||||
(utilisé par le calendrier des bases) ; My Tasks l'utilise maintenant aussi
|
||||
(`task_databases.user_today`), sans quoi une tâche du jour bascule dans
|
||||
« demain » dès que l'utilisateur est loin d'UTC.
|
||||
- **Colonne « Assigné à » écrasée par un booléen** — l'édition en direct
|
||||
écrivait `bool(value)` au lieu de la liste de personnes, vidant la colonne.
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_my_tasks_workspace_coherence.py` — **32 tests** : périmètre
|
||||
transverse, opt-in, mapping incomplet, validation des types et du double
|
||||
emploi, limite de 10, assignataires, filtres/tri/états d'échéance, édition
|
||||
en direct (statut, échéance, titre, désassignation), création rapide,
|
||||
rendu des 3 vues, fuseau, et les cascades de suppression (corbeille, purge,
|
||||
Empty Trash, workspace, base).
|
||||
- `tests/test_app.py` — les 3 tests My Tasks alignés sur le nouveau contrat.
|
||||
|
||||
## v7.46.1 (2026-10-04) — My Tasks : la vue ne montre plus que le contenu vivant du workspace
|
||||
|
||||
> Signalé : « dans un workspace dont j'ai supprimé tous les documents, My
|
||||
> Tasks affiche encore des tâches ». My Tasks listait **toutes** les
|
||||
> `collections` du workspace sans vérifier qu'un document existait encore —
|
||||
> or supprimer un document ne supprime pas sa base ni ses lignes.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Tâches orphelines** — `_collect_tasks` faisait
|
||||
`SELECT * FROM collections WHERE workspace_id = ?` : une base dont la page
|
||||
hôte avait été supprimée (corbeille ou purge) continuait d'afficher ses
|
||||
lignes, dans un workspace vide côté sidebar. Les bases et les lignes sont
|
||||
désormais filtrées sur la **vivacité de leur document**
|
||||
(`app/services/collection_lifecycle.py` : `live_collection_ids`,
|
||||
`live_row_ids`, `live_document_count`) — une page hôte absente ou à la
|
||||
corbeille exclut la base, une page contenu à la corbeille exclut la ligne.
|
||||
- **My Tasks réécrivait le cookie `flowdeck_workspace`** — un 302 +
|
||||
`set_cookie` à chaque visite où le cookie différait : **ouvrir My Tasks
|
||||
changeait le workspace actif de toute l'application** (sidebar, Home, arbre).
|
||||
Le cookie n'est plus touché ; le workspace résolu est simplement celui du
|
||||
contenu affiché.
|
||||
- **Divergence de résolution du workspace** — My Tasks réimplémentait son
|
||||
propre `_get_active_workspace_id` et ne comprenait pas le cookie
|
||||
`gitea:owner:repo` écrit par la sidebar : il retombait alors sur
|
||||
`owned[0]` et affichait les tâches d'un **autre** workspace que celui
|
||||
affiché. Résolution mutualisée avec `dashboard._common._get_active_workspace`
|
||||
(+ résolution du miroir Gitea local, comme `_sidebar_data`).
|
||||
- **Sidebar désynchronisée du contenu** — le contexte de la sidebar venait du
|
||||
cookie alors que le contenu venait du workspace résolu ; elle est
|
||||
réalignée sur le workspace réellement listé (nom, `local_ws_id`, arbre).
|
||||
- **État vide incohérent** — « Crée une collection » était proposé même
|
||||
quand le workspace n'avait plus aucun document. Distinction désormais :
|
||||
*workspace vide* (lien vers le workspace) vs *workspace sans tâche* (lien
|
||||
vers `/db`).
|
||||
- **Collections orphelines après suppression** — la suppression définitive
|
||||
d'une page hôte (poubelle purgée, `DELETE /api/trash/{id}`, Empty Trash),
|
||||
d'un workspace ou d'un utilisateur ne supprimait pas les bases : elles
|
||||
restaient listées par `/db` et par My Tasks avec un `workspace_id` ou un
|
||||
`parent_page_id` fantôme. Cascade appliquée sur ces 4 chemins
|
||||
(`trash.purge_expired`, `board/sharing.permanent_delete`,
|
||||
`board/sharing.empty_trash`, `dashboard/workspaces.delete_workspace`,
|
||||
`admin.delete_user`).
|
||||
- **`DELETE /db/api/{id}` levait un `IntegrityError`** dès que la base avait
|
||||
une page hôte : `pages.collection_id` et
|
||||
`collection_data_sources.source_collection_id` référencent `collections` en
|
||||
`NO ACTION`. `delete_collections()` détache d'abord ces deux références
|
||||
(la page hôte redevient une page blocs normale), puis supprime ; le reste
|
||||
part en `CASCADE`. Les 5 chemins de suppression de collection en profitent
|
||||
(`/db/api/{id}`, `/api/v2/collections/{id}`, sync, tool agent).
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_my_tasks_workspace_coherence.py` — **12 tests** : base à la
|
||||
corbeille (tâches masquées **et** restaurables), ligne à la corbeille,
|
||||
message « workspace vide » vs « sans tâche », suppression définitive /
|
||||
purge / Empty Trash / suppression de workspace (base + lignes + pages
|
||||
contenu emportées), détachement de la page hôte à la suppression d'une
|
||||
base, cookie non réécrit, sidebar alignée sur le contenu, isolation entre
|
||||
workspaces.
|
||||
|
||||
## v7.46.0 (2026-10-04) — Sidebar : navigation par onglets compressibles (Home / Chat / Meeting / Inbox)
|
||||
|
||||
> La rangée d'icônes du sidebar devient un **sélecteur d'onglet** : l'onglet
|
||||
> actif s'étend en pilule avec son libellé, les autres se réduisent à leur
|
||||
> icône, et chaque sélection **remplace intégralement** le contenu de la barre
|
||||
> latérale. Ordre imposé : **Home → Chat → Meeting → Inbox**.
|
||||
|
||||
### Added
|
||||
|
||||
- **Onglet Meeting** — calendrier de productivité : filtres `Upcoming` /
|
||||
`Today`, actions rapides (*Transcribe a podcast* : création de la page de
|
||||
réunion + `POST /api/v2/meetings/transcribe` ; *New AI meeting note* :
|
||||
commande `/meeting-note` de l'agent), et la liste chronologique des
|
||||
événements.
|
||||
- **`GET /api/v2/meetings/upcoming?days=30`** — nouveau endpoint authentifié
|
||||
(session ou Bearer) qui agrège les collections calendaires liées de
|
||||
l'utilisateur (`calendar_links`), extrait la propriété date de chaque ligne
|
||||
et renvoie les événements du jour et des `days` jours suivants, triés, avec
|
||||
`today` / `synced` / `provider` / `url`. Alias publics `date_prop_id` et
|
||||
`row_date` dans `app/services/calendar_sync.py`.
|
||||
- **Onglet Chat** — raccourcis circulaires vers les agents
|
||||
(`GET /api/agent`) + historique chronologique des conversations
|
||||
(`GET /api/agent/conversations`) regroupé en *Past 30 days* / *Older*. Un
|
||||
clic ouvre le panneau agent **sur la conversation choisie** via le nouveau
|
||||
`window.fdAgent.openConversation(id)`.
|
||||
- **Onglet Inbox** — vrai centre de notifications (`GET /api/notifications`) :
|
||||
bascule *All / Unread*, marquage lu au clic, *Mark all read*, état vide
|
||||
*Inbox Zero — You're all caught up*. Badge non lus sur l'onglet (et compteur
|
||||
dans l'en-tête du panneau quand l'onglet est actif).
|
||||
- Persistance de l'onglet actif (`localStorage: fd_sidebar_tab`, valeur
|
||||
validée contre la liste des onglets).
|
||||
|
||||
### Changed
|
||||
|
||||
- `sidebar-nav-row` : libellés désormais dans des `<span class="nav-tab-label">`
|
||||
masqués sauf sur l'onglet actif (CSS `.nav-tab-btn`), badges de compte,
|
||||
`role="tab"` / `aria-selected`. Les boutons Chat / Meeting / Inbox sont de
|
||||
vrais `<button type="button">` (le bouton Inbox n'avait aucune action).
|
||||
- Le contenu Home (arbre du workspace, sections, liens Library / My Tasks /
|
||||
Trash / Help) est encapsulé dans le panneau `sidebarTab === 'home'` ; le
|
||||
footer reste commun.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Clic sur Home → texte sur les quatre boutons** — la visibilité des
|
||||
libellés ne dépendait que de `app.css`, or l'URL des assets statiques est
|
||||
versionnée par le fichier `VERSION` (`?v=7.45.6`) : un navigateur conservait
|
||||
donc la feuille de style d'avant la livraison, où les
|
||||
`<span class="nav-tab-label">` n'étaient pas masqués. La décision est
|
||||
désormais prise dans le **HTML** (`style` inline piloté par Alpine) pour les
|
||||
libellés **et** les badges ; la CSS reste le repli sans JavaScript.
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_sidebar_tabs.py` — **15 tests** : ordre exact des onglets
|
||||
(template **et** HTML rendu), accessibilité, un panneau par onglet, sources de
|
||||
données réelles, persistance validée, pilule active / libellés masqués (CSS),
|
||||
401 anonyme sur `/api/v2/meetings/upcoming`, filtrage *aujourd'hui et
|
||||
futur*, exclusion du passé et de l'horizon, collection sans lien, validation
|
||||
et clamp de `days`, cloisonnement par propriétaire de lien.
|
||||
- `e2e/sidebar_tabs.spec.js` — **3 scénarios** : bascule des 4 onglets avec
|
||||
mesure de largeur (pilule > icône) et vérification qu'**après le clic sur
|
||||
Home seul Home garde son libellé**, appel réseau de
|
||||
`/api/v2/meetings/upcoming`, et non-régression avec une CSS périmée servie
|
||||
depuis le cache (le scénario échoue bien sur l'ancienne version déployée).
|
||||
- Suite complète : **1182 tests verts**.
|
||||
|
||||
## v7.46.0 (2026-10-04) — Audit de fonctionnement : 10 anomalies corrigées
|
||||
|
||||
> Second volet de la v7.46.0 (le premier volet, web tools + 11 skills, est
|
||||
> détaillé plus bas). Audit réalisé sur l'instance locale : 677 routes, 280
|
||||
> endpoints GET sondés anonymement, 64 templates/JS croisés avec les routes,
|
||||
> crawl Playwright authentifié, intégrité SQLite. **10 anomalies** trouvées,
|
||||
> toutes corrigées et verrouillées par **34 tests** dédiés
|
||||
> (`tests/test_anomalies_v746.py`). Suite complète : **1167 tests verts**.
|
||||
|
||||
### Security
|
||||
|
||||
- **Écritures anonymes closes** — `POST/DELETE /api/workspaces`,
|
||||
`POST /api/workspaces/{id}/select`, `PUT /api/workspaces/{id}` et les
|
||||
endpoints `local-workspace/items` (create/rename/delete/restore/move)
|
||||
répondaient **200 sans aucune session** et écrivaient au nom de
|
||||
l'utilisateur 1 (`uid = user["id"] if user and user.get("id") else 1`).
|
||||
Un visiteur anonyme pouvait donc créer un espace, **renommer** et
|
||||
**supprimer** n'importe quel espace de travail. Ces routes passent par
|
||||
`_require_user_id()` et un nouveau `_require_ws_owner()` (propriétaire ou
|
||||
admin global, sinon 403 ; workspace inexistant → 404).
|
||||
- **Fuite de données via `/workspace/*`** — le router legacy
|
||||
`app/routers/workspace.py` (relique v2.0.0) n'avait aucun garde :
|
||||
`GET /workspace/collections/{id}/export/csv` renvoyait **le contenu réel**
|
||||
d'une collection à un visiteur anonyme (titres, tâches, assignations), de
|
||||
même que l'historique de page, les commentaires, les sprints et les
|
||||
templates. Le router porte maintenant une dépendance
|
||||
`Depends(_require_session)` ; **la seule route publique**
|
||||
(`/workspace/public/{collection_id}`, page de partage) est déplacée dans un
|
||||
`public_router` sans dépendance et reste accessible.
|
||||
- **`password_hash` exposé** — `GET /auth/user` renvoyait la ligne `users`
|
||||
complète, **sel et hachage PBKDF2 compris**, et le cookie de session
|
||||
(signé mais **non chiffré**) embarquait la même donnée en base64. Nouveau
|
||||
`app.auth.session.public_user()` appliqué dans `create_session`,
|
||||
`refresh_session` et `/auth/user`.
|
||||
- **OAuth Gitea cassé** — `gitea_oauth_client_id`/`_secret` valent les
|
||||
placeholders `test-id`/`test-secret` dans `app/config.py`, valeurs non vides
|
||||
donc considérées comme « configurées » : `GET /auth/login` redirigeait vers
|
||||
Gitea avec un `client_id` invalide (échec d'authentification garanti).
|
||||
`app/auth/providers` traite désormais les placeholders comme « provider non
|
||||
configuré » → page « non configuré » avec lien vers le login local.
|
||||
- **Synced blocks** — `GET /board/api/synced-blocks` levait
|
||||
`AttributeError: 'NoneType' object has no attribute 'get'` (**HTTP 500**) en
|
||||
accès anonyme ; `PUT`/`DELETE /board/api/synced-blocks/{sid}` n'avaient
|
||||
**aucun contrôle de propriété** (un utilisateur connecté pouvait modifier le
|
||||
bloc d'un autre). Ajout de `_session_user()` (401) et
|
||||
`_assert_can_edit_block()` (auteur ou admin, sinon 403).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Bouton « Home » de la sidebar** — il pointait sur
|
||||
`/local-workspace?ws={{ local_workspaces[0].id }}`, c'est-à-dire le **premier
|
||||
espace trié par nom** et non l'espace courant : cliquer Home depuis
|
||||
`NewWS` basculait vers `allo` (et inversement), ce qui donne l'impression que
|
||||
« tout le site s'affiche d'un coup ». Les deux builders de sidebar
|
||||
(`dashboard/_common.py`, `board/_common.py`) exposent désormais
|
||||
`local_ws_id` = **ID de l'espace actif** (le miroir Gitea a été séparé dans
|
||||
`gitea_mirror_ws_id` pour ne plus écraser cette valeur), et `base.html`
|
||||
privilégie `local_ws_id` avant `local_workspaces[0]`.
|
||||
- **`GET /api/v2/agents/conversations` → 422** — la route paramétrée
|
||||
`/agents/{agent_id}` était enregistrée **avant** et capturait le segment
|
||||
`conversations`. Les trois routes passent en `{agent_id:int}` : un
|
||||
identifiant d'agent est un entier, le segment statique garde sa route.
|
||||
- **Sélecteur de blocs synchronisés → 404** — l'éditeur de page appelait
|
||||
`/api/synced-blocks` alors que les routes sont sous `/board/` :
|
||||
`static/js/page_editor_scripts.js` utilise maintenant
|
||||
`/board/api/synced-blocks`, et le message d'aide ne renvoie plus vers une
|
||||
section « Settings → Synced Blocks » **inexistante**.
|
||||
- **`/workspace/favorites` → 500 permanent** — `sqlite3.OperationalError: no
|
||||
such column: f.collection_id` : la migration v2.2.0 (`app/db.py`) avait
|
||||
supprimé `favorites.collection_id` et repointé `page_id` vers `pages(id)`,
|
||||
mais le handler lisait encore l'ancien schéma (jointure `collection_pages`).
|
||||
Requête et insertion alignées, `page_id` désormais obligatoire, suppression
|
||||
restreinte au propriétaire du favori.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Jinja : plus de silence** (`app/templating.py`) — une variable absente du
|
||||
contexte rendait `""` sans aucune trace, ce qui avait masqué le bug du
|
||||
bouton Home. `StrictUndefined` est inutilisable ici (40+ templates
|
||||
optionnelles, p. ex. `embed_mode`), donc `ENV.undefined` vaut désormais
|
||||
`LoggingUndefined` : **rendu identique, mais un `WARNING` journalise le nom
|
||||
de la variable manquante**.
|
||||
- `.env.example` : `GITEA_OAUTH_CLIENT_ID` / `GITEA_OAUTH_CLIENT_SECRET`
|
||||
documentés (laisses vides = provider Gitea considéré comme non configuré).
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_anomalies_v746.py` — **34 tests** : absence de `password_hash`
|
||||
(endpoint + cookie), placeholders OAuth (non configuré vs configuré), 9
|
||||
écritures anonymes refusées en 401, propriété des espaces (403 + état en
|
||||
base inchangé), 7 lectures `/workspace/*` en 401, partage public toujours
|
||||
accessible, round-trip favorites, ordre de routes agents, URLs synced blocks
|
||||
(fichier), 401 anonyme sur les synced blocks + 403 pour un autre
|
||||
utilisateur, bouton Home (template **et** HTML rendu sur l'espace actif),
|
||||
`LoggingUndefined`.
|
||||
|
||||
## v7.46.0 (2026-10-04) — Web tools de l'agent (recherche web, lecture de page, GitHub) & 11 nouveaux skills
|
||||
|
||||
### Added
|
||||
|
||||
- **3 nouveaux tools agent** — l'agent pouvait chercher dans FlowDeck et dans
|
||||
Gitea, mais **pas sur le web**. `app/services/tool_registry.py` passe de 22 à
|
||||
25 tools :
|
||||
- `web_search` — recherche web, résultats toujours sourcés (titre + URL +
|
||||
extrait) pour que le modèle cite au lieu d'inventer ;
|
||||
- `fetch_url` — télécharge une page et la convertit en Markdown (titres,
|
||||
listes, code, tableaux) ;
|
||||
- `search_code` — GitHub `repositories | code | issues`, fonctionnel sans jeton
|
||||
(10 requêtes/min) et à 30 avec `GITHUB_TOKEN`.
|
||||
- **`app/services/web_search.py`** — couche réseau des deux tools de recherche :
|
||||
provider **Exa** (sémantique + extraits) avec repli **DuckDuckGo** sans compte,
|
||||
`shared_client` pour le pool de connexions, **aucune dépendance ajoutée**, URLs
|
||||
du moteur filtrées des résultats, `transport` injectable pour les tests.
|
||||
- **11 nouveaux presets de skills** (`app/services/skill_gallery.py`, 6 → 17) :
|
||||
- locaux : `veille-documentaire`, `chasse-decisions`, `onboarding-nouveau`,
|
||||
`audit-base`, `sprint-review` ;
|
||||
- web : `recherche-marche`, `veille-techno`, `debug-web` (obligation de
|
||||
sourcer : `[titre — URL]`, sinon « non vérifié ») ;
|
||||
- pouvoir : `triage-incident`, `nettoyage-base` (aperçu avant toute écriture),
|
||||
`briefing-quotidien`.
|
||||
- **Config** — `WEB_SEARCH_PROVIDER` (`exa` | `duckduckgo`), `EXA_API_KEY`,
|
||||
`GITHUB_TOKEN`, `WEB_FETCH_MAX_CHARS` (`app/config.py` + `.env.example`).
|
||||
- **Libellés d'action** du journal agent pour les 3 tools (Recherche web
|
||||
effectuée / Page web lue / Recherche GitHub effectuée).
|
||||
|
||||
### Security
|
||||
|
||||
- **`fetch_url` est protégé par SSRF** : schémas `http(s)` uniquement, résolution
|
||||
DNS puis refus des IP loopback, privées, lien-local, réservées et multicast,
|
||||
et **re-validation à chaque saut de redirection** — une URL publique ne peut
|
||||
pas rediriger vers `169.254.169.254`. Il réutilise les garde-fous déjà éprouvés
|
||||
par `importers/url_fetch` et `og_fetcher` plutôt qu'un contrôle maison.
|
||||
- `search_code` ne touche que l'API publique GitHub (lecture seule) ; le
|
||||
message d'erreur 403 renvoie explicitement vers `GITHUB_TOKEN`.
|
||||
|
||||
### Tests
|
||||
|
||||
- `tests/test_agent_web_tools.py` : **29 tests**, sans aucun accès réseau
|
||||
(`httpx.MockTransport` + résolution DNS neutralisée — les IP littérales
|
||||
passent par la résolution réelle pour que le refus des adresses privées soit
|
||||
testé pour de vrai) — registre et périmètre des schémas, cohérence
|
||||
galerie/registre, cycle export→import des presets web, provider + repli +
|
||||
filtrage anti-bot, extraction Markdown, troncature, **6 vecteurs SSRF refusés**
|
||||
(localhost, 127.0.0.1, metadata cloud, IPv6 loopback, `file:`, `ftp:`),
|
||||
redirection vers metadata cloud refusée, redirection publique suivie, GitHub
|
||||
repos/issues/quota.
|
||||
- Non-régression : **suite complète 1126 tests verts** (`pytest -n auto`).
|
||||
|
||||
## v7.45.6 (2026-10-03) — Fenêtre de confirmation thématisée pour les suppressions de la poubelle
|
||||
|
||||
### Changed
|
||||
|
||||
Reference in New Issue
Block a user