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 %}
+
+
+ {% 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()