From 193fbd14ceff8e18e8c3c830a57e5d3abf96f965 Mon Sep 17 00:00:00 2001 From: Spark Date: Sun, 25 Oct 2026 22:38:53 +0000 Subject: [PATCH] fix(listes): exclure les pages-supports de templates des listes utilisateur MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Les pages-supports créées par TemplateService._create_holder (une par template, search_excluded=1, « masquées, jamais listées ») n'étaient exclues d'aucune liste : semées à l'init avec updated_at = now (99 sur une base fraîche), elles occupaient toutes les places des listes triées par récence (LIMIT 50/20/8) devant les vraies pages — cause racine de 7 échecs CI (library recents/private/workspace, sidebar recents, projets builtin, mentions agent), dont l'instabilité du test provenance (égalité de seconde avec le seed). Filtre COALESCE(search_excluded, 0) = 0 ajouté : onglets bibliothèque (meetings, recents, favorites, shared, published, private, workspace), sidebar Recents, projets builtin, scans de pages des mentions agent. --- app/routers/agent.py | 8 +++++--- app/routers/dashboard/_common.py | 1 + app/routers/dashboard/workspace.py | 3 ++- app/routers/library.py | 21 ++++++++++++++------- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/app/routers/agent.py b/app/routers/agent.py index c75d791..5a80f35 100644 --- a/app/routers/agent.py +++ b/app/routers/agent.py @@ -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"], diff --git a/app/routers/dashboard/_common.py b/app/routers/dashboard/_common.py index 15d0543..5a4a1a8 100644 --- a/app/routers/dashboard/_common.py +++ b/app/routers/dashboard/_common.py @@ -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"%' diff --git a/app/routers/dashboard/workspace.py b/app/routers/dashboard/workspace.py index 03ac2c5..5f3e956 100644 --- a/app/routers/dashboard/workspace.py +++ b/app/routers/dashboard/workspace.py @@ -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 = {} diff --git a/app/routers/library.py b/app/routers/library.py index 332801e..655d531 100644 --- a/app/routers/library.py +++ b/app/routers/library.py @@ -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: