From c53c1e1572a7ebe3423dd0a638fd13dbd1fd0761 Mon Sep 17 00:00:00 2001 From: ZdenekSrotyr Date: Tue, 5 May 2026 00:01:22 +0200 Subject: [PATCH] fix(ui): admin pending-review banner on /corporate-memory (#176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /corporate-memory page filters status IN ('approved','mandatory') and showed no hint that pending items exist. With approval_mode set to 'review_queue' (the default in instance.yaml.example), every collection run would silently funnel new items into the pending bucket where no operator ever saw them. For admins (is_km_admin), the page now renders a banner above the stats bar: N pending items awaiting review — review them at /corporate-memory/admin Non-admins see no change (the route zeroes the count server-side before passing to the template, so the hint is never leaked). Tests: tests/test_corporate_memory_page.py. --- app/web/router.py | 10 +++- app/web/templates/corporate_memory.html | 17 ++++++ tests/test_corporate_memory_page.py | 71 +++++++++++++++++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 tests/test_corporate_memory_page.py diff --git a/app/web/router.py b/app/web/router.py index 2fd05fe..2b72f1e 100644 --- a/app/web/router.py +++ b/app/web/router.py @@ -602,6 +602,11 @@ async def corporate_memory( categories = sorted(set(i.get("category", "") for i in all_items if i.get("category"))) domains = sorted(set(i.get("domain", "") for i in all_items if i.get("domain"))) + # #176: surface the pending review queue to admins. Without this the + # main page silently filtered status='pending' items and operators had + # no breadcrumb to /corporate-memory/admin. + pending_count = sum(1 for i in all_items if i.get("status") == "pending") + # "My contributions" — items the caller authored. Personal items are # always visible to their author regardless of audience filtering; # this is the surface the user uses to mark/unmark `is_personal`. @@ -612,6 +617,7 @@ async def corporate_memory( item["upvotes"] = votes["upvotes"] item["downvotes"] = votes["downvotes"] + is_admin_view = is_user_admin(user["id"], conn) ctx = _build_context( request, user=user, knowledge_items=items, @@ -621,7 +627,7 @@ async def corporate_memory( domains=domains, stats={"total": len(all_items), "approved": len([i for i in all_items if i.get("status") == "approved"])}, user_votes={}, - is_km_admin=is_user_admin(user["id"], conn), + is_km_admin=is_admin_view, user_contributions=user_contributions, user_stats={"authored": len(user_contributions), "votes_given": 0}, # Template expects knowledge as object with .items and .total_pages @@ -630,6 +636,8 @@ async def corporate_memory( current_page=1, page=1, per_page=100, + # #176: pending banner is admin-only. + pending_review_count=pending_count if is_admin_view else 0, ) return templates.TemplateResponse(request, "corporate_memory.html", ctx) diff --git a/app/web/templates/corporate_memory.html b/app/web/templates/corporate_memory.html index 9011389..aa8cd53 100644 --- a/app/web/templates/corporate_memory.html +++ b/app/web/templates/corporate_memory.html @@ -566,6 +566,23 @@ {% include '_app_header.html' %}
+ {% if pending_review_count and pending_review_count > 0 %} + +
+ + + + + + + {{ pending_review_count }} pending item{{ 's' if pending_review_count != 1 else '' }} + awaiting review — + review them at /corporate-memory/admin + +
+ {% endif %}
diff --git a/tests/test_corporate_memory_page.py b/tests/test_corporate_memory_page.py new file mode 100644 index 0000000..838a024 --- /dev/null +++ b/tests/test_corporate_memory_page.py @@ -0,0 +1,71 @@ +"""GET /corporate-memory page rendering — pending banner contract. + +The page used to filter `status IN ('approved','mandatory')` with no hint +that a `pending` review queue exists. Operators who configured +`approval_mode='review_queue'` saw an empty page after every collection +run and had no breadcrumb to /corporate-memory/admin. Closes one of +five defects in #176. + +Contract: +- Admins see a banner when count(*) WHERE status='pending' > 0, + with a link to /corporate-memory/admin. +- Non-admins see no change to the page. +""" + +from __future__ import annotations + + +def _auth(token: str) -> dict: + return {"Authorization": f"Bearer {token}"} + + +def _seed_pending_item(item_id: str = "pending_item_1"): + from src.db import get_system_db + from src.repositories.knowledge import KnowledgeRepository + + conn = get_system_db() + repo = KnowledgeRepository(conn) + repo.create( + id=item_id, + title=f"Pending review item {item_id}", + content="awaiting admin triage", + category="workflow", + status="pending", + ) + conn.close() + + +class TestPendingBannerForAdmins: + def test_admin_sees_pending_banner_when_pending_items_exist(self, seeded_app): + _seed_pending_item("p_admin_1") + c = seeded_app["client"] + token = seeded_app["admin_token"] + resp = c.get("/corporate-memory", headers=_auth(token)) + assert resp.status_code == 200 + body = resp.text + # Banner must mention the pending count and link to the admin queue. + assert "pending" in body.lower() + assert "/corporate-memory/admin" in body + + def test_admin_no_banner_when_no_pending(self, seeded_app): + # Default seed has zero pending items. + c = seeded_app["client"] + token = seeded_app["admin_token"] + resp = c.get("/corporate-memory", headers=_auth(token)) + assert resp.status_code == 200 + body = resp.text + # The literal banner copy mentions "awaiting review"; absent when no + # pending items. + assert "awaiting review" not in body.lower() + + +class TestNonAdminNeverSeesPendingBanner: + def test_analyst_does_not_see_banner_even_with_pending_items(self, seeded_app): + _seed_pending_item("p_no_admin_1") + c = seeded_app["client"] + token = seeded_app["analyst_token"] + resp = c.get("/corporate-memory", headers=_auth(token)) + assert resp.status_code == 200 + body = resp.text + # Non-admin must not see the admin-only banner copy. + assert "awaiting review" not in body.lower()