"""FlowDeck v7.69.6 — Library AI Meeting Notes, Recents sidebar, Publish unifié. - ``GET /api/library/meetings`` : notes de réunion groupées par date côté client ; exclues des onglets recents/workspace/private. - Sidebar Recents : fichiers réellement récents (``_recent_sidebar_pages``), icônes workspace, notes de réunion exclues. - Publish : ``is_published`` + ``published`` maintenus ensemble — la page publiée apparaît dans la sidebar Published, la Library et le dialogue. """ from __future__ import annotations import json import secrets from conftest import anon_csrf # noqa: F401 from app.db import get_conn def _login(client): from app.auth.session import SessionManager login = f"v696_{secrets.token_hex(4)}" with get_conn() as conn: conn.execute( "INSERT INTO users (login, full_name, email, is_admin) VALUES (?, 'V696', ?, 0)", (login, f"{login}@test.com"), ) uid = conn.execute("SELECT id FROM users WHERE login=?", (login,)).fetchone()["id"] conn.commit() return SessionManager.create_session({"id": uid, "login": login}), uid def _cookies(session): return {"flowdeck_session": session} def _mkworkspace(uid, name="WS696"): with get_conn() as conn: cur = conn.execute("INSERT INTO workspaces (name, owner_id) VALUES (?, ?)", (name, uid)) conn.commit() return cur.lastrowid def _mkpage(ws_id, title, with_meeting=False, updated="2026-10-09 12:00:00"): content = json.dumps([{"id": "b1", "type": "meeting" if with_meeting else "paragraph", "content": ""}]) with get_conn() as conn: cur = conn.execute( "INSERT INTO pages (workspace, workspace_id, title, content, content_format," " parent_section, updated_at) VALUES ('t', ?, ?, ?, 'blocks', 'Private', ?)", (ws_id, title, content, updated)) conn.commit() return cur.lastrowid # ── Library meetings ───────────────────────────────────────────────────── def test_library_meetings_tab_lists_only_meeting_notes(client): session, uid = _login(client) ws = _mkworkspace(uid) mp = _mkpage(ws, "Sprint Review", True) _mkpage(ws, "Plain notes", False) r = client.get("/api/library/meetings", cookies=_cookies(session)) assert r.status_code == 200, r.text items = r.json()["items"] assert [i["id"] for i in items] == [mp] assert items[0]["is_meeting"] is True assert items[0]["url"] == f"/pages/{mp}" def test_library_browsing_tabs_hide_meeting_notes(client): session, uid = _login(client) ws = _mkworkspace(uid) _mkpage(ws, "Sprint Review", True, updated="2026-10-09 12:00:00") plain = _mkpage(ws, "Plain notes", False, updated="2026-10-09 13:00:00") for tab in ("recents", "workspace", "private"): r = client.get(f"/api/library/{tab}", cookies=_cookies(session)) assert r.status_code == 200, r.text ids = [i["id"] for i in r.json()["items"]] assert plain in ids assert not any(i.get("is_meeting") for i in r.json()["items"]) assert len([i for i in ids if isinstance(i, int)]) == len(ids) # ── Sidebar recents ────────────────────────────────────────────────────── def test_recent_sidebar_pages_excludes_meetings_and_uses_workspace_icons(client): from app.routers.dashboard._common import _recent_sidebar_pages session, uid = _login(client) ws = _mkworkspace(uid) _mkpage(ws, "Sprint Review", True) plain = _mkpage(ws, "Plain notes", False) recents = _recent_sidebar_pages(uid) ids = [e["id"] for e in recents] assert f"page/{plain}" in ids assert not any("Sprint" in e["name"] for e in recents) icons = {e["id"]: e["icon"] for e in recents} assert icons[f"page/{plain}"] == "edit" # même convention que le Workspace assert all(e["url"].startswith("/pages/") for e in recents) # ── Publish unifié ─────────────────────────────────────────────────────── def test_publish_sets_both_flags_and_lists(client): session, uid = _login(client) ws = _mkworkspace(uid) pid = _mkpage(ws, "Launch notes", False) r = client.post(f"/api/pages/{pid}/publish", cookies=_cookies(session)) assert r.status_code == 200, r.text assert r.json()["is_published"] is True with get_conn() as conn: row = conn.execute("SELECT is_published, published, publish_slug FROM pages" " WHERE id=?", (pid,)).fetchone() assert row["is_published"] == 1 and row["published"] == 1 assert row["publish_slug"] # Library Published + sidebar Published voient la page. r = client.get("/api/library/published", cookies=_cookies(session)) assert pid in [i["id"] for i in r.json()["items"]] from app.routers.board._common import _load_shared_sidebar_pages _made, _received, published, _shared = _load_shared_sidebar_pages(uid) assert any(p["db_id"] == pid for p in published) # Le dialogue (page_data) reflète l'état. r = client.get(f"/pages/{pid}", cookies=_cookies(session)) assert r.status_code == 200 assert '"published": true' in r.text # Unpublish nettoie les deux drapeaux. r = client.delete(f"/api/pages/{pid}/publish", cookies=_cookies(session)) assert r.json()["is_published"] is False with get_conn() as conn: row = conn.execute("SELECT is_published, published FROM pages WHERE id=?", (pid,)).fetchone() assert row["is_published"] == 0 and row["published"] == 0 r = client.get("/api/library/published", cookies=_cookies(session)) assert pid not in [i["id"] for i in r.json()["items"]] def test_share_dialog_publish_path_sets_both_flags_and_slug(client): session, uid = _login(client) ws = _mkworkspace(uid) pid = _mkpage(ws, "Dialog publish", False) r = client.post(f"/board/api/share/{pid}", json={"mode": "private", "published": True}, cookies=_cookies(session)) assert r.status_code == 200, r.text with get_conn() as conn: row = conn.execute("SELECT is_published, published, publish_slug FROM pages" " WHERE id=?", (pid,)).fetchone() assert row["is_published"] == 1 and row["published"] == 1 assert row["publish_slug"] r = client.get("/api/library/published", cookies=_cookies(session)) assert pid in [i["id"] for i in r.json()["items"]] def test_customize_panel_above_overlay(): from pathlib import Path css = (Path(__file__).resolve().parent.parent / "static" / "css" / "app.css").read_text(encoding="utf-8") panel = css.split(".sidebar-customize-panel")[1].split("}")[0] assert "z-index: 999" in panel def test_library_column_selectors_beat_row_rules(): """Show columns : les règles de visibilité doivent battre ``.lib-table-header .th`` et ``.lib-row .row-cell`` (0,2,0) — avant, seul ``:not(.show-editedtime)`` gagnait et seule Last edited time s'affichait/masquait (vérifié en direct au navigateur).""" from pathlib import Path html = (Path(__file__).resolve().parent.parent / "app" / "templates" / "library.html").read_text(encoding="utf-8") for cls in ("lib-col-author", "lib-col-editedby", "lib-col-source", "lib-col-visited"): assert f".lib-table div.{cls}" in html, cls # ── v7.69.6b : provenance, badges, favoris ───────────────────────────────── def _save_blocks(client, session, pid, title="T"): r = client.post(f"/board/api/pages/{pid}/blocks", json={"title": title, "blocks": [{"id": "b1", "type": "paragraph", "content": "hello"}]}, cookies=_cookies(session)) assert r.status_code == 200, r.text def test_library_provenance_columns(client): session, uid = _login(client) ws = _mkworkspace(uid) pid = _mkpage(ws, "Prov notes", False) _save_blocks(client, session, pid, "Prov notes") r = client.get("/api/library/recents", cookies=_cookies(session)) assert r.status_code == 200, r.text items = [i for i in r.json()["items"] if i["id"] == pid] assert len(items) == 1 it = items[0] assert it["author"], "Created by must resolve (versions, sinon owner)" assert it["edited_by"], "Last edited by must resolve" assert it["source_icon"] in ("file", "link", "external-link") assert it["visited_at"] == "", "not visited yet" # Lecture → visite tracée. assert client.get(f"/pages/{pid}", cookies=_cookies(session)).status_code == 200 r = client.get("/api/library/recents", cookies=_cookies(session)) it = [i for i in r.json()["items"] if i["id"] == pid][0] assert it["visited_at"], "Last visited time must be tracked on view" def test_favorites_use_workspace_icons(client): session, uid = _login(client) ws = _mkworkspace(uid) pid = _mkpage(ws, "Fav notes", False) with get_conn() as conn: conn.execute("INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,0)", (uid, pid)) conn.commit() r = client.get("/api/library/favorites", cookies=_cookies(session)) assert r.status_code == 200, r.text items = [i for i in r.json()["items"] if i["id"] == pid] assert len(items) == 1 assert items[0]["icon"] in ("folder", "file", "edit", "image") def test_local_workspace_tree_status_flags(client): from app.routers.dashboard._common import _build_tree_children session, uid = _login(client) ws = _mkworkspace(uid) pid = _mkpage(ws, "Flagged notes", False) with get_conn() as conn: conn.execute("INSERT INTO favorites (user_id, page_id, position) VALUES (?,?,0)", (uid, pid)) conn.execute("UPDATE pages SET is_published=1, published=1, publish_slug='x'," " share_mode='anyone' WHERE id=?", (pid,)) conn.commit() tree = _build_tree_children(conn, None, ws, uid) nodes = [n for n in tree if n["id"] == pid] assert len(nodes) == 1 assert nodes[0]["favorited"] is True assert nodes[0]["published"] is True assert nodes[0]["is_shared"] is True