Add Corporate Memory admin review queue UI (Phase 2)
Admin page at /corporate-memory/admin with three tabs: - Review Queue: pending items with approve/mandate/reject + batch ops - All Items: status filter, promote/demote/revoke actions - Audit Log: filterable action history table Features: - Keyboard shortcuts (j/k navigate, a/r/m = approve/reject/mandate) - Inline mandate form (mandatory reason + audience targeting) - Toast notifications on action success/error - Pending count badge on main Corporate Memory page - Matches existing visual design (CSS variables, card styles)
This commit is contained in:
parent
1318b74ff1
commit
e85d296b0a
4 changed files with 1664 additions and 11 deletions
|
|
@ -40,6 +40,7 @@ from .corporate_memory_service import (
|
||||||
vote as memory_vote,
|
vote as memory_vote,
|
||||||
is_km_admin,
|
is_km_admin,
|
||||||
get_governance_mode,
|
get_governance_mode,
|
||||||
|
get_groups as get_memory_groups,
|
||||||
approve_item,
|
approve_item,
|
||||||
reject_item,
|
reject_item,
|
||||||
mandate_item,
|
mandate_item,
|
||||||
|
|
@ -49,6 +50,7 @@ from .corporate_memory_service import (
|
||||||
get_pending_queue,
|
get_pending_queue,
|
||||||
get_audit_log,
|
get_audit_log,
|
||||||
migrate_existing_items,
|
migrate_existing_items,
|
||||||
|
VALID_STATUSES,
|
||||||
)
|
)
|
||||||
from .user_service import (
|
from .user_service import (
|
||||||
UserInfo,
|
UserInfo,
|
||||||
|
|
@ -1393,9 +1395,11 @@ def register_routes(app: Flask) -> None:
|
||||||
knowledge = get_knowledge(page=0, per_page=20)
|
knowledge = get_knowledge(page=0, per_page=20)
|
||||||
|
|
||||||
# Governance context for admin features
|
# Governance context for admin features
|
||||||
|
_is_admin = is_km_admin(email) if email else False
|
||||||
governance = {
|
governance = {
|
||||||
"mode": get_governance_mode(),
|
"mode": get_governance_mode(),
|
||||||
"is_km_admin": is_km_admin(email) if email else False,
|
"is_km_admin": _is_admin,
|
||||||
|
"pending_count": get_memory_stats().get("pending_count", 0) if _is_admin else 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
|
|
@ -1407,6 +1411,31 @@ def register_routes(app: Flask) -> None:
|
||||||
governance=governance,
|
governance=governance,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route("/corporate-memory/admin")
|
||||||
|
@login_required
|
||||||
|
@km_admin_required
|
||||||
|
def corporate_memory_admin():
|
||||||
|
"""Corporate Memory admin review queue page."""
|
||||||
|
user = session.get("user", {})
|
||||||
|
email = user.get("email", "")
|
||||||
|
|
||||||
|
stats = get_memory_stats()
|
||||||
|
groups = get_memory_groups()
|
||||||
|
|
||||||
|
# Build groups list for audience dropdown (name + member count)
|
||||||
|
groups_list = [
|
||||||
|
{"name": name, "members_count": len(g.get("members", []))}
|
||||||
|
for name, g in groups.items()
|
||||||
|
if isinstance(g, dict)
|
||||||
|
]
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
"corporate_memory_admin.html",
|
||||||
|
stats=stats,
|
||||||
|
groups=groups_list,
|
||||||
|
governance_mode=get_governance_mode(),
|
||||||
|
)
|
||||||
|
|
||||||
# ─────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────
|
||||||
# Activity Center routes
|
# Activity Center routes
|
||||||
# ─────────────────────────────────────────────────────────────────
|
# ─────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -1440,8 +1469,12 @@ def register_routes(app: Flask) -> None:
|
||||||
# Admin status filter (only km_admins can filter by status)
|
# Admin status filter (only km_admins can filter by status)
|
||||||
status = request.args.get("status")
|
status = request.args.get("status")
|
||||||
include_statuses = None
|
include_statuses = None
|
||||||
if status and is_km_admin(email):
|
if is_km_admin(email):
|
||||||
|
if status:
|
||||||
include_statuses = {status}
|
include_statuses = {status}
|
||||||
|
elif request.args.get("all_statuses", "").lower() == "true":
|
||||||
|
# Admin requesting all statuses (for admin "All Items" view)
|
||||||
|
include_statuses = set(VALID_STATUSES)
|
||||||
|
|
||||||
result = get_knowledge(
|
result = get_knowledge(
|
||||||
category=category,
|
category=category,
|
||||||
|
|
@ -1723,9 +1756,16 @@ def register_routes(app: Flask) -> None:
|
||||||
def corporate_memory_admin_config():
|
def corporate_memory_admin_config():
|
||||||
"""Get current governance configuration."""
|
"""Get current governance configuration."""
|
||||||
try:
|
try:
|
||||||
|
groups = get_memory_groups()
|
||||||
|
groups_list = [
|
||||||
|
{"name": name, "members_count": len(g.get("members", []))}
|
||||||
|
for name, g in groups.items()
|
||||||
|
if isinstance(g, dict)
|
||||||
|
]
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"governance_mode": get_governance_mode(),
|
"governance_mode": get_governance_mode(),
|
||||||
|
"groups": groups_list,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception("Error fetching governance config")
|
logger.exception("Error fetching governance config")
|
||||||
|
|
|
||||||
|
|
@ -104,6 +104,16 @@ def get_governance_mode() -> str | None:
|
||||||
return gov.get("distribution_mode", "hybrid")
|
return gov.get("distribution_mode", "hybrid")
|
||||||
|
|
||||||
|
|
||||||
|
def get_groups() -> dict:
|
||||||
|
"""Return the groups dict from instance config (public wrapper around _load_groups).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict mapping group name to group config (with 'members' list).
|
||||||
|
Empty dict if no groups configured.
|
||||||
|
"""
|
||||||
|
return _load_groups()
|
||||||
|
|
||||||
|
|
||||||
def get_approval_mode() -> str | None:
|
def get_approval_mode() -> str | None:
|
||||||
"""Return the approval mode, or None if legacy (no config)."""
|
"""Return the approval mode, or None if legacy (no config)."""
|
||||||
gov = _load_governance_config()
|
gov = _load_governance_config()
|
||||||
|
|
|
||||||
|
|
@ -437,6 +437,40 @@
|
||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Admin Link Button */
|
||||||
|
.admin-link-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-2);
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: var(--surface);
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--text-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-link-btn:hover {
|
||||||
|
background: var(--primary-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0 5px;
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
background: var(--warning);
|
||||||
|
color: white;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
}
|
||||||
|
|
||||||
/* Responsive */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.page-header {
|
.page-header {
|
||||||
|
|
@ -499,6 +533,18 @@
|
||||||
<h1>Corporate Memory</h1>
|
<h1>Corporate Memory</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div style="display: flex; align-items: center; gap: var(--space-4);">
|
||||||
|
{% if governance.is_km_admin %}
|
||||||
|
<a href="{{ url_for('corporate_memory_admin') }}" class="admin-link-btn">
|
||||||
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||||
|
</svg>
|
||||||
|
Admin Review
|
||||||
|
{% if governance.pending_count > 0 %}
|
||||||
|
<span class="pending-badge">{{ governance.pending_count }}</span>
|
||||||
|
{% endif %}
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
<div class="user-info-v2">
|
<div class="user-info-v2">
|
||||||
{% if session.user.picture %}
|
{% if session.user.picture %}
|
||||||
<img src="{{ session.user.picture }}" alt="Profile" class="avatar-v2">
|
<img src="{{ session.user.picture }}" alt="Profile" class="avatar-v2">
|
||||||
|
|
@ -509,6 +555,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ session.user.email }}
|
{{ session.user.email }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Stats Bar -->
|
<!-- Stats Bar -->
|
||||||
|
|
|
||||||
1556
webapp/templates/corporate_memory_admin.html
Normal file
1556
webapp/templates/corporate_memory_admin.html
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue