ldv-pilot / ldv-frontend /admin.html
system's picture
system HF Staff
Sync from GitHub master (e9a60c67b365f40e7d83fe11705b5c7d807bccda)
47362a9 verified
Raw
History Blame Contribute Delete
94.3 kB
<!DOCTYPE html>
<html class="dark" lang="en">
<head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Admin Dashboard — Sydeco CRA</title>
<link rel="icon" type="image/svg+xml" href="images/sydeco_shield.svg">
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.12/dist/cdn.min.js" integrity="sha384-pb6hrQvo4s23cEUFtj0CZkzGE3jyK3pj26RIupXXxhSrrcUA/Cn0lZgcCrGH0t6L" crossorigin="anonymous"></script>
<script defer src="i18n.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;600;700;800&family=Plus+Jakarta+Sans:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-background": "#dee2f4",
"outline-variant": "#4d4635",
"background": "#0e131f",
"on-error": "#690005",
"on-secondary-fixed": "#2d1600",
"secondary-fixed": "#ffdcbf",
"secondary": "#f1bc8c",
"primary": "#d4af37",
"inverse-on-surface": "#2b303d",
"on-primary": "#101828",
"on-primary-container": "#554300",
"on-secondary-fixed-variant": "#633f19",
"surface-tint": "#d4af37",
"on-tertiary-fixed-variant": "#414754",
"outline": "#99907c",
"inverse-surface": "#dee2f4",
"on-tertiary-container": "#3f4553",
"on-secondary-container": "#e2ae7f",
"primary-container": "#d4af37",
"tertiary": "#c9cedf",
"surface-container-highest": "#303542",
"on-tertiary-fixed": "#161c27",
"on-secondary": "#492905",
"primary-fixed": "#ffe088",
"tertiary-fixed-dim": "#c1c6d7",
"on-surface-variant": "#d0c5af",
"inverse-primary": "#735c00",
"surface-container": "#1b1f2c",
"surface-variant": "#303542",
"surface-container-high": "#252a37",
"surface-container-lowest": "#090e1a",
"on-primary-fixed-variant": "#574500",
"tertiary-container": "#adb3c3",
"tertiary-fixed": "#dde2f3",
"secondary-container": "#66411b",
"on-error-container": "#ffdad6",
"surface-bright": "#343946",
"error": "#ffb4ab",
"surface": "#0e131f",
"surface-container-low": "#161b28",
"on-primary-fixed": "#241a00",
"secondary-fixed-dim": "#f1bc8c",
"surface-dim": "#0e131f",
"on-tertiary": "#2a303d",
"on-surface": "#dee2f4",
"primary-fixed-dim": "#d4af37",
"error-container": "#93000a"
},
"borderRadius": {
"DEFAULT": "4px",
"md": "8px",
"lg": "12px",
"xl": "16px",
"full": "9999px"
},
"spacing": {
"margin-desktop": "64px",
"margin-tablet": "32px",
"margin-mobile": "16px",
"base": "8px",
"gutter": "24px",
"container-max": "1440px"
},
"fontFamily": {
"display": ["Playfair Display", "serif"],
"body": ["Inter", "Plus Jakarta Sans", "sans-serif"],
"display-lg": ["Playfair Display"],
"body-md": ["Inter"],
"headline-lg-mobile": ["Playfair Display"],
"headline-md": ["Playfair Display"],
"label-sm": ["Inter"],
"headline-lg": ["Playfair Display"],
"label-md": ["Inter"],
"body-lg": ["Inter"]
},
"fontSize": {
"display-lg": ["48px", {"lineHeight": "1.1", "letterSpacing": "-0.02em", "fontWeight": "700"}],
"body-md": ["16px", {"lineHeight": "1.5", "fontWeight": "400"}],
"headline-lg-mobile": ["28px", {"lineHeight": "1.2", "fontWeight": "600"}],
"headline-md": ["24px", {"lineHeight": "1.3", "fontWeight": "600"}],
"label-sm": ["12px", {"lineHeight": "1.4", "fontWeight": "500"}],
"headline-lg": ["32px", {"lineHeight": "1.2", "fontWeight": "600"}],
"label-md": ["14px", {"lineHeight": "1.4", "letterSpacing": "0.05em", "fontWeight": "600"}],
"body-lg": ["18px", {"lineHeight": "1.6", "fontWeight": "400"}]
}
},
},
}
</script>
<style>
:root {
--bg-surface: rgba(27,31,44,0.75);
--bg-surface-elevated: rgba(37,42,55,0.85);
--border-color: rgba(212,175,55,0.1);
--color-low: #039855; --color-low-bg: rgba(3,152,85,0.12);
--color-medium: #DC6803; --color-medium-bg: rgba(220,104,3,0.12);
--color-high: #D92D20; --color-high-bg: rgba(217,45,32,0.12);
--color-critical: #7A271A; --color-critical-bg: rgba(122,39,26,0.12);
--transition-fast: 0.15s cubic-bezier(0.16,1,0.3,1);
}
body {
background-color: #0e131f;
color: #dee2f4;
font-family: 'Inter', sans-serif;
overflow-x: hidden;
}
.font-editorial {
font-family: 'Playfair Display', serif;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
.obsidian-panel {
background: #161b28;
border: 1px solid rgba(212, 175, 55, 0.15);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
}
.gold-button {
background: #d4af37;
color: #101828;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.gold-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 20px rgba(212, 175, 55, 0.3);
filter: brightness(1.1);
}
.gold-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.glass-card {
background: rgba(14, 19, 31, 0.4);
backdrop-filter: blur(20px);
border: 1px solid rgba(212, 175, 55, 0.1);
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0,0,0,0.35);
}
.reports-table {
width: 100%;
border-collapse: collapse;
font-size: 0.85rem;
text-align: left;
}
.reports-table th {
background: rgba(48, 53, 66, 0.45);
padding: 12px 16px;
font-weight: 700;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: #9ca3af;
border-bottom: 1px solid rgba(212, 175, 55, 0.1);
}
.reports-table td {
padding: 12px 16px;
border-bottom: 1px solid rgba(212, 175, 55, 0.1);
color: #dee2f4;
vertical-align: middle;
}
.reports-table tbody tr.row-clickable {
cursor: pointer;
transition: background var(--transition-fast);
}
.reports-table tbody tr.row-clickable:hover {
background: rgba(242, 202, 80, 0.03);
}
.reports-table tbody tr:last-child td {
border-bottom: none;
}
.tbl-pill {
display: inline-block;
padding: 3px 10px;
border-radius: 12px;
font-size: 0.68rem;
font-weight: 700;
letter-spacing: 0.03em;
text-transform: uppercase;
}
.tbl-pill-LOW { background: var(--color-low-bg); color: #039855; border: 1px solid rgba(6,95,70,0.2); }
.tbl-pill-MEDIUM { background: var(--color-medium-bg); color: #DC6803; border: 1px solid rgba(180,83,9,0.2); }
.tbl-pill-HIGH { background: var(--color-high-bg); color: #D92D20; border: 1px solid rgba(153,27,27,0.2); }
.tbl-pill-CRITICAL { background: var(--color-critical-bg); color: #ffdad6; border: 1px solid rgba(127,29,29,0.2); }
.tbl-pill-PENDING { background: rgba(212,175,55,0.15); color: #d4af37; border: 1px solid rgba(212,175,55,0.3); }
.tbl-pill-FAILED { background: rgba(153,27,27,0.15); color: #D92D20; border: 1px solid rgba(153,27,27,0.3); }
.score-cell {
font-family: 'Playfair Display', serif;
font-weight: 700;
font-size: 0.95rem;
}
.score-cell.risk-LOW { color: #039855; }
.score-cell.risk-MEDIUM { color: #DC6803; }
.score-cell.risk-HIGH { color: #D92D20; }
.score-cell.risk-CRITICAL { color: #ffb4ab; }
.mono-badge {
font-family: monospace;
font-size: 0.7rem;
padding: 2px 6px;
border-radius: 4px;
background: rgba(48, 53, 66, 0.5);
border: 1px solid rgba(212, 175, 55, 0.1);
color: #9ca3af;
}
.fill-LOW { background-color: var(--color-low); }
.fill-MEDIUM { background-color: var(--color-medium); }
.fill-HIGH { background-color: var(--color-high); }
.fill-CRITICAL { background-color: var(--color-critical); }
.spinner-ring {
width: 36px;
height: 36px;
border: 3px solid rgba(212,175,55,0.15);
border-top-color:#d4af37;
border-radius:50%;
animation:spin 0.9s linear infinite;
}
@keyframes spin { to{transform:rotate(360deg)} }
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(31, 41, 55, 0.5);
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #D4AF37;
border-radius: 2px;
}
/* Navigation Link active styling */
.nav-link-active {
color: #d4af37 !important;
background-color: rgba(212, 175, 55, 0.1) !important;
border-left: 3px solid #d4af37;
}
</style>
</head>
<body class="bg-background text-on-background font-body selection:bg-primary/30" x-data="adminDashboard()" x-init="load()">
<button type="button" data-lang-toggle onclick="i18n.toggleLang()"
class="fixed top-3 right-3 z-[60] px-3 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider border border-primary/40 bg-surface-container/90 text-primary hover:bg-primary hover:text-on-primary transition-colors backdrop-blur-sm"
aria-label="Toggle language / Ganti bahasa">ID</button>
<!-- TopNavBar -->
<header class="fixed top-0 left-0 lg:left-64 right-0 z-40 h-16 flex items-center justify-between gap-3 px-4 sm:px-6 border-b border-outline-variant/10 shadow-sm" style="background:rgba(14,19,31,0.9);backdrop-filter:blur(20px);">
<div class="flex items-center gap-2 min-w-0 text-xs font-semibold text-on-surface-variant/60 font-body">
<button type="button" class="lg:hidden flex items-center justify-center w-11 h-11 -ml-2 rounded text-on-surface-variant hover:text-primary transition-colors shrink-0" aria-label="Open navigation menu" onclick="toggleSidebar()">
<span class="material-symbols-outlined">menu</span>
</button>
<a href="/admin" class="hover:text-primary transition-colors text-on-surface-variant/80 hidden sm:inline shrink-0" style="text-decoration:none;" data-i18n="nav.sidebar.admin_portal">Admin Portal</a>
<span class="material-symbols-outlined text-[16px] text-on-surface-variant/30 hidden sm:inline shrink-0">chevron_right</span>
<span class="text-primary font-bold truncate" x-text="$store.lang && getTabTitle()">System Administration</span>
</div>
<a href="/" class="text-sm font-bold font-body uppercase tracking-wider px-3 sm:px-5 py-2 rounded bg-primary text-on-primary hover:brightness-110 transition-all shadow-md shadow-primary/10 whitespace-nowrap shrink-0" style="text-decoration:none;"><span data-i18n="admin.header.new_analysis_prefix">+ New</span><span class="hidden sm:inline" data-i18n="admin.header.new_analysis_suffix"> Analysis</span></a>
</header>
<!-- SideNavBar -->
<div id="sidebar-overlay" class="fixed inset-0 bg-black/60 z-[45] hidden lg:hidden" onclick="toggleSidebar()"></div>
<aside id="sidebar" class="fixed left-0 top-0 bottom-0 w-64 flex flex-col py-6 bg-surface-container/95 lg:bg-surface-container/40 backdrop-blur-2xl border-r border-outline-variant/10 shadow-2xl z-50 -translate-x-full lg:translate-x-0 transition-transform duration-200">
<div class="px-6 mb-8 flex items-center gap-3 pt-4">
<div class="w-10 h-10 rounded bg-primary/20 flex items-center justify-center border border-primary/30 shrink-0">
<svg class="h-6 w-6" viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M100 20L40 50V100C40 137.01 65.59 171.41 100 180C134.41 171.41 160 137.01 160 100V50L100 20Z" stroke="#D4AF37" stroke-width="8" stroke-linejoin="round"/>
<path d="M100 60V140" stroke="#D4AF37" stroke-width="8" stroke-linecap="round"/>
<path d="M70 80L130 80" stroke="#D4AF37" stroke-width="8" stroke-linecap="round"/>
<path d="M80 120L120 120" stroke="#D4AF37" stroke-width="8" stroke-linecap="round"/>
</svg>
</div>
<div>
<p class="font-editorial text-primary leading-tight text-[18px]">Sydeco CRA</p>
<p class="font-body text-[10px] text-on-surface-variant uppercase tracking-widest opacity-70 font-semibold">LightML Engine</p>
</div>
</div>
<!-- Sidebar Menu tabs (Section 10.J Admin screens navigation) -->
<nav class="flex-grow flex flex-col gap-1 font-body">
<!-- Core Operations -->
<div class="px-6 mb-2 mt-4 text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/40" data-i18n="nav.sidebar.core_operations">Core Operations</div>
<a class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all" href="/">
<span class="material-symbols-outlined">upload_file</span>
<span class="text-sm font-semibold" data-i18n="nav.new_analysis">New Analysis</span>
</a>
<button class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all text-left"
:class="activeTab === 'overview' ? 'nav-link-active' : ''" @click="activeTab = 'overview'; window.location.hash = 'overview'">
<span class="material-symbols-outlined">analytics</span>
<span class="text-sm font-semibold" data-i18n="nav.sidebar.overview">Overview</span>
</button>
<button class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all text-left"
:class="activeTab === 'history' ? 'nav-link-active' : ''" @click="activeTab = 'history'; window.location.hash = 'history'; fetchHistory()">
<span class="material-symbols-outlined">folder_open</span>
<span class="text-sm font-semibold" data-i18n="nav.sidebar.case_history">Case History</span>
</button>
<!-- Management -->
<div class="px-6 mb-2 mt-4 text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/40" x-show="role === 'admin' || role === 'manager'" data-i18n="nav.sidebar.management">Management</div>
<button class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all text-left"
x-show="role === 'admin' || role === 'manager'"
:class="activeTab === 'team' ? 'nav-link-active' : ''" @click="activeTab = 'team'; window.location.hash = 'team'">
<span class="material-symbols-outlined">group</span>
<span class="text-sm font-semibold" data-i18n="nav.sidebar.team_management">Team Management</span>
</button>
<button class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all text-left"
x-show="role === 'admin'"
:class="activeTab === 'orgs' ? 'nav-link-active' : ''" @click="activeTab = 'orgs'; window.location.hash = 'orgs'">
<span class="material-symbols-outlined">corporate_fare</span>
<span class="text-sm font-semibold" data-i18n="nav.sidebar.organizations">Organizations</span>
</button>
<!-- Compliance & Health -->
<div class="px-6 mb-2 mt-4 text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/40" x-show="role === 'admin' || role === 'reviewer'" data-i18n="nav.sidebar.compliance_health">Compliance & Health</div>
<a class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all" href="/citations"
x-show="role === 'admin' || role === 'reviewer'">
<span class="material-symbols-outlined">verified</span>
<span class="text-sm font-semibold" data-i18n="nav.citation_library">Citation Library</span>
</a>
<button class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all text-left"
x-show="role === 'admin'"
:class="activeTab === 'audits' ? 'nav-link-active' : ''" @click="activeTab = 'audits'; window.location.hash = 'audits'">
<span class="material-symbols-outlined">receipt_long</span>
<span class="text-sm font-semibold" data-i18n="nav.sidebar.audit_logs">Audit Logs</span>
</button>
<button class="flex items-center gap-3 px-4 py-3 mx-2 text-on-surface-variant hover:bg-surface-variant/30 hover:text-primary rounded-lg transition-all text-left"
x-show="role === 'admin'"
:class="activeTab === 'status' ? 'nav-link-active' : ''" @click="activeTab = 'status'; window.location.hash = 'status'">
<span class="material-symbols-outlined">dns</span>
<span class="text-sm font-semibold" data-i18n="nav.sidebar.system_health">System Health</span>
</button>
</nav>
<div class="px-6 mt-auto font-body">
<div class="flex flex-col gap-3 pt-6 border-t border-outline-variant/10">
<a class="flex items-center gap-3 text-on-surface-variant hover:text-primary transition-colors text-xs" href="/account" style="text-decoration:none;">
<span class="material-symbols-outlined text-[20px]">shield_person</span>
<span data-i18n="nav.sidebar.account_security">Account & Security</span>
</a>
<a class="flex items-center gap-3 text-on-surface-variant hover:text-primary transition-colors text-xs" href="/logout" style="text-decoration:none;">
<span class="material-symbols-outlined text-[20px]">logout</span>
<span data-i18n="nav.secure_logout">Secure Logout</span>
</a>
</div>
</div>
</aside>
<!-- Main Content Area -->
<div class="lg:ml-64 min-h-screen">
<main class="max-w-[1200px] mx-auto px-4 sm:px-6 pb-12 pt-24">
<!-- Error / System Alerts -->
<div class="mb-4 bg-red-950/20 border border-red-500/30 rounded px-4 py-3 text-red-300 text-sm font-body"
x-show="errorMsg" x-text="errorMsg" style="display:none;" x-transition></div>
<div class="mb-4 bg-teal-950/20 border border-teal-500/30 rounded px-4 py-3 text-teal-300 text-sm font-body"
x-show="successMsg" x-text="successMsg" style="display:none;" x-transition></div>
<!-- Loading State -->
<div class="flex flex-col items-center gap-4 py-20 text-on-surface-variant" x-show="loading">
<div class="spinner-ring"></div>
<p class="font-body text-sm" data-i18n="admin.loading.aggregating">Aggregating system statistics…</p>
</div>
<!-- MAIN TABS CONTAINER -->
<div x-show="!loading" x-cloak style="display:none;" :style="{ display: loading ? 'none' : 'block' }">
<!-- ==================== TAB: OVERVIEW ==================== -->
<div x-show="activeTab === 'overview'" class="space-y-8 animate-fade-in">
<div class="mb-4">
<h1 class="font-editorial text-3xl mb-2 text-on-background" data-i18n="admin.overview.title">System Overview</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.overview.subtitle">Monitor historical document assessments, risk ratings, and pipeline operations.</p>
</div>
<!-- Telemetry Counters Grid -->
<div class="grid grid-cols-2 lg:grid-cols-4 gap-gutter">
<div class="glass-card p-6 flex flex-col gap-2 relative overflow-hidden group">
<span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant font-body" data-i18n="admin.overview.stat_documents_label">Stored Documents</span>
<span class="font-editorial text-4xl font-bold" x-text="stats.total_documents ?? 0"></span>
<span class="text-xs text-on-surface-variant font-body" data-i18n="admin.overview.stat_documents_desc">Retained active agreements</span>
</div>
<div class="glass-card p-6 flex flex-col gap-2 relative overflow-hidden group">
<span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant font-body" data-i18n="admin.overview.stat_analyses_label">Analyses Executed</span>
<span class="font-editorial text-4xl font-bold" x-text="stats.total_analyses ?? 0"></span>
<span class="text-xs text-on-surface-variant font-body" data-i18n="admin.overview.stat_analyses_desc">Completed model jobs</span>
</div>
<div class="glass-card p-6 flex flex-col gap-2 relative overflow-hidden group">
<span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant font-body" data-i18n="admin.overview.stat_risk_label">Average Risk Index</span>
<span class="font-editorial text-4xl font-bold text-primary" x-text="stats.average_risk_score ?? 0"></span>
<span class="text-xs text-on-surface-variant font-body" data-i18n="admin.overview.stat_risk_desc">Combined L3 score metric</span>
</div>
<div class="glass-card p-6 flex flex-col gap-2 relative overflow-hidden group">
<span class="text-xs font-bold uppercase tracking-widest text-on-surface-variant font-body" data-i18n="admin.overview.stat_risky_label">Risky Agreements</span>
<span class="font-editorial text-4xl font-bold text-error" x-text="getRiskyCount()"></span>
<span class="text-xs text-on-surface-variant font-body" data-i18n="admin.overview.stat_risky_desc">Flagged HIGH or CRITICAL</span>
</div>
</div>
<!-- Risk Distribution & Recent Reports -->
<div class="grid grid-cols-1 lg:grid-cols-[360px_1fr] gap-6">
<!-- Left: Risk Distribution Chart -->
<div class="glass-card p-6 min-w-0">
<h2 class="font-editorial font-bold text-base mb-5 flex items-center gap-2">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:#d4af37;stroke-width:2px;">
<path d="M21.21 15.89A10 10 0 1 1 8 2.83" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 12A10 10 0 0 0 12 2v10z" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span data-i18n="admin.overview.distribution_title">Risk Class Distribution</span>
</h2>
<div class="flex flex-col gap-5 font-body">
<template x-for="lbl in ['LOW','MEDIUM','HIGH','CRITICAL']">
<div class="flex flex-col gap-2">
<div class="flex justify-between items-center text-sm font-semibold">
<span class="text-on-surface-variant" x-text="lbl"></span>
<span class="mono-badge" x-text="stats.distribution ? stats.distribution[lbl] || 0 : 0"></span>
</div>
<div class="h-3 rounded-full overflow-hidden" style="background:rgba(48, 53, 66, 0.45); border:1px solid rgba(212, 175, 55, 0.1);">
<div class="h-full rounded-full transition-all duration-1000" :class="'fill-' + lbl"
:style="{ width: getPercentage(stats.distribution ? stats.distribution[lbl] || 0 : 0) + '%' }"></div>
</div>
</div>
</template>
</div>
</div>
<!-- Right: Recent Reports List -->
<div class="glass-card p-6 min-w-0">
<h2 class="font-editorial font-bold text-base mb-5 flex items-center gap-2">
<svg viewBox="0 0 24 24" style="width:18px;height:18px;fill:none;stroke:#d4af37;stroke-width:2px;">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8zM14 2v6h6M16 13H8M16 17H8M10 9H8" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<span data-i18n="admin.overview.recent_title">Recent Risk Reports</span>
</h2>
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30 custom-scrollbar">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.overview.col_ref_id">Ref ID</th>
<th data-i18n="admin.overview.col_filename">Filename</th>
<th data-i18n="admin.overview.col_type">Type</th>
<th data-i18n="admin.overview.col_jurisdiction">Jurisdiction</th>
<th data-i18n="admin.overview.col_score">Score</th>
<th data-i18n="admin.overview.col_risk_rating">Risk Rating</th>
<th data-i18n="admin.overview.col_analyzed">Analyzed</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-if="recent.length === 0">
<tr><td colspan="7" style="text-align:center;color:#9ca3af;font-style:italic;padding:24px;" data-i18n="admin.overview.empty_reports">No legal reports processed in history.</td></tr>
</template>
<template x-for="row in recent">
<tr class="row-clickable" @click="window.location='/result/' + row.id">
<td style="font-family:monospace;color:#9ca3af;" x-text="row.id ? row.id.substring(0,8) : '—'"></td>
<td style="font-weight:600;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dee2f4;" :title="row.original_filename" x-text="row.original_filename"></td>
<td><span class="mono-badge" x-text="(row.file_type || '').replace('.','').toUpperCase()"></span></td>
<td x-text="row.jurisdiction || '—'"></td>
<td class="score-cell" :class="'risk-' + (row.status === 'completed' ? row.risk_label : (row.status === 'failed' ? 'FAILED' : 'PENDING'))"
x-text="row.status === 'completed' ? (row.risk_score ?? '—') : '—'"></td>
<td>
<span class="tbl-pill flex items-center justify-center gap-1.5 w-max"
:style="{ minWidth: '85px' }"
:class="'tbl-pill-' + (row.status === 'completed' ? row.risk_label : (row.status === 'failed' ? 'FAILED' : 'PENDING'))">
<span class="material-symbols-outlined text-[12px] animate-spin" x-show="row.status === 'processing' || row.status === 'queued'">sync</span>
<span x-text="row.status === 'completed' ? row.risk_label : (row.status === 'failed' ? 'FAILED' : (row.status === 'processing' ? 'PROCESSING' : 'QUEUED'))"></span>
</span>
</td>
<td style="color:#9ca3af;" x-text="timeAgo(row.analyzed_at)"></td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
</div>
<!-- ==================== TAB: CASE HISTORY ==================== -->
<div x-show="activeTab === 'history'" class="space-y-6 animate-fade-in" style="display:none;">
<div>
<h1 class="font-editorial text-3xl mb-1 text-on-background" data-i18n="admin.history.title">Case History</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.history.subtitle">Search, filter, and organize historical contract risk reports.</p>
</div>
<!-- Advanced Search Filters -->
<div class="glass-card p-6 space-y-4">
<h2 class="font-editorial font-bold text-base text-primary flex items-center gap-2">
<span class="material-symbols-outlined">filter_alt</span>
<span data-i18n="admin.history.filter_title">Filter Documents</span>
</h2>
<form @submit.prevent="historyFilters.offset = 0; fetchHistory()" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- General Search -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.search_label">General Search</label>
<input type="text" x-model="historyFilters.search" placeholder="Filename, error..." data-i18n-placeholder="admin.history.search_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm"/>
</div>
<!-- Client -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.client_label">Client Name</label>
<input type="text" x-model="historyFilters.client" placeholder="e.g. Acme Corp" data-i18n-placeholder="admin.history.client_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm"/>
</div>
<!-- Case Folder -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.folder_label">Case / Folder</label>
<input type="text" x-model="historyFilters.case_folder" placeholder="e.g. Q3 NDA" data-i18n-placeholder="admin.history.folder_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm"/>
</div>
<!-- Document Type -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.doc_type_label">Document Type</label>
<select x-model="historyFilters.type"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm">
<option value="" data-i18n="admin.history.type_all">All Types</option>
<option value="nda" data-i18n="admin.history.type_nda">Non-Disclosure Agreement</option>
<option value="employment" data-i18n="admin.history.type_employment">Employment Contract</option>
<option value="service" data-i18n="admin.history.type_service">Service Agreement</option>
<option value="software" data-i18n="admin.history.type_software">Software Licensing</option>
<option value="generic" data-i18n="admin.history.type_generic">Generic/Other</option>
</select>
</div>
<!-- Status -->
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.status_label">Job Status</label>
<select x-model="historyFilters.status"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm">
<option value="" data-i18n="admin.history.status_all">All Statuses</option>
<option value="completed" data-i18n="admin.history.status_completed">Completed</option>
<option value="processing" data-i18n="admin.history.status_processing">Processing</option>
<option value="queued" data-i18n="admin.history.status_queued">Queued</option>
<option value="failed" data-i18n="admin.history.status_failed">Failed</option>
</select>
</div>
<!-- Score Ranges -->
<div class="grid grid-cols-2 gap-2">
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.min_score_label">Min Score</label>
<input type="number" x-model="historyFilters.min_score" min="0" max="100" placeholder="0"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm"/>
</div>
<div class="flex flex-col gap-1.5">
<label class="text-[10px] font-bold uppercase tracking-widest text-on-surface-variant/70" data-i18n="admin.history.max_score_label">Max Score</label>
<input type="number" x-model="historyFilters.max_score" min="0" max="100" placeholder="100"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-3 py-2 rounded text-sm"/>
</div>
</div>
<!-- Action Buttons -->
<div class="md:col-span-3 flex justify-end gap-3 pt-2">
<button type="button" @click="historyFilters = { search: '', client: '', case_folder: '', type: '', status: '', min_score: '', max_score: '', limit: 25, offset: 0 }; fetchHistory()"
class="px-4 py-2 border border-outline-variant/40 rounded text-xs font-semibold" data-i18n="admin.history.clear_button">Clear Filters</button>
<button type="submit" class="gold-button px-6 py-2 rounded text-xs font-bold uppercase tracking-wider" data-i18n="admin.history.search_button">Search</button>
</div>
</form>
</div>
<!-- History Table -->
<div class="glass-card p-6">
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30 custom-scrollbar">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.history.col_ref_id">Ref ID</th>
<th data-i18n="admin.history.col_filename">Filename</th>
<th data-i18n="admin.history.col_client">Client</th>
<th data-i18n="admin.history.col_folder">Folder/Case</th>
<th data-i18n="admin.history.col_score">Score</th>
<th data-i18n="admin.history.col_status">Status</th>
<th data-i18n="admin.history.col_reviewed">Reviewed</th>
<th data-i18n="admin.history.col_analyzed">Analyzed</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-if="historyResults.length === 0">
<tr><td colspan="8" style="text-align:center;color:#9ca3af;font-style:italic;padding:24px;" data-i18n="admin.history.empty">No historical analyses match your query.</td></tr>
</template>
<template x-for="row in historyResults">
<tr class="row-clickable" @click="window.location='/result/' + row.id">
<td style="font-family:monospace;color:#9ca3af;" x-text="row.id ? row.id.substring(0,8) : '—'"></td>
<td style="font-weight:600;max-width:200px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#dee2f4;" :title="row.original_filename" x-text="row.original_filename"></td>
<td x-text="row.client || '—'"></td>
<td x-text="row.case_folder || '—'"></td>
<td class="score-cell" :class="'risk-' + (row.status === 'completed' ? row.risk_label : (row.status === 'failed' ? 'FAILED' : 'PENDING'))"
x-text="row.status === 'completed' ? (row.risk_score ?? '—') : '—'"></td>
<td>
<span class="tbl-pill flex items-center justify-center gap-1.5 w-max"
:style="{ minWidth: '85px' }"
:class="'tbl-pill-' + (row.status === 'completed' ? row.risk_label : (row.status === 'failed' ? 'FAILED' : 'PENDING'))">
<span x-text="row.status === 'completed' ? row.risk_label : (row.status === 'failed' ? 'FAILED' : (row.status === 'processing' ? 'PROCESSING' : 'QUEUED'))"></span>
</span>
</td>
<td>
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider border"
:class="row.review_status !== 'unreviewed' ? 'bg-teal-950/40 text-teal-400 border-teal-500/20' : 'bg-surface-container-highest text-on-surface-variant border-outline-variant/10'"
x-text="row.review_status !== 'unreviewed' ? row.review_status : 'no'"></span>
</td>
<td style="color:#9ca3af;" x-text="timeAgo(row.analyzed_at)"></td>
</tr>
</template>
</tbody>
</table>
</div>
<!-- Pagination controls -->
<div class="flex justify-between items-center mt-5 text-xs text-on-surface-variant font-body">
<button type="button" :disabled="historyFilters.offset === 0" @click="historyFilters.offset = Math.max(0, historyFilters.offset - historyFilters.limit); fetchHistory()"
class="px-4 py-2 border border-outline-variant/20 rounded disabled:opacity-40 disabled:cursor-not-allowed" data-i18n="admin.history.prev_page">Previous Page</button>
<button type="button" :disabled="historyResults.length < historyFilters.limit" @click="historyFilters.offset += historyFilters.limit; fetchHistory()"
class="px-4 py-2 border border-outline-variant/20 rounded disabled:opacity-40 disabled:cursor-not-allowed" data-i18n="admin.history.next_page">Next Page</button>
</div>
</div>
</div>
<!-- ==================== TAB: TEAM MANAGEMENT ==================== -->
<div x-show="activeTab === 'team'" class="space-y-6 animate-fade-in" style="display:none;">
<div class="flex justify-between items-center">
<div>
<h1 class="font-editorial text-3xl mb-1 text-on-background" data-i18n="admin.team.title">Team Management</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.team.subtitle">Provision users, assign organizational access roles, and audit security credentials.</p>
</div>
<button class="gold-button px-5 py-2.5 rounded font-bold text-xs uppercase tracking-wider flex items-center gap-2"
@click="showCreateUserModal = true">
<span class="material-symbols-outlined text-[16px]">person_add</span>
<span data-i18n="admin.team.create_button">Create User</span>
</button>
</div>
<!-- Users list -->
<div class="glass-card p-6">
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.team.col_id">ID</th>
<th data-i18n="admin.team.col_email">Email / Identity</th>
<th data-i18n="admin.team.col_role">Role</th>
<th data-i18n="admin.team.col_org_id">Org ID</th>
<th data-i18n="admin.team.col_mfa_setup">MFA Setup</th>
<th data-i18n="admin.team.col_downloads">Downloads</th>
<th data-i18n="admin.team.col_status">Status</th>
<th style="text-align:right;" data-i18n="admin.team.col_actions">Actions</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-for="usr in users" :key="usr.id">
<tr>
<td style="font-family:monospace;color:#9ca3af;" x-text="usr.id"></td>
<td class="font-bold text-on-surface" x-text="usr.email"></td>
<td>
<select :value="usr.role" @change="changeUserRole(usr.id, $event.target.value)"
class="bg-surface-container border border-outline-variant/20 rounded text-xs px-2 py-1 text-on-surface focus:outline-none">
<option value="analyst" data-i18n="admin.team.role_analyst">Analyst</option>
<option value="reviewer" data-i18n="admin.team.role_reviewer">Reviewer</option>
<option value="manager" data-i18n="admin.team.role_manager">Manager</option>
<option value="viewer" data-i18n="admin.team.role_viewer">Viewer</option>
<option value="admin" data-i18n="admin.team.role_admin">Administrator</option>
</select>
</td>
<td x-text="usr.org_id"></td>
<td>
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase"
:class="usr.mfa_enabled ? 'bg-green-950/40 text-green-400 border border-green-500/20' : 'bg-amber-950/40 text-amber-300 border border-amber-500/20'"
x-text="$store.lang && (usr.mfa_enabled ? i18n.t('admin.team.mfa_active') : i18n.t('admin.team.mfa_unset'))"></span>
</td>
<td>
<select :value="usr.download_disabled ? '1' : '0'" @change="changeUserDownload(usr.id, $event.target.value)"
class="bg-surface-container border border-outline-variant/20 rounded text-xs px-2 py-1 text-on-surface focus:outline-none">
<option value="0" data-i18n="admin.team.download_enabled">Enabled</option>
<option value="1" data-i18n="admin.team.download_disabled">Disabled</option>
</select>
</td>
<td>
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase"
:class="usr.active ? 'bg-teal-950/40 text-teal-400 border border-teal-500/20' : 'bg-red-950/40 text-red-400 border border-red-500/20'"
x-text="$store.lang && (usr.active ? i18n.t('admin.team.status_active') : i18n.t('admin.team.status_suspended'))"></span>
</td>
<td style="text-align:right;" class="space-x-1">
<button class="px-2 py-1 rounded bg-surface-container border border-outline-variant/30 text-[10px] font-bold uppercase tracking-wider text-on-surface hover:border-primary/50 transition-colors"
@click="resetUserMfa(usr.id)" data-i18n="admin.team.reset_mfa_button">Reset MFA</button>
<button class="px-2 py-1 rounded bg-surface-container border border-outline-variant/30 text-[10px] font-bold uppercase tracking-wider text-on-surface hover:border-primary/50 transition-colors"
@click="revokeUserSession(usr.email)" data-i18n="admin.team.revoke_session_button">Revoke Session</button>
<button class="px-2 py-1 rounded border text-[10px] font-bold uppercase tracking-wider transition-colors"
:class="usr.active ? 'bg-red-950/20 hover:bg-red-900/40 text-red-400 border-red-500/20' : 'bg-teal-950/20 hover:bg-teal-900/40 text-teal-400 border-teal-500/20'"
x-text="$store.lang && (usr.active ? i18n.t('admin.team.suspend_button') : i18n.t('admin.team.activate_button'))"
@click="toggleUserStatus(usr.id, usr.active)"></button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== TAB: ORGANIZATIONS ==================== -->
<div x-show="activeTab === 'orgs'" class="space-y-6 animate-fade-in" style="display:none;">
<div class="flex justify-between items-center">
<div>
<h1 class="font-editorial text-3xl mb-1 text-on-background" data-i18n="admin.orgs.title">Organizations</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.orgs.subtitle">Provision corporate tenants, check storage retention rules, and define scan limitations.</p>
</div>
<button class="gold-button px-5 py-2.5 rounded font-bold text-xs uppercase tracking-wider flex items-center gap-2"
@click="showCreateOrgModal = true">
<span class="material-symbols-outlined text-[16px]">domain_add</span>
<span data-i18n="admin.orgs.create_button">Create Org</span>
</button>
</div>
<!-- Orgs list -->
<div class="glass-card p-6">
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.orgs.col_org_id">Org ID</th>
<th data-i18n="admin.orgs.col_name">Corporate Tenant Name</th>
<th data-i18n="admin.orgs.col_retention">Data Retention Policy</th>
<th data-i18n="admin.orgs.col_mfa_enforcement">MFA Enforcement</th>
<th data-i18n="admin.orgs.col_scan_limits">Monthly Scan Limits</th>
<th data-i18n="admin.orgs.col_status">Status</th>
<th style="text-align:right;" data-i18n="admin.orgs.col_save">Save Configurations</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-for="org in organizations" :key="org.id">
<tr>
<td style="font-family:monospace;color:#9ca3af;" x-text="org.id"></td>
<td class="font-bold text-on-surface" x-text="org.name"></td>
<td>
<select :value="org.retention_days || 30" @change="tempRetention[org.id] = $event.target.value"
class="bg-surface-container border border-outline-variant/20 rounded text-xs px-2 py-1 text-on-surface focus:outline-none">
<option value="7" data-i18n="admin.orgs.retention_7">7-Day Ephemeral Purge</option>
<option value="15" data-i18n="admin.orgs.retention_15">15-Day Auto-Purge</option>
<option value="30" data-i18n="admin.orgs.retention_30">30-Day Storage Lock</option>
<option value="90" data-i18n="admin.orgs.retention_90">90-Day Standard Log</option>
</select>
</td>
<td>
<button type="button"
class="px-2 py-0.5 rounded text-[10px] font-bold uppercase border transition-colors"
:class="org.mfa_required ? 'bg-teal-950/40 text-teal-400 border-teal-500/20' : 'bg-surface-container border-outline-variant/30 text-on-surface-variant hover:border-primary/40'"
@click="toggleOrgMfaRequired(org)"
x-text="$store.lang && (org.mfa_required ? i18n.t('admin.orgs.mfa_enforced') : i18n.t('admin.orgs.mfa_optional'))"></button>
</td>
<td>
<select class="bg-surface-container border border-outline-variant/20 rounded text-xs px-2 py-1 text-on-surface focus:outline-none">
<option value="50" data-i18n="admin.orgs.scan_50">50 scans / mo</option>
<option value="100" selected data-i18n="admin.orgs.scan_100">100 scans / mo</option>
<option value="500" data-i18n="admin.orgs.scan_500">500 scans / mo</option>
<option value="9999" data-i18n="admin.orgs.scan_unlimited">Unlimited Scans</option>
</select>
</td>
<td>
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-teal-950/40 text-teal-400 border border-teal-500/20" data-i18n="admin.orgs.status_active">Active</span>
</td>
<td style="text-align:right;">
<button class="gold-button px-4 py-1.5 rounded text-[10px] font-bold uppercase tracking-wider"
@click="saveOrgRetention(org.id)" data-i18n="admin.orgs.update_button">Update Policy</button>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== TAB: CITATION LIBRARY ==================== -->
<div x-show="activeTab === 'citations'" class="space-y-6 animate-fade-in" style="display:none;">
<div>
<h1 class="font-editorial text-3xl mb-1 text-on-background" data-i18n="admin.citations.title">Citation Library workspace</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.citations.subtitle">Review, verify, reject, or supersede draft legal source citations mapped by standard rules.</p>
</div>
<!-- Citations Review Workspace -->
<div class="glass-card p-6 space-y-6">
<h2 class="font-editorial text-lg font-bold text-primary border-b border-outline-variant/10 pb-2" data-i18n="admin.citations.pending_title">Pending Draft Citations</h2>
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.citations.col_finding_id">Finding ID</th>
<th data-i18n="admin.citations.col_jurisdiction">Jurisdiction</th>
<th data-i18n="admin.citations.col_article_ref">Article Reference</th>
<th data-i18n="admin.citations.col_source_desc">Legal Source Description</th>
<th data-i18n="admin.citations.col_verifier_note">Verifier Note</th>
<th data-i18n="admin.citations.col_status">Status</th>
<th style="text-align:right;" data-i18n="admin.citations.col_decision">Decision Workspace</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-for="cite in draftCitations" :key="cite.finding_id + cite.jurisdiction">
<tr>
<td style="font-family:monospace;color:#dee2f4;" x-text="cite.finding_id"></td>
<td class="font-semibold" x-text="cite.jurisdiction"></td>
<td class="font-bold text-primary" x-text="cite.article"></td>
<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis;" x-text="cite.source"></td>
<td style="max-width:150px;overflow:hidden;text-overflow:ellipsis;color:#9ca3af;" x-text="cite.note"></td>
<td>
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-amber-950/40 text-amber-300 border border-amber-500/20"
x-text="cite.status"></span>
</td>
<td style="text-align:right;" class="space-x-1">
<button class="px-2.5 py-1 rounded bg-teal-950/20 hover:bg-teal-900/40 text-teal-400 border border-teal-500/20 text-[10px] font-bold uppercase transition-colors"
@click="verifyCitation(cite)" data-i18n="admin.citations.verify_button">Verify</button>
<button class="px-2.5 py-1 rounded bg-red-950/20 hover:bg-red-900/40 text-red-400 border border-red-500/20 text-[10px] font-bold uppercase transition-colors"
@click="rejectCitation(cite)" data-i18n="admin.citations.reject_button">Reject</button>
<button class="px-2.5 py-1 rounded bg-surface-container border border-outline-variant/30 text-[10px] font-bold uppercase text-on-surface hover:border-primary/50 transition-colors"
@click="supersedeCitation(cite)" data-i18n="admin.citations.supersede_button">Supersede</button>
<button class="px-2.5 py-1 rounded bg-surface-container border border-outline-variant/30 text-[10px] font-bold uppercase text-on-surface hover:border-primary/50 transition-colors"
@click="viewCitationHistory(cite)" data-i18n="admin.citations.history_button">History</button>
</td>
</tr>
</template>
<template x-if="draftCitations.length === 0">
<tr><td colspan="7" style="text-align:center;color:#9ca3af;font-style:italic;padding:24px;" data-i18n="admin.citations.empty_draft">No draft citations pending verification.</td></tr>
</template>
</tbody>
</table>
</div>
</div>
<!-- Verified Citations library overview -->
<div class="glass-card p-6 space-y-6">
<h2 class="font-editorial text-lg font-bold text-primary border-b border-outline-variant/10 pb-2" data-i18n="admin.citations.verified_title">Verified Citation Index</h2>
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.citations.col_finding_id">Finding ID</th>
<th data-i18n="admin.citations.col_jurisdiction">Jurisdiction</th>
<th data-i18n="admin.citations.col_article">Article</th>
<th data-i18n="admin.citations.col_legal_code_ref">Legal Code Reference</th>
<th data-i18n="admin.citations.col_status">Status</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-for="cite in verifiedCitations" :key="cite.finding_id + cite.jurisdiction">
<tr>
<td style="font-family:monospace;color:#9ca3af;" x-text="cite.finding_id"></td>
<td x-text="cite.jurisdiction"></td>
<td class="font-semibold text-teal-400" x-text="cite.article"></td>
<td x-text="cite.source"></td>
<td>
<span class="px-2 py-0.5 rounded text-[10px] font-bold uppercase bg-teal-950/40 text-teal-400 border border-teal-500/20" data-i18n="admin.citations.status_verified">Verified</span>
</td>
</tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== TAB: AUDIT LOGS ==================== -->
<div x-show="activeTab === 'audits'" class="space-y-6 animate-fade-in" style="display:none;">
<div class="flex justify-between items-center">
<div>
<h1 class="font-editorial text-3xl mb-1 text-on-background" data-i18n="admin.audits.title">System Audit Log</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.audits.subtitle">Filter, search, and export security event records for local institutional auditing.</p>
</div>
<button class="btn btn-outline text-xs uppercase font-bold tracking-wider flex items-center gap-2"
@click="exportAuditLogs()">
<span class="material-symbols-outlined text-[16px]">download</span>
<span data-i18n="admin.audits.export_button">Export Log (CSV)</span>
</button>
</div>
<!-- Audit filters bar -->
<div class="flex flex-wrap gap-4 p-4 bg-surface-container-low border border-outline-variant/15 rounded-md text-xs font-body">
<div class="flex-grow min-w-[200px] relative flex items-center">
<span class="material-symbols-outlined text-[18px] text-on-surface-variant/40 absolute left-3">search</span>
<input type="text" x-model="auditFilters.search" placeholder="Search logs by action or details…" data-i18n-placeholder="admin.audits.search_placeholder"
class="w-full bg-surface-container border border-outline-variant/20 rounded pl-10 pr-3 py-2 text-xs text-on-surface focus:outline-none focus:border-primary/50 focus:ring-0"/>
</div>
<div>
<select x-model="auditFilters.action" class="bg-surface-container border border-outline-variant/20 rounded px-3 py-2 text-xs text-on-surface focus:outline-none">
<option value="all" data-i18n="admin.audits.action_all">All Actions</option>
<option value="user.create">user.create</option>
<option value="user.suspend">user.suspend</option>
<option value="user.mfa_reset">user.mfa_reset</option>
<option value="org.create">org.create</option>
<option value="org.retention_change">org.retention_change</option>
<option value="cite.verify">cite.verify</option>
</select>
</div>
<div>
<input type="date" x-model="auditFilters.date" class="bg-surface-container border border-outline-variant/20 rounded px-3 py-2 text-xs text-on-surface focus:outline-none"/>
</div>
<button class="px-4 py-2 border border-outline-variant/30 text-on-surface-variant hover:text-primary hover:border-primary/50 rounded transition-colors"
@click="clearAuditFilters()" data-i18n="admin.audits.reset_button">Reset</button>
</div>
<!-- Audits table list -->
<div class="glass-card p-6">
<div class="overflow-x-auto rounded-lg border border-outline-variant/10 bg-surface-container-lowest/30">
<table class="reports-table">
<thead>
<tr class="font-body text-xs text-on-surface-variant">
<th data-i18n="admin.audits.col_timestamp">Timestamp</th>
<th data-i18n="admin.audits.col_action">Action</th>
<th data-i18n="admin.audits.col_operator_id">Operator ID</th>
<th data-i18n="admin.audits.col_org_id">Org ID</th>
<th data-i18n="admin.audits.col_resource_target">Resource Target</th>
<th data-i18n="admin.audits.col_ip">IP Address</th>
<th data-i18n="admin.audits.col_detail">Detail Summary</th>
</tr>
</thead>
<tbody class="font-body text-xs">
<template x-for="log in filteredAudits">
<tr>
<td style="color:#9ca3af;" x-text="formatDate(log.timestamp)"></td>
<td class="font-bold text-primary" x-text="log.action"></td>
<td style="font-family:monospace;" x-text="'User #' + log.user_id"></td>
<td x-text="log.org_id"></td>
<td style="font-family:monospace;" x-text="log.resource_id || '—'"></td>
<td x-text="log.ip"></td>
<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;" x-text="log.detail || '—'"></td>
</tr>
</template>
<template x-if="filteredAudits.length === 0">
<tr><td colspan="7" style="text-align:center;color:#9ca3af;font-style:italic;padding:24px;" data-i18n="admin.audits.empty">No matching audit events logged.</td></tr>
</template>
</tbody>
</table>
</div>
</div>
</div>
<!-- ==================== TAB: SYSTEM STATUS ==================== -->
<div x-show="activeTab === 'status'" class="space-y-6 animate-fade-in" style="display:none;">
<div>
<h1 class="font-editorial text-3xl mb-1 text-on-background" data-i18n="admin.status.title">System Health status</h1>
<p class="font-body text-on-surface-variant text-sm" data-i18n="admin.status.subtitle">Monitor local encryption cores, rate limit configs, backup schedules, and active ML models.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 font-body text-sm">
<!-- Health Card -->
<div class="glass-card p-6 space-y-4">
<h2 class="font-editorial text-base font-bold text-primary border-b border-outline-variant/10 pb-2" data-i18n="admin.status.overview_title">Status overview</h2>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span data-i18n="admin.status.sovereign_health_label">Sovereign health:</span>
<span class="px-2 py-0.5 rounded text-xs font-bold uppercase tracking-wider"
:class="healthData.status === 'healthy' ? 'bg-teal-950/40 text-teal-400 border border-teal-500/20' : 'bg-red-950/40 text-red-400 border border-red-500/20'"
x-text="healthData.status || 'unknown'"></span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.database_label">Localized Database:</span>
<span class="text-on-surface font-semibold" x-text="healthData.checks ? healthData.checks.database : '—'"></span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.datasets_label">Datasets verification:</span>
<span class="text-on-surface font-semibold" x-text="healthData.checks ? healthData.checks.datasets : '—'"></span>
</div>
</div>
</div>
<!-- Encryption & Security status -->
<div class="glass-card p-6 space-y-4">
<h2 class="font-editorial text-base font-bold text-primary border-b border-outline-variant/10 pb-2" data-i18n="admin.status.encryption_title">Encryption Core</h2>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span data-i18n="admin.status.aes_label">AES-256 state:</span>
<span class="px-2 py-0.5 rounded text-xs font-bold uppercase border"
:class="healthData.encryption && healthData.encryption.enabled ? 'bg-teal-950/40 text-teal-400 border-teal-500/20' : 'bg-red-950/40 text-red-400 border-red-500/20'"
x-text="$store.lang && (healthData.encryption && healthData.encryption.enabled ? i18n.t('admin.status.aes_configured') : i18n.t('admin.status.aes_not_configured'))"></span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.storage_node_label">Storage Encryption Node:</span>
<span class="text-on-surface-variant" data-i18n="admin.status.storage_node_value">PT SYDECO Sovereign Memory Module</span>
</div>
</div>
</div>
<!-- Model telemetry -->
<div class="glass-card p-6 space-y-4">
<h2 class="font-editorial text-base font-bold text-primary border-b border-outline-variant/10 pb-2" data-i18n="admin.status.models_title">Active PyTorch Models</h2>
<div class="space-y-3 text-xs">
<div class="flex justify-between items-center">
<span data-i18n="admin.status.l2_label">L2 DistilBERT Classifier:</span>
<span class="font-semibold text-teal-400" x-text="$store.lang && (healthData.layer2_distilbert ? i18n.t('admin.status.l2_ready') : i18n.t('admin.status.l2_unavailable'))"></span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.l4_label">L4 Qwen Reasoning:</span>
<span class="font-semibold text-teal-400" x-text="$store.lang && (healthData.layer4_qwen ? i18n.t('admin.status.l4_available') : i18n.t('admin.status.l4_disabled'))"></span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.mlp_label">Sydeco MLP Network:</span>
<span class="font-semibold text-teal-400" x-text="$store.lang && (healthData.sydeco_mlp ? i18n.t('admin.status.mlp_online') : i18n.t('admin.status.mlp_offline'))"></span>
</div>
</div>
</div>
<!-- Backend Rate Limit & Backup status -->
<div class="glass-card p-6 space-y-4">
<h2 class="font-editorial text-base font-bold text-primary border-b border-outline-variant/10 pb-2" data-i18n="admin.status.limits_title">Limits & Backups</h2>
<div class="space-y-3">
<div class="flex justify-between items-center">
<span data-i18n="admin.status.rate_limiter_label">API Rate Limiter:</span>
<span class="text-on-surface font-semibold" data-i18n="admin.status.rate_limiter_value">Active (Max 20 req/min)</span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.last_backup_label">Last System Backup:</span>
<span class="text-on-surface font-semibold">2026-06-29T23:59:59Z</span>
</div>
<div class="flex justify-between items-center">
<span data-i18n="admin.status.backup_sync_label">Backup Node Sync:</span>
<span class="text-teal-400 font-bold" data-i18n="admin.status.backup_sync_value">Synced</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CREATE USER MODAL -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4 font-body" x-show="showCreateUserModal" style="display:none;" x-transition>
<div class="obsidian-panel w-full max-w-md p-8 rounded-lg space-y-6">
<header>
<h3 class="font-editorial text-xl text-on-surface font-bold" data-i18n="admin.modals.create_user.title">Create User Account</h3>
<p class="text-xs text-on-surface-variant mt-1" data-i18n="admin.modals.create_user.subtitle">Configure credentials, roles, and corporate tenant associations.</p>
</header>
<div class="space-y-4 text-xs font-body">
<div class="flex flex-col gap-1">
<label class="font-semibold text-on-surface-variant uppercase tracking-wider" data-i18n="admin.modals.create_user.email_label">Email Identity</label>
<input type="email" x-model="createUserForm.email" placeholder="user@company.com" data-i18n-placeholder="admin.modals.create_user.email_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-4 py-2.5 rounded text-sm"/>
</div>
<div class="flex flex-col gap-1">
<label class="font-semibold text-on-surface-variant uppercase tracking-wider" data-i18n="admin.modals.create_user.password_label">Secure password</label>
<input type="password" x-model="createUserForm.password" placeholder="••••••••" data-i18n-placeholder="admin.modals.create_user.password_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-4 py-2.5 rounded text-sm"/>
</div>
<div class="flex flex-col gap-1">
<label class="font-semibold text-on-surface-variant uppercase tracking-wider" data-i18n="admin.modals.create_user.role_label">Assign Role</label>
<select x-model="createUserForm.role" class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-4 py-2.5 rounded text-sm focus:outline-none">
<option value="analyst" data-i18n="admin.team.role_analyst">Analyst</option>
<option value="reviewer" data-i18n="admin.team.role_reviewer">Reviewer</option>
<option value="manager" data-i18n="admin.team.role_manager">Manager</option>
<option value="viewer" data-i18n="admin.team.role_viewer">Viewer</option>
<option value="admin" data-i18n="admin.team.role_admin">Administrator</option>
</select>
</div>
<div class="flex flex-col gap-1">
<label class="font-semibold text-on-surface-variant uppercase tracking-wider" data-i18n="admin.modals.create_user.org_id_label">Corporate Organization ID</label>
<input type="number" x-model.number="createUserForm.org_id" placeholder="1" data-i18n-placeholder="admin.modals.create_user.org_id_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-4 py-2.5 rounded text-sm"/>
</div>
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-outline-variant/10">
<button type="button" class="px-5 py-2.5 rounded border border-outline-variant/40 hover:bg-surface-container text-xs font-semibold text-on-surface-variant transition-colors"
@click="showCreateUserModal = false" data-i18n="admin.modals.create_user.cancel_button">Cancel</button>
<button type="button" class="gold-button px-6 py-2.5 rounded text-xs font-bold transition-all"
@click="createUser()" data-i18n="admin.modals.create_user.submit_button">Create User</button>
</div>
</div>
</div>
<!-- CREATE ORG MODAL -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4 font-body" x-show="showCreateOrgModal" style="display:none;" x-transition>
<div class="obsidian-panel w-full max-w-md p-8 rounded-lg space-y-6">
<header>
<h3 class="font-editorial text-xl text-on-surface font-bold" data-i18n="admin.modals.create_org.title">Create Corporate Tenant</h3>
<p class="text-xs text-on-surface-variant mt-1" data-i18n="admin.modals.create_org.subtitle">Register a new sovereign corporate organization domain.</p>
</header>
<div class="space-y-4 text-xs font-body">
<div class="flex flex-col gap-1">
<label class="font-semibold text-on-surface-variant uppercase tracking-wider" data-i18n="admin.modals.create_org.name_label">Corporate Tenant Name</label>
<input type="text" x-model="createOrgForm.name" placeholder="Company Name Inc." data-i18n-placeholder="admin.modals.create_org.name_placeholder"
class="bg-surface-container-lowest border border-outline-variant/30 text-on-surface px-4 py-2.5 rounded text-sm"/>
</div>
</div>
<div class="flex justify-end gap-3 pt-4 border-t border-outline-variant/10">
<button type="button" class="px-5 py-2.5 rounded border border-outline-variant/40 hover:bg-surface-container text-xs font-semibold text-on-surface-variant transition-colors"
@click="showCreateOrgModal = false" data-i18n="admin.modals.create_org.cancel_button">Cancel</button>
<button type="button" class="gold-button px-6 py-2.5 rounded text-xs font-bold transition-all"
@click="createOrg()" data-i18n="admin.modals.create_org.submit_button">Create Tenant</button>
</div>
</div>
</div>
<!-- CITATION HISTORY MODAL -->
<div class="fixed inset-0 z-50 flex items-center justify-center bg-background/80 backdrop-blur-sm p-4 font-body" x-show="showHistoryModal" style="display:none;" x-transition>
<div class="obsidian-panel w-full max-w-lg p-8 rounded-lg space-y-6">
<header>
<h3 class="font-editorial text-xl text-on-surface font-bold" data-i18n="admin.modals.citation_history.title">Citation Revision History</h3>
<p class="text-xs text-on-surface-variant mt-1" x-text="selectedCitation ? (selectedCitation.finding_id + ' / ' + selectedCitation.jurisdiction) : ''"></p>
</header>
<div class="space-y-4 text-xs font-body leading-relaxed max-h-60 overflow-y-auto custom-scrollbar pr-2">
<div class="flex flex-col gap-3">
<div class="border-l-2 border-primary pl-4 py-2 space-y-1">
<div class="flex justify-between font-bold text-on-surface">
<span data-i18n="admin.modals.citation_history.status_verified">Status: verified</span>
<span class="text-on-surface-variant/40">2026-06-30 08:30</span>
</div>
<p class="text-on-surface-variant text-[11px]" x-text="selectedCitation ? ('Article referenced: ' + selectedCitation.article) : ''"></p>
<p class="text-[10px] text-teal-400 font-semibold" data-i18n="admin.modals.citation_history.verified_note">Transitioned to verified baseline by System Administrator.</p>
</div>
<div class="border-l-2 border-outline-variant/30 pl-4 py-2 space-y-1">
<div class="flex justify-between font-bold text-on-surface-variant">
<span data-i18n="admin.modals.citation_history.status_draft">Status: draft</span>
<span class="text-on-surface-variant/40">2026-06-28 14:15</span>
</div>
<p class="text-on-surface-variant text-[11px]" data-i18n="admin.modals.citation_history.draft_note">Draft provision parsed via machine extraction.</p>
</div>
</div>
</div>
<div class="flex justify-end pt-4 border-t border-outline-variant/10">
<button type="button" class="gold-button px-6 py-2.5 rounded text-xs font-bold transition-all"
@click="showHistoryModal = false" data-i18n="admin.modals.citation_history.close_button">Close History</button>
</div>
</div>
</div>
</main>
</div>
<script>
function adminDashboard() {
return {
loading: true,
activeTab: 'overview', // 'overview', 'team', 'orgs', 'citations', 'audits', 'status'
role: 'analyst',
usage: null,
stats: {},
recent: [],
users: [],
organizations: [],
citations: [],
audits: [],
healthData: {},
tempRetention: {},
historyResults: [],
historyFilters: { search: '', client: '', case_folder: '', type: '', status: '', min_score: '', max_score: '', limit: 25, offset: 0 },
// Modals & form bindings
showCreateUserModal: false,
showCreateOrgModal: false,
showHistoryModal: false,
selectedCitation: null,
createUserForm: { email: '', password: '', role: 'analyst', org_id: 1 },
createOrgForm: { name: '' },
// Messages
errorMsg: '',
successMsg: '',
// Filters
auditFilters: { search: '', action: 'all', date: '' },
getTabTitle() {
const titles = {
overview: i18n.t('nav.sidebar.overview'),
history: i18n.t('nav.sidebar.case_history'),
team: i18n.t('nav.sidebar.team_management'),
orgs: i18n.t('nav.sidebar.organizations'),
citations: i18n.t('admin.tab_titles.citation_verification'),
audits: i18n.t('nav.sidebar.audit_logs'),
status: i18n.t('nav.sidebar.system_health')
};
return titles[this.activeTab] || i18n.t('nav.sidebar.overview');
},
async load() {
this.loading = true;
this.errorMsg = '';
this.successMsg = '';
try {
const roleResp = await fetch('/api/v1/mfa/status');
if (roleResp.status === 401) { window.location.href = '/login'; return; }
const roleData = await roleResp.json();
this.role = roleData.role || 'analyst';
// Filter activeTab based on user role
const hash = window.location.hash.substring(1);
let validTabs = ['overview', 'history'];
if (this.role === 'admin') {
validTabs = ['overview', 'history', 'team', 'orgs', 'citations', 'audits', 'status'];
} else if (this.role === 'manager') {
validTabs = ['overview', 'history', 'team'];
}
if (validTabs.includes(hash)) {
this.activeTab = hash;
} else {
this.activeTab = 'overview';
window.location.hash = 'overview';
}
const fetches = [
fetch('/api/v1/stats'),
fetch('/api/v1/recent?limit=20')
];
if (this.role === 'admin') {
fetches.push(
fetch('/api/v1/admin/users'),
fetch('/api/v1/admin/organizations'),
fetch('/api/v1/citations'),
fetch('/api/v1/audit?limit=150'),
fetch('/health')
);
} else if (this.role === 'manager') {
fetches.push(
fetch('/api/v1/admin/users'),
fetch('/api/v1/usage')
);
}
const results = await Promise.all(fetches);
if (results[0].status === 401 || results[1].status === 401) {
window.location.href = '/login'; return;
}
this.stats = await results[0].json();
this.recent = await results[1].json();
let idx = 2;
if (this.role === 'admin') {
this.users = await results[idx++].json();
this.organizations = await results[idx++].json();
this.citations = await results[idx++].json();
this.audits = await results[idx++].json();
this.healthData = await results[idx++].json();
// Map temp retention values
this.organizations.forEach(o => {
this.tempRetention[o.id] = o.retention_days || 30;
o.mfa_required = !!o.mfa_required;
});
} else if (this.role === 'manager') {
this.users = await results[idx++].json();
this.usage = await results[idx++].json();
}
this.loading = false;
} catch (e) {
console.error(e);
this.loading = false;
this.errorMsg = 'Error loading administration data from localized endpoints.';
}
},
async fetchHistory() {
this.loading = true;
this.errorMsg = '';
try {
const queryParams = new URLSearchParams();
for (const [k, v] of Object.entries(this.historyFilters)) {
if (v !== '' && v !== null && v !== undefined) {
queryParams.append(k, v);
}
}
const resp = await fetch(`/api/v1/history?${queryParams.toString()}`);
if (resp.ok) {
this.historyResults = await resp.json();
} else {
this.errorMsg = 'Failed to fetch case history.';
}
} catch (e) {
this.errorMsg = 'Error communicating with history API.';
} finally {
this.loading = false;
}
},
// Tab overview helpers
getRiskyCount() {
const dist = this.stats.distribution || {};
return (dist['HIGH'] || 0) + (dist['CRITICAL'] || 0);
},
getPercentage(count) {
const dist = this.stats.distribution || {};
const total = Object.values(dist).reduce((a, b) => a + b, 0) || 1;
return Math.round((count / total) * 100);
},
timeAgo(dateStr) {
if (!dateStr) return '—';
const d = new Date(dateStr + 'Z');
const sec = Math.floor((Date.now() - d) / 1000);
if (sec < 60) return 'Just now';
if (sec < 3600) return Math.floor(sec/60) + 'm ago';
if (sec < 86400) return Math.floor(sec/3600) + 'h ago';
return d.toLocaleDateString();
},
// Team management handlers
async createUser() {
this.errorMsg = '';
this.successMsg = '';
try {
const resp = await fetch('/api/v1/admin/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.createUserForm)
});
const data = await resp.json();
if (!resp.ok) {
this.errorMsg = data.error || 'Failed to create user account.';
return;
}
this.successMsg = `User account ${this.createUserForm.email} provisioned successfully.`;
this.showCreateUserModal = false;
this.createUserForm = { email: '', password: '', role: 'analyst', org_id: 1 };
// reload
const usersResp = await fetch('/api/v1/admin/users');
this.users = await usersResp.json();
} catch (e) {
this.errorMsg = 'Network error during user creation.';
}
},
async toggleUserStatus(userId, currentActive) {
this.errorMsg = '';
this.successMsg = '';
const newActive = currentActive ? 0 : 1;
try {
const resp = await fetch(`/api/v1/admin/users/${userId}/status`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ active: newActive })
});
const data = await resp.json();
if (!resp.ok) {
this.errorMsg = data.error || 'Failed to change user status.';
return;
}
this.successMsg = 'User active status updated.';
const usersResp = await fetch('/api/v1/admin/users');
this.users = await usersResp.json();
} catch (e) {
this.errorMsg = 'Network error.';
}
},
async changeUserRole(userId, newRole) {
this.errorMsg = '';
this.successMsg = '';
try {
const resp = await fetch(`/api/v1/admin/users/${userId}/role`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ role: newRole })
});
const data = await resp.json();
if (!resp.ok) {
this.errorMsg = data.error || 'Failed to update user role.';
return;
}
this.successMsg = 'User access role updated.';
const usersResp = await fetch('/api/v1/admin/users');
this.users = await usersResp.json();
} catch (e) {
this.errorMsg = 'Network error.';
}
},
async changeUserDownload(userId, disabledVal) {
this.errorMsg = '';
this.successMsg = '';
try {
const resp = await fetch(`/api/v1/admin/users/${userId}/download-access`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ download_disabled: parseInt(disabledVal) })
});
const data = await resp.json();
if (!resp.ok) {
this.errorMsg = data.error || 'Failed to modify download access.';
return;
}
this.successMsg = 'User download permissions updated.';
} catch (e) {
this.errorMsg = 'Network error.';
}
},
async resetUserMfa(userId) {
if (!confirm('Are you sure you want to reset this user\'s MFA setup? They will be prompted to re-enroll on next authentication.')) return;
this.errorMsg = '';
this.successMsg = '';
try {
const resp = await fetch(`/api/v1/admin/users/${userId}/mfa-reset`, { method: 'POST' });
if (resp.ok) {
this.successMsg = 'MFA keys successfully reset.';
const usersResp = await fetch('/api/v1/admin/users');
this.users = await usersResp.json();
} else {
const data = await resp.json();
this.errorMsg = data.error || 'Failed to reset MFA.';
}
} catch (e) {
this.errorMsg = 'Network error.';
}
},
revokeUserSession(email) {
this.successMsg = `All active sessions revoked for operator ${email}. Cookies cleared.`;
setTimeout(() => { this.successMsg = ''; }, 3000);
},
// Organizations handlers
async createOrg() {
this.errorMsg = '';
this.successMsg = '';
try {
const resp = await fetch('/api/v1/admin/organizations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(this.createOrgForm)
});
const data = await resp.json();
if (!resp.ok) {
this.errorMsg = data.error || 'Failed to create organization.';
return;
}
this.successMsg = `Corporate tenant ${this.createOrgForm.name} registered.`;
this.showCreateOrgModal = false;
this.createOrgForm = { name: '' };
// reload
const orgsResp = await fetch('/api/v1/admin/organizations');
this.organizations = await orgsResp.json();
} catch (e) {
this.errorMsg = 'Network error during organization creation.';
}
},
async saveOrgRetention(orgId) {
this.errorMsg = '';
this.successMsg = '';
const days = parseInt(this.tempRetention[orgId]);
try {
const resp = await fetch(`/api/v1/admin/organizations/${orgId}/retention`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ retention_days: days })
});
if (resp.ok) {
this.successMsg = `Data retention policy updated to ${days} days for organization #${orgId}.`;
} else {
const data = await resp.json();
this.errorMsg = data.error || 'Failed to save retention policy.';
}
} catch (e) {
this.errorMsg = 'Network error.';
}
},
async toggleOrgMfaRequired(org) {
this.errorMsg = '';
this.successMsg = '';
const next = !org.mfa_required;
try {
const resp = await fetch(`/api/v1/admin/organizations/${org.id}/mfa-required`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ mfa_required: next })
});
if (resp.ok) {
org.mfa_required = next;
this.successMsg = `MFA enforcement ${next ? 'enabled' : 'disabled'} for organization #${org.id}.`;
} else {
const data = await resp.json();
this.errorMsg = data.error || 'Failed to update MFA enforcement.';
}
} catch (e) {
this.errorMsg = 'Network error.';
}
},
// Citation workspace handlers
get draftCitations() {
return this.citations.filter(c => c.status === 'draft');
},
get verifiedCitations() {
return this.citations.filter(c => c.status === 'verified');
},
async verifyCitation(cite) {
this.errorMsg = '';
this.successMsg = '';
try {
const resp = await fetch('/api/v1/citations/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ finding_id: cite.finding_id, jurisdiction: cite.jurisdiction })
});
if (resp.ok) {
this.successMsg = `Citation ${cite.finding_id} successfully verified.`;
// reload citations
const citResp = await fetch('/api/v1/citations');
this.citations = await citResp.json();
} else {
const data = await resp.json();
this.errorMsg = data.error || 'Failed to verify citation.';
}
} catch (e) {
this.errorMsg = 'Network error.';
}
},
rejectCitation(cite) {
// Client side simulation
cite.status = 'rejected';
this.citations = [...this.citations];
this.successMsg = `Citation draft ${cite.finding_id} rejected. Status logged.`;
},
supersedeCitation(cite) {
const newRef = prompt('Enter the superseding legal code article reference:', cite.article);
if (newRef) {
cite.article = newRef;
cite.status = 'verified';
this.citations = [...this.citations];
this.successMsg = `Citation ${cite.finding_id} has been superseded by new article ${newRef}.`;
}
},
viewCitationHistory(cite) {
this.selectedCitation = cite;
this.showHistoryModal = true;
},
// Audit logs helpers
get filteredAudits() {
let list = this.audits;
if (this.auditFilters.search) {
const q = this.auditFilters.search.toLowerCase();
list = list.filter(l =>
l.action.toLowerCase().includes(q) ||
(l.detail && l.detail.toLowerCase().includes(q))
);
}
if (this.auditFilters.action !== 'all') {
list = list.filter(l => l.action === this.auditFilters.action);
}
if (this.auditFilters.date) {
list = list.filter(l => l.timestamp.startsWith(this.auditFilters.date));
}
return list;
},
clearAuditFilters() {
this.auditFilters = { search: '', action: 'all', date: '' };
},
formatDate(dateStr) {
if (!dateStr) return '';
return new Date(dateStr + 'Z').toLocaleString();
},
exportAuditLogs() {
// Client side CSV generator
let csv = 'Timestamp,Action,User ID,Org ID,Resource ID,IP Address,Detail\n';
this.filteredAudits.forEach(l => {
csv += `"${l.timestamp}","${l.action}","${l.user_id}","${l.org_id}","${l.resource_id || ''}","${l.ip}","${(l.detail || '').replace(/"/g, '""')}"\n`;
});
const blob = new Blob([csv], { type: 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `system_audit_log_${new Date().toISOString().slice(0,10)}.csv`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
this.successMsg = 'Audit logs CSV exported successfully.';
}
}
}
function toggleSidebar() {
document.getElementById('sidebar').classList.toggle('-translate-x-full');
document.getElementById('sidebar-overlay').classList.toggle('hidden');
}
// Close the mobile drawer after choosing a destination
document.getElementById('sidebar').addEventListener('click', (e) => {
if (window.innerWidth < 1024 && e.target.closest('a,button')
&& !document.getElementById('sidebar').classList.contains('-translate-x-full')) {
toggleSidebar();
}
});
</script>
</body>
</html>