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,
|
||||
is_km_admin,
|
||||
get_governance_mode,
|
||||
get_groups as get_memory_groups,
|
||||
approve_item,
|
||||
reject_item,
|
||||
mandate_item,
|
||||
|
|
@ -49,6 +50,7 @@ from .corporate_memory_service import (
|
|||
get_pending_queue,
|
||||
get_audit_log,
|
||||
migrate_existing_items,
|
||||
VALID_STATUSES,
|
||||
)
|
||||
from .user_service import (
|
||||
UserInfo,
|
||||
|
|
@ -1393,9 +1395,11 @@ def register_routes(app: Flask) -> None:
|
|||
knowledge = get_knowledge(page=0, per_page=20)
|
||||
|
||||
# Governance context for admin features
|
||||
_is_admin = is_km_admin(email) if email else False
|
||||
governance = {
|
||||
"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(
|
||||
|
|
@ -1407,6 +1411,31 @@ def register_routes(app: Flask) -> None:
|
|||
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
|
||||
# ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -1440,8 +1469,12 @@ def register_routes(app: Flask) -> None:
|
|||
# Admin status filter (only km_admins can filter by status)
|
||||
status = request.args.get("status")
|
||||
include_statuses = None
|
||||
if status and is_km_admin(email):
|
||||
include_statuses = {status}
|
||||
if is_km_admin(email):
|
||||
if 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(
|
||||
category=category,
|
||||
|
|
@ -1723,9 +1756,16 @@ def register_routes(app: Flask) -> None:
|
|||
def corporate_memory_admin_config():
|
||||
"""Get current governance configuration."""
|
||||
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({
|
||||
"ok": True,
|
||||
"governance_mode": get_governance_mode(),
|
||||
"groups": groups_list,
|
||||
})
|
||||
except Exception as e:
|
||||
logger.exception("Error fetching governance config")
|
||||
|
|
|
|||
|
|
@ -104,6 +104,16 @@ def get_governance_mode() -> str | None:
|
|||
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:
|
||||
"""Return the approval mode, or None if legacy (no config)."""
|
||||
gov = _load_governance_config()
|
||||
|
|
|
|||
|
|
@ -437,6 +437,40 @@
|
|||
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 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
|
|
@ -499,15 +533,28 @@
|
|||
<h1>Corporate Memory</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div class="user-info-v2">
|
||||
{% if session.user.picture %}
|
||||
<img src="{{ session.user.picture }}" alt="Profile" class="avatar-v2">
|
||||
{% else %}
|
||||
<div class="avatar-v2" style="background: var(--primary-light); display: flex; align-items: center; justify-content: center; font-weight: 600; color: var(--primary); font-size: 12px;">
|
||||
{{ session.user.email[:2].upper() }}
|
||||
</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 %}
|
||||
{{ session.user.email }}
|
||||
<div class="user-info-v2">
|
||||
{% if session.user.picture %}
|
||||
<img src="{{ session.user.picture }}" alt="Profile" class="avatar-v2">
|
||||
{% else %}
|
||||
<div class="avatar-v2" style="background: var(--primary-light); display: flex; align-items: center; justify-content: center; font-weight: 600; color: var(--primary); font-size: 12px;">
|
||||
{{ session.user.email[:2].upper() }}
|
||||
</div>
|
||||
{% endif %}
|
||||
{{ session.user.email }}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
|
|
|||
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