Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53ba3d1124 | ||
|
|
8ce3b4b421 | ||
|
|
193fbd14ce |
@@ -1,5 +1,36 @@
|
||||
# Changelog - FlowDeck
|
||||
|
||||
## v7.82.2 (2026-10-25) — CI test entièrement verte : pages-supports de templates exclues des listes
|
||||
|
||||
Cause racine commune à 7 des 8 échecs restants : la refonte templates
|
||||
(`fc85481`, 2026-10-10) a introduit les **pages-supports** — une page
|
||||
interne par template système (99 sur une base fraîche), créée par
|
||||
`TemplateService._create_holder` avec `search_excluded=1` et la consigne
|
||||
« masquée, jamais listée ». Aucune liste ne respectait cette consigne :
|
||||
datées `updated_at = now`, elles occupaient toutes les places des
|
||||
listes triées par récence (LIMIT 50/20/8), devant les vraies pages.
|
||||
|
||||
- **Bibliothèque** (`app/routers/library.py`) : filtre `NO_HOLDER`
|
||||
(`COALESCE(p.search_excluded, 0) = 0`) ajouté aux onglets meetings,
|
||||
recents, favorites, shared, published, private et workspace.
|
||||
- **Sidebar Recents** (`dashboard/_common.py::_recent_sidebar_pages`)
|
||||
et **projets builtin** (`dashboard/workspace.py`) : même exclusion —
|
||||
les pages-supports n'y sont plus ni des « fichiers récents » ni des
|
||||
« projets ».
|
||||
- **Mentions agent** (`routers/agent.py::list_mentions`) : les trois
|
||||
scans de pages excluent les pages-supports, jamais mentionnables.
|
||||
- **Flake `test_v55` (og parser)** : `mock_http`
|
||||
(`tests/test_agent_web_tools.py`) répare désormais aussi la référence
|
||||
`shared_client` figée par `og_fetcher` quand il est importé pendant
|
||||
la fenêtre patchée (même classe de fuite que celle déjà traitée pour
|
||||
`url_fetch`) — le handler d'un test `fetch_url` répondait sinon aux
|
||||
fetchs OG des tests suivants du même worker xdist.
|
||||
- **`test_editor_front_end_wired`** : depuis `fc85481`, le picker de
|
||||
`base.html` passe par le registre unifié (`/api/templates`) et le
|
||||
câblage `page-templates` vit dans l'éditeur
|
||||
(`static/js/page_editor_scripts.js`) ; le marqueur est vérifié dans
|
||||
le fichier qui le porte. Aucune assertion supprimée ni affaiblie.
|
||||
|
||||
## v7.82.1 (2026-10-25) — CI : lint ruff vert et littéral de version resynchronisé
|
||||
|
||||
- **Lint (CI run 2304, job 7058)** : les 16 erreurs `ruff check
|
||||
|
||||
+1
-1
@@ -216,7 +216,7 @@ def _lire_version() -> str:
|
||||
|
||||
|
||||
# Littéral de version synchronisé avec le fichier VERSION :
|
||||
# version="7.82.1"
|
||||
# version="7.82.2"
|
||||
# Patron historique des bumps (voir scripts/bump_version.py, qui cherche
|
||||
# ce littéral) et contrôle de tests/test_pwa_offline.py. La version
|
||||
# runtime reste lue dynamiquement depuis VERSION par _lire_version()
|
||||
|
||||
@@ -838,6 +838,8 @@ def list_mentions(request: Request, q: str = ""):
|
||||
Les entrées identiques (même type + même titre, ex. plusieurs pages « Sans
|
||||
titre ») sont **dédupliquées** pour ne pas afficher le même fichier plusieurs fois.
|
||||
"""
|
||||
# Les scans de pages excluent les pages-supports de templates
|
||||
# (search_excluded=1 — cf. library.NO_HOLDER) : jamais mentionnables.
|
||||
query = (q or "").strip()[:60]
|
||||
like = f"%{query}%"
|
||||
limit = 8
|
||||
@@ -873,7 +875,7 @@ def list_mentions(request: Request, q: str = ""):
|
||||
if ws:
|
||||
sql_ws = (
|
||||
"SELECT p.id, p.title FROM pages p "
|
||||
"WHERE p.workspace_id=? AND p.deleted_at IS NULL AND (? = '' OR p.title LIKE ?) "
|
||||
"WHERE p.workspace_id=? AND p.deleted_at IS NULL AND COALESCE(p.search_excluded, 0) = 0 AND (? = '' OR p.title LIKE ?) "
|
||||
"ORDER BY p.updated_at DESC LIMIT ?"
|
||||
)
|
||||
files = [
|
||||
@@ -922,7 +924,7 @@ def list_mentions(request: Request, q: str = ""):
|
||||
for r in conn.execute(
|
||||
"SELECT p.id, p.title, w.name AS ws_name "
|
||||
"FROM pages p LEFT JOIN workspaces w ON w.id=p.workspace_id "
|
||||
"WHERE p.deleted_at IS NULL AND (p.workspace_id IS ? OR p.workspace_id IS NULL) "
|
||||
"WHERE p.deleted_at IS NULL AND COALESCE(p.search_excluded, 0) = 0 AND (p.workspace_id IS ? OR p.workspace_id IS NULL) "
|
||||
"AND (? = '' OR p.title LIKE ?) ORDER BY p.updated_at DESC LIMIT ?",
|
||||
(ws, query, like, limit)
|
||||
).fetchall():
|
||||
@@ -939,7 +941,7 @@ def list_mentions(request: Request, q: str = ""):
|
||||
for r in conn.execute(
|
||||
"SELECT p.id, p.title, w.name AS ws_name FROM pages p "
|
||||
"LEFT JOIN workspaces w ON w.id=p.workspace_id "
|
||||
"WHERE p.deleted_at IS NULL AND (? = '' OR p.title LIKE ?) "
|
||||
"WHERE p.deleted_at IS NULL AND COALESCE(p.search_excluded, 0) = 0 AND (? = '' OR p.title LIKE ?) "
|
||||
"ORDER BY p.updated_at DESC LIMIT ?", (query, like, limit)
|
||||
).fetchall():
|
||||
docs.append({"type": "document", "icon": "📄", "id": r["id"],
|
||||
|
||||
@@ -107,6 +107,7 @@ def _recent_sidebar_pages(uid: int, limit: int = 8) -> list:
|
||||
FROM pages p
|
||||
LEFT JOIN workspaces w ON w.id = p.workspace_id
|
||||
WHERE p.deleted_at IS NULL AND p.parent_section != 'Trash'
|
||||
AND COALESCE(p.search_excluded, 0) = 0
|
||||
AND (w.owner_id = ? OR p.workspace_id IS NULL)
|
||||
AND REPLACE(COALESCE(p.content,''), ' ', '')
|
||||
NOT LIKE '%"type":"meeting"%'
|
||||
|
||||
@@ -222,7 +222,8 @@ async def list_workspace_projects(request: Request):
|
||||
builtin = []
|
||||
with get_conn() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, title FROM pages WHERE parent_id IS NULL AND collection_row_id IS NULL ORDER BY updated_at DESC LIMIT 20"
|
||||
"SELECT id, title FROM pages WHERE parent_id IS NULL AND collection_row_id IS NULL "
|
||||
"AND COALESCE(search_excluded, 0) = 0 ORDER BY updated_at DESC LIMIT 20"
|
||||
).fetchall()
|
||||
# A23 : une seule agrégation GROUP BY au lieu d'un COUNT par ligne.
|
||||
counts = {}
|
||||
|
||||
+14
-7
@@ -210,6 +210,12 @@ BASE_SELECT = (
|
||||
"p.parent_id, p.share_mode, p.parent_section, p.page_icon"
|
||||
)
|
||||
|
||||
# Pages-supports de templates (fd_templates._create_holder) : pages internes
|
||||
# marquées search_excluded=1 — « masquées, jamais listées ». Elles ne doivent
|
||||
# apparaître dans AUCUN onglet de la bibliothèque (une base fraîche en sème 99,
|
||||
# qui noieraient sinon recents/private/workspace sous leur updated_at = now).
|
||||
NO_HOLDER = "COALESCE(p.search_excluded, 0) = 0"
|
||||
|
||||
|
||||
# Lignes contenant un bloc AI Meeting Notes (formats JSON navigateur/Python).
|
||||
MEETING_BLOCK_LIKE = "REPLACE(COALESCE(p.content,''), ' ', '') LIKE '%\"type\":\"meeting\"%'"
|
||||
@@ -240,7 +246,7 @@ def library_meetings(
|
||||
" ORDER BY mt.id DESC LIMIT 1) AS meeting_summary "
|
||||
"FROM pages p LEFT JOIN workspaces w ON w.id = p.workspace_id "
|
||||
"WHERE p.deleted_at IS NULL AND (w.owner_id = ? OR p.workspace_id IS NULL) "
|
||||
f"AND {MEETING_BLOCK_LIKE}"
|
||||
f"AND {NO_HOLDER} AND {MEETING_BLOCK_LIKE}"
|
||||
)
|
||||
params: list = [uid]
|
||||
query, params = _apply_source_filter(query, params, source_type)
|
||||
@@ -275,7 +281,7 @@ def library_recents(
|
||||
source_type = "all"
|
||||
|
||||
uid = _get_user_id(request)
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE p.parent_section != 'Trash' AND p.deleted_at IS NULL AND {NO_MEETING_BLOCK}"
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE p.parent_section != 'Trash' AND p.deleted_at IS NULL AND {NO_HOLDER} AND {NO_MEETING_BLOCK}"
|
||||
params: list = []
|
||||
|
||||
# Hierarchical view: show only root-level pages at the top
|
||||
@@ -312,7 +318,8 @@ def library_favorites(
|
||||
"SELECT p.id, p.title, p.workspace, p.updated_at, p.content_format, "
|
||||
"p.parent_id, p.share_mode, p.parent_section, 1 as favorited "
|
||||
"FROM favorites f JOIN pages p ON p.id = f.page_id "
|
||||
"WHERE f.user_id = ? AND p.parent_section != 'Trash' AND p.deleted_at IS NULL"
|
||||
"WHERE f.user_id = ? AND p.parent_section != 'Trash' AND p.deleted_at IS NULL "
|
||||
f"AND {NO_HOLDER}"
|
||||
)
|
||||
params: list = [uid]
|
||||
if tree:
|
||||
@@ -379,7 +386,7 @@ def library_shared(
|
||||
|
||||
query = (
|
||||
f"{BASE_SELECT} FROM pages p "
|
||||
f"WHERE ({' OR '.join(conds)}) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL"
|
||||
f"WHERE ({' OR '.join(conds)}) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL AND {NO_HOLDER}"
|
||||
)
|
||||
if tree:
|
||||
query += " AND p.parent_id IS NULL"
|
||||
@@ -410,7 +417,7 @@ def library_published(
|
||||
|
||||
uid = _get_user_id(request)
|
||||
# v7.69.6 : les deux drapeaux (lignes historiques issues d'un seul chemin).
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE (p.is_published = 1 OR p.published = 1) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL"
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE (p.is_published = 1 OR p.published = 1) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL AND {NO_HOLDER}"
|
||||
params: list = []
|
||||
if tree:
|
||||
query += " AND p.parent_id IS NULL"
|
||||
@@ -435,7 +442,7 @@ def library_private(
|
||||
source_type = "all"
|
||||
|
||||
uid = _get_user_id(request)
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE p.parent_section = 'Private' AND p.deleted_at IS NULL AND {NO_MEETING_BLOCK}"
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE p.parent_section = 'Private' AND p.deleted_at IS NULL AND {NO_HOLDER} AND {NO_MEETING_BLOCK}"
|
||||
params: list = []
|
||||
if tree:
|
||||
query += " AND p.parent_id IS NULL"
|
||||
@@ -501,7 +508,7 @@ def library_workspace(
|
||||
source_type = "all"
|
||||
|
||||
uid = _get_user_id(request)
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE (p.workspace != '' OR p.workspace_id IS NOT NULL) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL AND {NO_MEETING_BLOCK}"
|
||||
query = f"{BASE_SELECT} FROM pages p WHERE (p.workspace != '' OR p.workspace_id IS NOT NULL) AND p.parent_section != 'Trash' AND p.deleted_at IS NULL AND {NO_HOLDER} AND {NO_MEETING_BLOCK}"
|
||||
params: list = []
|
||||
|
||||
if tree:
|
||||
|
||||
@@ -24,6 +24,7 @@ import contextlib
|
||||
import ipaddress
|
||||
import json
|
||||
import socket
|
||||
import sys
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
@@ -108,6 +109,14 @@ def mock_http(handler):
|
||||
from app.services.importers import url_fetch
|
||||
if getattr(url_fetch, "shared_client", None) is build:
|
||||
url_fetch.shared_client = _REAL_SHARED_CLIENT
|
||||
# Même fuite via og_fetcher : url_fetch l'importe paresseusement
|
||||
# (parse_og) PENDANT la fenêtre, et son `from … import shared_client`
|
||||
# fige alors `build` — le handler du test courant répondait ensuite
|
||||
# aux fetch_og_metadata des tests suivants du même worker xdist
|
||||
# (flake test_v55 : titre « Titre de la page » au lieu du sien).
|
||||
og_fetcher = sys.modules.get("app.services.og_fetcher")
|
||||
if og_fetcher is not None and getattr(og_fetcher, "shared_client", None) is build:
|
||||
og_fetcher.shared_client = _REAL_SHARED_CLIENT
|
||||
|
||||
|
||||
def run(coro):
|
||||
|
||||
@@ -396,7 +396,11 @@ def test_editor_front_end_wired(client):
|
||||
assert "gtTok" in src and "fd-wiki-chip" in src # chip render + reader
|
||||
assert "wiki/pages" in src and "wiki/titles" in src # picker + label resolution
|
||||
assert "WM={" in src and "fdpage:" in src # [[ picker module
|
||||
assert "_openTemplatePicker" in base and "page-templates" in base
|
||||
# Depuis la refonte templates (fc85481), le picker de base.html passe par
|
||||
# le registre unifié (/api/templates) ; le câblage « page-templates » vit
|
||||
# dans l'éditeur (fetch /board/api/page-templates) — chaque marqueur est
|
||||
# donc vérifié dans le fichier qui le porte réellement.
|
||||
assert "_openTemplatePicker" in base and "page-templates" in src
|
||||
assert "toggleLock" in src and "saveAsTemplate" in src
|
||||
css = pathlib.Path("static/css/app.css").read_text(encoding="utf-8")
|
||||
assert ".fd-wiki-chip" in css and ".fd-lock-banner" in css and ".tpl-picker-modal" in css
|
||||
|
||||
Reference in New Issue
Block a user