Swap the muddy #f3ae3e palette for the real OLIVER brand pulled from the master
PPT template: yellow #FFCB05 + near-black #1A1A1A + off-white #F6F7F7, Montserrat
font. White-first page with a brand-yellow highlight rectangle behind page titles,
stat tiles with yellow left-strip, and a short yellow accent line under each
card section title — picks up the template's "01" chapter-marker rhythm.
Fixes two production bugs along the way:
- Nav stays pinned at top while page scrolls. The conflicting
`.navbar { position: relative !important }` rule was removed from nav.html
so the `position: fixed` from style.css can take effect.
- Clicking admin tabs no longer scrolls the page. Converted
`<a href="#users">` to `<button data-bs-target="#users">` (Bootstrap 5's
recommended pattern), so the anchor jump can't happen.
Other refinements: table padding loosened, `transform: scale` row hover
removed (jittery on dense rows), modal headers switched to near-black,
Chart.js palette aligned with brand tokens.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2871 lines
No EOL
117 KiB
HTML
2871 lines
No EOL
117 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Admin Dashboard - AgentHub{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container my-5">
|
|
<!-- Header -->
|
|
<div class="row mb-4">
|
|
<div class="col-12">
|
|
<div class="d-flex justify-content-between align-items-center">
|
|
<div>
|
|
<h2 class="page-title">Admin Dashboard</h2>
|
|
<p class="page-subtitle">Manage users and AI agents across the system</p>
|
|
</div>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-outline-primary" id="refreshBtn">
|
|
<i class="fas fa-sync-alt me-2"></i>Refresh
|
|
</button>
|
|
<button class="btn btn-outline-danger" onclick="logout()">
|
|
<i class="fas fa-sign-out-alt me-2"></i>Logout
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unresolved-owner banner (collector agents whose contact email doesn't match a user) -->
|
|
<div class="row mb-3" id="unresolvedOwnerRow" style="display:none;">
|
|
<div class="col-12">
|
|
<div class="alert alert-info d-flex align-items-center justify-content-between" role="alert" style="border-radius:10px;">
|
|
<div>
|
|
<i class="fas fa-user-slash me-2"></i>
|
|
<strong id="unresolvedOwnerCount"></strong>
|
|
<span class="text-muted small ms-2">— these won't receive completion-reminder emails until reassigned.</span>
|
|
</div>
|
|
<button type="button" class="btn btn-sm btn-info text-white" data-bs-toggle="modal" data-bs-target="#unresolvedOwnerModal">
|
|
<i class="fas fa-list me-1"></i>Review & reassign
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unresolved-owner modal -->
|
|
<div class="modal fade" id="unresolvedOwnerModal" tabindex="-1" aria-hidden="true">
|
|
<div class="modal-dialog modal-xl">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-user-slash me-2"></i>Agents with unresolved owners</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<p class="text-muted small">
|
|
These agents were created via the LibreChat collector but their <code>agent_contact_person</code>
|
|
email doesn't match any active AgentHub user. Reassign each one to a real user — they'll
|
|
then start receiving completion-reminder emails on the next daily run.
|
|
</p>
|
|
<table class="table table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent</th>
|
|
<th>Stored contact</th>
|
|
<th>Reassign to (email)</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="unresolvedOwnerTableBody"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Statistics Cards -->
|
|
<div class="row mb-4 align-items-stretch">
|
|
<div class="col-lg-2 col-md-4 mb-3 d-flex">
|
|
<div class="stat-tile w-100">
|
|
<div class="stat-tile-icon"><i class="fas fa-users"></i></div>
|
|
<div>
|
|
<h3 id="totalUsers" class="stat-tile-value">0</h3>
|
|
<p class="stat-tile-label">Total Users</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-4 mb-3 d-flex">
|
|
<div class="stat-tile w-100">
|
|
<div class="stat-tile-icon"><i class="fas fa-robot"></i></div>
|
|
<div>
|
|
<h3 id="totalAgents" class="stat-tile-value">0</h3>
|
|
<p class="stat-tile-label">Total Agents</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-4 mb-3 d-flex">
|
|
<div class="stat-tile w-100">
|
|
<div class="stat-tile-icon"><i class="fas fa-envelope"></i></div>
|
|
<div>
|
|
<h3 id="totalMessages" class="stat-tile-value">0</h3>
|
|
<p class="stat-tile-label">Total Messages</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-4 mb-3 d-flex">
|
|
<div class="stat-tile w-100">
|
|
<div class="stat-tile-icon"><i class="fas fa-coins"></i></div>
|
|
<div>
|
|
<h3 id="totalTokens" class="stat-tile-value">0</h3>
|
|
<p class="stat-tile-label">Total Tokens</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-4 mb-3 d-flex">
|
|
<div class="stat-tile w-100">
|
|
<div class="stat-tile-icon"><i class="fas fa-comments"></i></div>
|
|
<div>
|
|
<h3 id="totalConversations" class="stat-tile-value">0</h3>
|
|
<p class="stat-tile-label">Conversations</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-lg-2 col-md-4 mb-3 d-flex">
|
|
<div class="stat-tile w-100">
|
|
<div class="stat-tile-icon"><i class="fas fa-user-friends"></i></div>
|
|
<div>
|
|
<h3 id="totalUniqueUsers" class="stat-tile-value">0</h3>
|
|
<p class="stat-tile-label">Agent-User Sessions</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs Navigation -->
|
|
<div class="row">
|
|
<div class="col-12">
|
|
<div class="card shadow-sm border-0">
|
|
<div class="card-header bg-white">
|
|
<ul class="nav nav-tabs card-header-tabs" id="adminTabs" role="tablist">
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link active" id="analytics-tab" type="button"
|
|
data-bs-toggle="tab" data-bs-target="#analytics"
|
|
role="tab" aria-controls="analytics" aria-selected="true">
|
|
<i class="fas fa-chart-line me-2"></i>Analytics
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="users-tab" type="button"
|
|
data-bs-toggle="tab" data-bs-target="#users"
|
|
role="tab" aria-controls="users" aria-selected="false">
|
|
<i class="fas fa-users me-2"></i>Users Management
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="agents-tab" type="button"
|
|
data-bs-toggle="tab" data-bs-target="#agents"
|
|
role="tab" aria-controls="agents" aria-selected="false">
|
|
<i class="fas fa-robot me-2"></i>Agents Management
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="verification-tab" type="button"
|
|
data-bs-toggle="tab" data-bs-target="#verification"
|
|
role="tab" aria-controls="verification" aria-selected="false">
|
|
<i class="fas fa-check-circle me-2"></i>Verification
|
|
<span class="badge bg-warning ms-1" id="verificationPendingBadge" style="display:none;">0</span>
|
|
</button>
|
|
</li>
|
|
<li class="nav-item" role="presentation">
|
|
<button class="nav-link" id="audit-tab" type="button"
|
|
data-bs-toggle="tab" data-bs-target="#audit"
|
|
role="tab" aria-controls="audit" aria-selected="false">
|
|
<i class="fas fa-shield-alt me-2"></i>Prompt Audit
|
|
<span class="badge bg-danger ms-1" id="auditFlaggedBadge" style="display:none;">0</span>
|
|
</button>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="tab-content" id="adminTabsContent">
|
|
<!-- Analytics Tab -->
|
|
<div class="tab-pane fade show active" id="analytics">
|
|
<!-- Analytics Filter Bar -->
|
|
<div class="row mb-3">
|
|
<div class="col-12">
|
|
<div class="d-flex flex-wrap align-items-center gap-2 p-3 bg-light rounded">
|
|
<span class="fw-semibold text-muted me-1"><i class="fas fa-filter me-1"></i>Filters:</span>
|
|
<select id="analyticsStatusFilter" class="form-select form-select-sm" style="width:auto;min-width:150px;">
|
|
<option value="">All Statuses</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Development">Development</option>
|
|
<option value="Inactive">Inactive</option>
|
|
<option value="Deprecated">Deprecated</option>
|
|
</select>
|
|
<select id="analyticsDisciplineFilter" class="form-select form-select-sm" style="width:auto;min-width:200px;">
|
|
<option value="">All Disciplines</option>
|
|
<option value="Strategy">Strategy</option>
|
|
<option value="Creative">Creative</option>
|
|
<option value="Oversight including delivery">Oversight including delivery</option>
|
|
<option value="Optimisation">Optimisation</option>
|
|
<option value="Back Office including operations">Back Office including operations</option>
|
|
<option value="Pencil Agents">Pencil Agents</option>
|
|
</select>
|
|
<select id="analyticsTimeRange" class="form-select form-select-sm" style="width:auto;min-width:140px;">
|
|
<option value="30">Last 30 Days</option>
|
|
<option value="60">Last 60 Days</option>
|
|
<option value="90" selected>Last 90 Days</option>
|
|
<option value="180">Last 180 Days</option>
|
|
</select>
|
|
<button id="analyticsApplyBtn" class="btn btn-sm btn-primary">
|
|
<i class="fas fa-check me-1"></i>Apply
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<!-- Usage Over Time Chart -->
|
|
<div class="col-lg-8 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-chart-area me-2"></i>System Usage Over Time <span id="timeRangeLabel">(Last 90 Days)</span></h6></div>
|
|
<div class="card-body">
|
|
<div id="usageTimelineEmpty" class="text-center text-muted py-5" style="display:none;">No usage timeline data available</div>
|
|
<canvas id="usageTimelineChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Token Breakdown Doughnut -->
|
|
<div class="col-lg-4 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-coins me-2"></i>Token Breakdown</h6></div>
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
<div id="tokenBreakdownEmpty" class="text-center text-muted py-5" style="display:none;">No token data available</div>
|
|
<canvas id="tokenBreakdownChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<!-- Status Breakdown Doughnut -->
|
|
<div class="col-lg-4 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-signal me-2"></i>Agents by Status</h6></div>
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
<div id="statusBreakdownEmpty" class="text-center text-muted py-5" style="display:none;">No agent data available</div>
|
|
<canvas id="statusBreakdownChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Discipline Breakdown Doughnut -->
|
|
<div class="col-lg-4 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-layer-group me-2"></i>Agents by Discipline</h6></div>
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
<div id="disciplineBreakdownEmpty" class="text-center text-muted py-5" style="display:none;">No discipline data available</div>
|
|
<canvas id="disciplineBreakdownChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Rating Distribution Bar Chart -->
|
|
<div class="col-lg-4 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-star me-2"></i>Rating Distribution</h6></div>
|
|
<div class="card-body d-flex align-items-center justify-content-center">
|
|
<div id="ratingDistEmpty" class="text-center text-muted py-5" style="display:none;">No rating data available</div>
|
|
<canvas id="ratingDistChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<!-- Top 5 Agents by Messages -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-trophy me-2"></i>Top 5 Agents by Messages</h6></div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 analytics-table">
|
|
<thead><tr><th>#</th><th>Agent</th><th>Status</th><th>Messages</th></tr></thead>
|
|
<tbody id="topMessagesTbody">
|
|
<tr><td colspan="4" class="text-center text-muted py-3">No data</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Top 5 Agents by Tokens -->
|
|
<div class="col-lg-6 mb-4">
|
|
<div class="card h-100 border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-trophy me-2"></i>Top 5 Agents by Tokens</h6></div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 analytics-table">
|
|
<thead><tr><th>#</th><th>Agent</th><th>Status</th><th>Tokens</th></tr></thead>
|
|
<tbody id="topTokensTbody">
|
|
<tr><td colspan="4" class="text-center text-muted py-3">No data</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row mb-4">
|
|
<!-- Recently Active Agents -->
|
|
<div class="col-12">
|
|
<div class="card border-0 shadow-sm">
|
|
<div class="card-header bg-white"><h6 class="mb-0"><i class="fas fa-clock me-2"></i>Recently Active Agents</h6></div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover mb-0 analytics-table">
|
|
<thead><tr><th>Agent</th><th>Status</th><th>Discipline</th><th>Messages</th><th>Tokens</th><th>Last Used</th></tr></thead>
|
|
<tbody id="recentlyActiveTbody">
|
|
<tr><td colspan="6" class="text-center text-muted py-3">No recently active agents</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Users Management Tab -->
|
|
<div class="tab-pane fade" id="users">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="mb-0">Users Management</h5>
|
|
<div class="d-flex gap-2">
|
|
<button class="btn btn-success admin-write-action" onclick="showCreateUserModal()">
|
|
<i class="fas fa-user-plus me-2"></i>Create User
|
|
</button>
|
|
<div class="input-group" style="width: 300px;">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="text" class="form-control" id="userSearch" placeholder="Search users...">
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Email</th>
|
|
<th>Auth</th>
|
|
<th>Type</th>
|
|
<th>Status</th>
|
|
<th>Agents</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="usersTableBody">
|
|
<tr>
|
|
<td colspan="7" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Agents Management Tab -->
|
|
<div class="tab-pane fade" id="agents">
|
|
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-3">
|
|
<h5 class="mb-0">Agent Management</h5>
|
|
<div class="d-flex flex-wrap gap-2 admin-write-action">
|
|
<button type="button" class="btn btn-success btn-sm" id="exportFilteredXlsxBtn"
|
|
title="Download agents matching the filters below as an Excel file">
|
|
<i class="fas fa-file-excel me-1"></i>Export
|
|
</button>
|
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
onclick="document.getElementById('csv-import-input').click()">
|
|
<i class="fas fa-file-upload me-1"></i>Import CSV
|
|
</button>
|
|
<button type="button" class="btn btn-outline-danger btn-sm"
|
|
onclick="document.getElementById('csv-delete-input').click()">
|
|
<i class="fas fa-trash-alt me-1"></i>Delete by CSV
|
|
</button>
|
|
<input type="file" id="csv-import-input" accept=".csv" style="display:none;" onchange="uploadCsv(this)">
|
|
<input type="file" id="csv-delete-input" accept=".csv" style="display:none;" onchange="deleteCsv(this)">
|
|
</div>
|
|
</div>
|
|
<div class="d-flex flex-wrap align-items-center gap-2 mb-3 p-2 bg-light rounded">
|
|
<span class="fw-semibold text-muted me-1"><i class="fas fa-filter me-1"></i>Filters:</span>
|
|
<select class="form-select form-select-sm" id="agentStatusFilter" style="width:auto;min-width:130px;">
|
|
<option value="">All Statuses</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Development">Development</option>
|
|
<option value="Inactive">Inactive</option>
|
|
<option value="Deprecated">Deprecated</option>
|
|
</select>
|
|
<select class="form-select form-select-sm" id="agentDisciplineFilter" style="width:auto;min-width:180px;">
|
|
<option value="">All Disciplines</option>
|
|
<option value="Strategy">Strategy</option>
|
|
<option value="Creative">Creative</option>
|
|
<option value="Oversight including delivery">Oversight incl. delivery</option>
|
|
<option value="Optimisation">Optimisation</option>
|
|
<option value="Back Office including operations">Back Office incl. ops</option>
|
|
<option value="Pencil Agents">Pencil Agents</option>
|
|
</select>
|
|
<select class="form-select form-select-sm" id="agentAuditFilter" style="width:auto;min-width:120px;">
|
|
<option value="">All Audit</option>
|
|
<option value="audited">Audited</option>
|
|
<option value="not_audited">Not Audited</option>
|
|
</select>
|
|
<select class="form-select form-select-sm" id="agentBusinessEntityFilter" style="width:auto;min-width:140px;">
|
|
<option value="">All Entities</option>
|
|
<option value="OLIVER">OLIVER</option>
|
|
<option value="DARE">DARE</option>
|
|
<option value="Brandtech Group">Brandtech Group</option>
|
|
<option value="Pencil">Pencil</option>
|
|
<option value="Jellyfish">Jellyfish</option>
|
|
<option value="Adjust">Adjust</option>
|
|
<option value="Other">Other</option>
|
|
</select>
|
|
<select class="form-select form-select-sm" id="agentTypeFilter" style="width:auto;min-width:120px;">
|
|
<option value="">All Types</option>
|
|
<option value="Utility">Utility</option>
|
|
<option value="Functional">Functional</option>
|
|
<option value="Supervisory">Supervisory</option>
|
|
<option value="Guardian">Guardian</option>
|
|
</select>
|
|
<select class="form-select form-select-sm" id="agentAutonomyFilter" style="width:auto;min-width:140px;">
|
|
<option value="">All Autonomy</option>
|
|
<option value="Human-Led">Human-Led</option>
|
|
<option value="Hybrid">Hybrid</option>
|
|
<option value="Autopilot">Autopilot</option>
|
|
</select>
|
|
<button type="button" class="btn btn-outline-danger btn-sm" id="adminComplianceRiskBtn"
|
|
title="Show only PII=Yes, IP=Shared/TBD, or Autopilot agents">
|
|
<i class="fas fa-shield-alt me-1"></i>Risks
|
|
</button>
|
|
<div class="input-group input-group-sm" style="width:260px;">
|
|
<span class="input-group-text"><i class="fas fa-search"></i></span>
|
|
<input type="text" class="form-control" id="agentSearch" placeholder="Search agents...">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent Name</th>
|
|
<th>Owner</th>
|
|
<th>Version</th>
|
|
<th>Created</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="agentsTableBody">
|
|
<tr>
|
|
<td colspan="5" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Verification Tab -->
|
|
<div class="tab-pane fade" id="verification">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="mb-0"><i class="fas fa-check-circle me-2"></i>Agent Verification</h5>
|
|
<div class="d-flex gap-2">
|
|
<select id="verificationFilter" class="form-select form-select-sm" style="width:auto;" onchange="filterVerificationAgents()">
|
|
<option value="all">All</option>
|
|
<option value="needs_verification" selected>Needs Verification</option>
|
|
<option value="verified">Verified</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div class="table-responsive">
|
|
<table class="table table-hover">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent Name</th>
|
|
<th>Client Name</th>
|
|
<th>Studio</th>
|
|
<th>Created By</th>
|
|
<th>Date Created</th>
|
|
<th>Status</th>
|
|
<th>Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="verificationTableBody">
|
|
<tr>
|
|
<td colspan="7" class="text-center py-4">
|
|
<div class="spinner-border text-primary" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Prompt Audit Tab -->
|
|
<div class="tab-pane fade" id="audit">
|
|
<!-- Config Warning (shown when Gemini not configured) -->
|
|
<div id="auditConfigWarning" class="alert alert-info" style="display:none;">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
<strong>Gemini API not configured.</strong> Set <code>GOOGLE_API_KEY</code> in your environment to enable automated prompt auditing.
|
|
</div>
|
|
|
|
<!-- Header + Actions -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="mb-0"><i class="fas fa-shield-alt me-2"></i>Prompt Audit & Classification</h5>
|
|
<div class="d-flex gap-2 admin-write-action">
|
|
<button class="btn btn-sm btn-outline-primary" id="runAuditUnclassifiedBtn" onclick="runAudit(true)">
|
|
<i class="fas fa-bolt me-1"></i>Run Unclassified Only
|
|
</button>
|
|
<button class="btn btn-sm btn-primary" id="runAuditBtn" onclick="runAudit(false)">
|
|
<i class="fas fa-play me-1"></i>Run Full Audit
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Progress -->
|
|
<div id="auditProgress" class="mb-3" style="display:none;">
|
|
<div class="alert alert-warning d-flex align-items-center">
|
|
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
|
<span id="auditProgressText">Running audit... This may take several minutes for large batches.</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="row mb-3">
|
|
<div class="col-md-2">
|
|
<div class="card text-center border-0 shadow-sm">
|
|
<div class="card-body py-2">
|
|
<h5 class="mb-0" id="auditTotalCount">0</h5>
|
|
<small class="text-muted">Audited</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card text-center border-0 shadow-sm" style="border-left:3px solid #dc3545 !important;">
|
|
<div class="card-body py-2">
|
|
<h5 class="mb-0 text-danger" id="auditFlaggedCount">0</h5>
|
|
<small class="text-muted">Flagged</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card text-center border-0 shadow-sm" style="border-left:3px solid #ffc107 !important;">
|
|
<div class="card-body py-2">
|
|
<h5 class="mb-0 text-warning" id="auditReviewedCount">0</h5>
|
|
<small class="text-muted">Reviewed</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card text-center border-0 shadow-sm" style="border-left:3px solid #28a745 !important;">
|
|
<div class="card-body py-2">
|
|
<h5 class="mb-0 text-success" id="auditClearedCount">0</h5>
|
|
<small class="text-muted">Cleared</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card text-center border-0 shadow-sm" style="border-left:3px solid #fd7e14 !important;">
|
|
<div class="card-body py-2">
|
|
<h5 class="mb-0 text-orange" id="auditClientCount">0</h5>
|
|
<small class="text-muted">Client Detected</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-2">
|
|
<div class="card text-center border-0 shadow-sm">
|
|
<div class="card-body py-2">
|
|
<h5 class="mb-0 text-secondary" id="auditNoPromptCount">0</h5>
|
|
<small class="text-muted">No Instructions</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Filters -->
|
|
<div class="d-flex flex-wrap align-items-center gap-2 mb-3 p-2 bg-light rounded">
|
|
<span class="fw-semibold text-muted me-1"><i class="fas fa-filter me-1"></i>Filters:</span>
|
|
<select id="auditCategoryFilter" class="form-select form-select-sm" style="width:auto;min-width:120px;" onchange="filterAuditResults()">
|
|
<option value="">All Categories</option>
|
|
<option value="1">Cat 1</option>
|
|
<option value="1B">Cat 1B</option>
|
|
<option value="2">Cat 2</option>
|
|
<option value="3">Cat 3</option>
|
|
</select>
|
|
<select id="auditRiskFilter" class="form-select form-select-sm" style="width:auto;min-width:120px;" onchange="filterAuditResults()">
|
|
<option value="">All Risk Levels</option>
|
|
<option value="critical">Critical</option>
|
|
<option value="high">High</option>
|
|
<option value="medium">Medium</option>
|
|
<option value="low">Low</option>
|
|
</select>
|
|
<select id="auditDisciplineFilter" class="form-select form-select-sm" style="width:auto;min-width:180px;" onchange="filterAuditResults()">
|
|
<option value="">All Disciplines</option>
|
|
<option value="Strategy">Strategy</option>
|
|
<option value="Creative">Creative</option>
|
|
<option value="Oversight including delivery">Oversight incl. delivery</option>
|
|
<option value="Optimisation">Optimisation</option>
|
|
<option value="Back Office including operations">Back Office incl. ops</option>
|
|
<option value="Pencil Agents">Pencil Agents</option>
|
|
</select>
|
|
<select id="auditStatusFilter" class="form-select form-select-sm" style="width:auto;min-width:120px;" onchange="filterAuditResults()">
|
|
<option value="">All Statuses</option>
|
|
<option value="flagged">Flagged</option>
|
|
<option value="reviewed">Reviewed</option>
|
|
<option value="cleared">Cleared</option>
|
|
<option value="none">Not Audited</option>
|
|
</select>
|
|
<select id="auditClientFilter" class="form-select form-select-sm" style="width:auto;min-width:130px;" onchange="filterAuditResults()">
|
|
<option value="">All Client Status</option>
|
|
<option value="yes">Client Detected</option>
|
|
<option value="no">Not Client</option>
|
|
</select>
|
|
<input type="text" id="auditSearchInput" class="form-control form-control-sm" style="width:auto;min-width:180px;" placeholder="Search agents..." oninput="filterAuditResults()">
|
|
</div>
|
|
|
|
<!-- Results Table -->
|
|
<div class="table-responsive">
|
|
<table class="table table-hover table-sm">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent Name</th>
|
|
<th>Discipline</th>
|
|
<th>Department</th>
|
|
<th>Category</th>
|
|
<th>Risk</th>
|
|
<th>Client</th>
|
|
<th>Flags</th>
|
|
<th>Status</th>
|
|
<th>Last Audited</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="auditTableBody">
|
|
<tr>
|
|
<td colspan="10" class="text-center py-4 text-muted">
|
|
Click "Run Full Audit" to classify agents, or loading...
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Detail Modal -->
|
|
<div class="modal fade" id="auditDetailModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-scrollable">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-shield-alt me-2"></i>Audit Detail</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body" id="auditDetailBody">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
<div class="modal-footer admin-write-action">
|
|
<div class="d-flex gap-2 align-items-center w-100">
|
|
<select class="form-select form-select-sm" id="auditReviewStatus" style="width:auto;">
|
|
<option value="flagged">Flagged</option>
|
|
<option value="reviewed">Reviewed</option>
|
|
<option value="cleared">Cleared</option>
|
|
</select>
|
|
<input type="text" class="form-control form-control-sm" id="auditReviewNotes" placeholder="Reviewer notes (optional)">
|
|
<button class="btn btn-sm btn-primary" id="auditReviewSaveBtn" onclick="submitAuditReview()">
|
|
<i class="fas fa-save me-1"></i>Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit User Modal -->
|
|
<div class="modal fade" id="editUserModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editUserForm">
|
|
<input type="hidden" id="editUserId">
|
|
<input type="hidden" id="editUserAuthProvider">
|
|
<div class="mb-3">
|
|
<label for="editUserEmail" class="form-label">Email Address</label>
|
|
<input type="email" class="form-control" id="editUserEmail" readonly>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="editUserFullName" class="form-label">Full Name</label>
|
|
<input type="text" class="form-control" id="editUserFullName">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="form-label">Authentication Method</label>
|
|
<div id="editUserAuthDisplay">
|
|
<span class="badge bg-secondary">Loading...</span>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="editUserIsActive">
|
|
<label class="form-check-label" for="editUserIsActive">
|
|
Account is active
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check" style="display:none;">
|
|
<input class="form-check-input" type="checkbox" id="editUserIsAdmin">
|
|
<label class="form-check-label" for="editUserIsAdmin">
|
|
User has admin privileges
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="editUserRole" class="form-label">Role</label>
|
|
<select class="form-select" id="editUserRole">
|
|
<option value="user">User</option>
|
|
<option value="admin">Admin</option>
|
|
<option value="readonly_admin">Read-Only Admin</option>
|
|
</select>
|
|
<div class="form-text">Read-Only Admin can view the admin dashboard but cannot make changes.</div>
|
|
</div>
|
|
<!-- Password Reset Section (only for local users) -->
|
|
<div class="mb-3" id="passwordResetSection" style="display: none;">
|
|
<hr>
|
|
<label class="form-label">Password Management</label>
|
|
<button type="button" class="btn btn-outline-warning btn-sm" onclick="showResetPasswordModal()">
|
|
<i class="fas fa-key me-2"></i>Reset Password
|
|
</button>
|
|
<div class="form-text">Reset the user's password. They will need to use the new password to log in.</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="editUserForm" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div class="modal fade" id="createUserModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-user-plus me-2"></i>Create New User</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-info small">
|
|
<i class="fas fa-info-circle me-2"></i>
|
|
This creates a local user account with email/password authentication.
|
|
For Microsoft SSO users, they will be created automatically on first login.
|
|
</div>
|
|
<form id="createUserForm">
|
|
<div class="mb-3">
|
|
<label for="createUserEmail" class="form-label">Email Address <span class="text-danger">*</span></label>
|
|
<input type="email" class="form-control" id="createUserEmail" required placeholder="user@example.com">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="createUserFullName" class="form-label">Full Name</label>
|
|
<input type="text" class="form-control" id="createUserFullName" placeholder="John Doe">
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="createUserPassword" class="form-label">Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="createUserPassword" required minlength="8" placeholder="Minimum 8 characters">
|
|
<div class="form-text">Password must be at least 8 characters.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="createUserPasswordConfirm" class="form-label">Confirm Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="createUserPasswordConfirm" required minlength="8" placeholder="Confirm password">
|
|
</div>
|
|
<div class="mb-3">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="createUserIsAdmin">
|
|
<label class="form-check-label" for="createUserIsAdmin">
|
|
Grant admin privileges
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="createUserForm" class="btn btn-success">
|
|
<i class="fas fa-user-plus me-2"></i>Create User
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Reset Password Modal -->
|
|
<div class="modal fade" id="resetPasswordModal" tabindex="-1">
|
|
<div class="modal-dialog">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-key me-2"></i>Reset Password</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="alert alert-warning small">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
|
This will reset the password for <strong id="resetPasswordUserEmail"></strong>.
|
|
The user will need to use the new password to log in.
|
|
</div>
|
|
<form id="resetPasswordForm">
|
|
<input type="hidden" id="resetPasswordEmail">
|
|
<div class="mb-3">
|
|
<label for="newPassword" class="form-label">New Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="newPassword" required minlength="8" placeholder="Minimum 8 characters">
|
|
<div class="form-text">Password must be at least 8 characters.</div>
|
|
</div>
|
|
<div class="mb-3">
|
|
<label for="newPasswordConfirm" class="form-label">Confirm New Password <span class="text-danger">*</span></label>
|
|
<input type="password" class="form-control" id="newPasswordConfirm" required minlength="8" placeholder="Confirm new password">
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="resetPasswordForm" class="btn btn-warning">
|
|
<i class="fas fa-key me-2"></i>Reset Password
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Edit Agent Modal -->
|
|
<div class="modal fade" id="editAgentModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg modal-dialog-centered">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title">Edit Agent</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<form id="editAgentForm">
|
|
<input type="hidden" id="editAgentId">
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentName" class="form-label">Agent Name *</label>
|
|
<input type="text" class="form-control" id="editAgentName" required>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentTool" class="form-label">Tool *</label>
|
|
<input type="text" class="form-control" id="editAgentTool" required>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentStatus" class="form-label">Status</label>
|
|
<select class="form-select" id="editAgentStatus">
|
|
<option value="Development">Development</option>
|
|
<option value="Active">Active</option>
|
|
<option value="Inactive">Inactive</option>
|
|
<option value="Deprecated">Deprecated</option>
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentVersion" class="form-label">Version</label>
|
|
<input type="text" class="form-control" id="editAgentVersion">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentDescription" class="form-label">Description</label>
|
|
<textarea class="form-control" id="editAgentDescription" rows="3" maxlength="300"></textarea>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentPurpose" class="form-label">Purpose</label>
|
|
<input type="text" class="form-control" id="editAgentPurpose" maxlength="200">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentLocation" class="form-label">Location</label>
|
|
<input type="text" class="form-control" id="editAgentLocation">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentDepartment" class="form-label">Department</label>
|
|
<input type="text" class="form-control" id="editAgentDepartment">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentContact" class="form-label">Contact Person</label>
|
|
<input type="text" class="form-control" id="editAgentContact">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentTags" class="form-label">Tags</label>
|
|
<input type="text" class="form-control" id="editAgentTags" placeholder="Separate with commas">
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentUserbase" class="form-label">Target Userbase</label>
|
|
<input type="text" class="form-control" id="editAgentUserbase" placeholder="Separate with commas">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label for="editAgentCapabilities" class="form-label">Capabilities</label>
|
|
<input type="text" class="form-control" id="editAgentCapabilities" placeholder="Separate with commas">
|
|
</div>
|
|
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<label for="editAgentDiscipline" class="form-label">
|
|
<i class="fas fa-layer-group me-2"></i>Discipline
|
|
</label>
|
|
<select class="form-select" id="editAgentDiscipline">
|
|
<option value="">Select Discipline</option>
|
|
<option value="Strategy">Strategy</option>
|
|
<option value="Creative">Creative</option>
|
|
<option value="Oversight including delivery">Oversight including delivery</option>
|
|
<option value="Optimisation">Optimisation</option>
|
|
<option value="Back Office including operations">Back Office including operations</option>
|
|
<option value="Pencil Agents">Pencil Agents</option>
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="editQualityAuditSection">
|
|
<div class="form-check">
|
|
<input class="form-check-input" type="checkbox" id="editQualityAuditStatus" onchange="toggleAdminRiskFactor()">
|
|
<label class="form-check-label" for="editQualityAuditStatus">
|
|
<i class="fas fa-certificate me-2"></i>Quality Audit
|
|
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
|
</label>
|
|
</div>
|
|
<div class="form-text" id="editQualityAuditNote">
|
|
Check this box if the agent has passed quality audit review.
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-3" id="editRiskFactorSection" style="display: none;">
|
|
<label for="editRiskFactor" class="form-label">
|
|
<i class="fas fa-exclamation-triangle me-2"></i>Risk Factor
|
|
<span class="badge bg-warning text-dark ms-2">Admin Only</span>
|
|
<span class="text-danger">*</span>
|
|
</label>
|
|
<select class="form-select" id="editRiskFactor">
|
|
<option value="">Select Risk Level</option>
|
|
<option value="1">1 - Very Low Risk</option>
|
|
<option value="2">2 - Low Risk</option>
|
|
<option value="3">3 - Medium Risk</option>
|
|
<option value="4">4 - High Risk</option>
|
|
<option value="5">5 - Very High Risk</option>
|
|
</select>
|
|
<div class="form-text" id="editRiskFactorNote">
|
|
Required when Quality Audit is checked. Select the appropriate risk level for this agent.
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
|
<button type="submit" form="editAgentForm" class="btn btn-primary">Save Changes</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Rating Framework Info Modal -->
|
|
<div class="modal fade" id="ratingFrameworkModal" tabindex="-1">
|
|
<div class="modal-dialog modal-lg">
|
|
<div class="modal-content">
|
|
<div class="modal-header">
|
|
<h5 class="modal-title"><i class="fas fa-info-circle me-2"></i>Rating Framework</h5>
|
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<h6 class="mb-3">Star Rating Scale</h6>
|
|
<table class="table table-bordered table-sm">
|
|
<thead class="table-light">
|
|
<tr><th>Rating</th><th>Level</th><th>Description</th></tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr><td><span class="text-warning">★</span> 1</td><td>Initial</td><td>Agent is in early stages; minimal functionality, limited testing, and not yet ready for regular use.</td></tr>
|
|
<tr><td><span class="text-warning">★★</span> 2</td><td>Developing</td><td>Agent has basic functionality but significant gaps remain; occasional errors and inconsistent outputs.</td></tr>
|
|
<tr><td><span class="text-warning">★★★</span> 3</td><td>Competent</td><td>Agent performs core tasks reliably; meets basic expectations with acceptable accuracy and response quality.</td></tr>
|
|
<tr><td><span class="text-warning">★★★★</span> 4</td><td>Proficient</td><td>Agent delivers high-quality results consistently; handles edge cases well and provides good user experience.</td></tr>
|
|
<tr><td><span class="text-warning">★★★★★</span> 5</td><td>Expert</td><td>Agent excels across all dimensions; exceptional accuracy, reliability, and user satisfaction.</td></tr>
|
|
</tbody>
|
|
</table>
|
|
<h6 class="mt-4 mb-3">Performance Areas</h6>
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-title"><i class="fas fa-bullseye me-2 text-primary"></i>Accuracy & Reliability</h6>
|
|
<p class="card-text small text-muted mb-0">How correct and consistent are the agent's outputs? Does it handle inputs reliably without errors?</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-title"><i class="fas fa-user-check me-2 text-success"></i>User Experience</h6>
|
|
<p class="card-text small text-muted mb-0">Is the agent easy to interact with? Does it understand user intent and provide clear, helpful responses?</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-title"><i class="fas fa-tachometer-alt me-2 text-warning"></i>Efficiency & Performance</h6>
|
|
<p class="card-text small text-muted mb-0">Does the agent respond promptly? Does it complete tasks without unnecessary steps or resource usage?</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6 mb-3">
|
|
<div class="card h-100">
|
|
<div class="card-body">
|
|
<h6 class="card-title"><i class="fas fa-expand-arrows-alt me-2 text-info"></i>Scope & Adaptability</h6>
|
|
<p class="card-text small text-muted mb-0">How well does the agent handle varied or unexpected scenarios? Can it adapt to different user needs?</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style>
|
|
.user-avatar-sm {
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 4px;
|
|
background: var(--brand-yellow);
|
|
color: var(--brand-dark);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-size: 0.8rem;
|
|
font-weight: 700;
|
|
}
|
|
|
|
/* Column width adjustments for better visibility - Override Bootstrap with higher specificity */
|
|
|
|
/* Users table column widths */
|
|
#users .table-responsive .table.table-hover th:nth-child(1),
|
|
#users .table-responsive .table.table-hover td:nth-child(1) {
|
|
width: 22% !important;
|
|
min-width: 140px !important;
|
|
max-width: 180px !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(2),
|
|
#users .table-responsive .table.table-hover td:nth-child(2) {
|
|
width: 25% !important;
|
|
min-width: 160px !important;
|
|
max-width: 220px !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(3),
|
|
#users .table-responsive .table.table-hover td:nth-child(3) {
|
|
width: 10% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(4),
|
|
#users .table-responsive .table.table-hover td:nth-child(4) {
|
|
width: 10% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(5),
|
|
#users .table-responsive .table.table-hover td:nth-child(5) {
|
|
width: 8% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(6),
|
|
#users .table-responsive .table.table-hover td:nth-child(6) {
|
|
width: 12% !important;
|
|
}
|
|
#users .table-responsive .table.table-hover th:nth-child(7),
|
|
#users .table-responsive .table.table-hover td:nth-child(7) {
|
|
width: 15% !important;
|
|
min-width: 120px !important;
|
|
}
|
|
|
|
/* Agents table column widths (5 columns: Agent Name, Owner, Version, Created, Actions) */
|
|
#agents .table-responsive .table.table-hover th:nth-child(1),
|
|
#agents .table-responsive .table.table-hover td:nth-child(1) {
|
|
width: 40% !important;
|
|
min-width: 220px !important;
|
|
max-width: 320px !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(2),
|
|
#agents .table-responsive .table.table-hover td:nth-child(2) {
|
|
width: 25% !important;
|
|
min-width: 160px !important;
|
|
max-width: 200px !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(3),
|
|
#agents .table-responsive .table.table-hover td:nth-child(3) {
|
|
width: 12% !important;
|
|
max-width: 90px !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(4),
|
|
#agents .table-responsive .table.table-hover td:nth-child(4) {
|
|
width: 13% !important;
|
|
}
|
|
#agents .table-responsive .table.table-hover th:nth-child(5),
|
|
#agents .table-responsive .table.table-hover td:nth-child(5) {
|
|
width: 10% !important;
|
|
min-width: 120px !important;
|
|
}
|
|
|
|
/* Ensure table uses full width and layout is fixed */
|
|
#users .table-responsive .table.table-hover,
|
|
#agents .table-responsive .table.table-hover {
|
|
table-layout: fixed !important;
|
|
width: 100% !important;
|
|
}
|
|
|
|
/* Make sure text wraps in narrow columns */
|
|
#users .table-responsive .table.table-hover td,
|
|
#agents .table-responsive .table.table-hover td {
|
|
word-wrap: break-word !important;
|
|
overflow-wrap: break-word !important;
|
|
}
|
|
|
|
.table-hover tbody tr:hover {
|
|
background-color: var(--brand-off-white);
|
|
}
|
|
|
|
.card {
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border-color);
|
|
box-shadow: var(--shadow);
|
|
}
|
|
|
|
.card-header {
|
|
background: #FFFFFF;
|
|
border-bottom: 1px solid var(--border-color);
|
|
border-radius: 12px 12px 0 0 !important;
|
|
padding: 0;
|
|
}
|
|
|
|
.card-header-tabs {
|
|
margin: 0 !important;
|
|
}
|
|
|
|
.nav-tabs {
|
|
border-bottom: none;
|
|
padding: 0 0.5rem;
|
|
}
|
|
|
|
.nav-tabs .nav-link {
|
|
background: transparent;
|
|
border: none;
|
|
color: var(--brand-grey);
|
|
padding: 14px 18px;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-weight: 600;
|
|
font-size: 0.85rem;
|
|
letter-spacing: 0.02em;
|
|
border-bottom: 3px solid transparent;
|
|
border-radius: 0;
|
|
transition: color 0.15s ease, border-color 0.15s ease;
|
|
}
|
|
|
|
.nav-tabs .nav-link:hover {
|
|
color: var(--brand-dark);
|
|
border-bottom-color: var(--brand-yellow-soft);
|
|
}
|
|
|
|
.nav-tabs .nav-link.active {
|
|
background-color: transparent;
|
|
border-bottom: 3px solid var(--brand-yellow);
|
|
color: var(--brand-dark);
|
|
}
|
|
|
|
.status-Active { background-color: #d4edda; color: #155724; }
|
|
.status-Development { background-color: #fff3cd; color: #856404; }
|
|
.status-Inactive { background-color: #f8d7da; color: #721c24; }
|
|
.status-Deprecated { background-color: #e2e3e5; color: #41464b; }
|
|
|
|
.bg-orange {
|
|
background-color: #fd7e14 !important;
|
|
color: white !important;
|
|
}
|
|
|
|
.analytics-table th {
|
|
font-size: 0.8rem;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
padding: 0.75rem 1rem;
|
|
}
|
|
.analytics-table td {
|
|
padding: 0.6rem 1rem;
|
|
vertical-align: middle;
|
|
}
|
|
.rank-badge {
|
|
width: 26px;
|
|
height: 26px;
|
|
border-radius: 4px;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-family: 'Montserrat', sans-serif;
|
|
font-weight: 700;
|
|
font-size: 0.75rem;
|
|
color: var(--brand-dark);
|
|
background: var(--brand-yellow);
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.stat-tile {
|
|
flex-direction: column;
|
|
text-align: center;
|
|
padding: 0.9rem 0.9rem 0.9rem 1.2rem;
|
|
}
|
|
|
|
.stat-tile .stat-tile-icon {
|
|
margin-bottom: 0.35rem;
|
|
}
|
|
|
|
.stat-tile .stat-tile-value {
|
|
font-size: 1.25rem;
|
|
}
|
|
|
|
.table-responsive {
|
|
font-size: 0.875rem;
|
|
}
|
|
}
|
|
|
|
/* Audit tab styles */
|
|
.risk-critical { background-color: #dc3545; color: white; }
|
|
.risk-high { background-color: #fd7e14; color: white; }
|
|
.risk-medium { background-color: #ffc107; color: #212529; }
|
|
.risk-low { background-color: #28a745; color: white; }
|
|
|
|
.cat-1 { background-color: #28a745; color: white; }
|
|
.cat-1b { background-color: #17a2b8; color: white; }
|
|
.cat-2 { background-color: #6f42c1; color: white; }
|
|
.cat-3 { background-color: #dc3545; color: white; }
|
|
|
|
.audit-status-flagged { border-left: 4px solid #dc3545; }
|
|
.audit-status-reviewed { border-left: 4px solid #ffc107; }
|
|
.audit-status-cleared { border-left: 4px solid #28a745; }
|
|
|
|
.audit-section { margin-bottom: 1rem; }
|
|
.audit-section h6 { font-size: 0.85rem; font-weight: 600; color: #6c757d; text-transform: uppercase; margin-bottom: 0.5rem; }
|
|
.audit-instructions-pre { max-height: 300px; overflow-y: auto; font-size: 0.8rem; background: #f8f9fa; padding: 0.75rem; border-radius: 4px; white-space: pre-wrap; word-wrap: break-word; }
|
|
</style>
|
|
|
|
<script>
|
|
let allUsers = [];
|
|
let allAgents = [];
|
|
let verificationAgents = [];
|
|
let auditAgents = [];
|
|
let currentAuditAgentId = null;
|
|
const isReadonlyAdmin = {% if current_user and current_user.get('role') == 'readonly_admin' %}true{% else %}false{% endif %};
|
|
const isFullAdmin = {% if current_user and current_user.get('is_admin') %}true{% else %}false{% endif %};
|
|
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
loadAdminData();
|
|
loadAnalytics();
|
|
loadVerificationData();
|
|
loadAuditResults();
|
|
loadUnresolvedOwners();
|
|
setupEventListeners();
|
|
|
|
// Hide write-action buttons for readonly admins
|
|
if (isReadonlyAdmin && !isFullAdmin) {
|
|
document.querySelectorAll('.admin-write-action').forEach(el => el.style.display = 'none');
|
|
}
|
|
});
|
|
|
|
async function loadUnresolvedOwners() {
|
|
if (!isFullAdmin) return; // readonly admins can't reassign
|
|
try {
|
|
const r = await fetch('{{ base_path }}/api/admin/agents/unresolved-owner', { credentials: 'include' });
|
|
if (!r.ok) return;
|
|
const list = await r.json();
|
|
if (!list.length) return;
|
|
document.getElementById('unresolvedOwnerRow').style.display = '';
|
|
document.getElementById('unresolvedOwnerCount').textContent =
|
|
list.length + (list.length === 1 ? ' agent has' : ' agents have') + ' an unresolved owner';
|
|
const tbody = document.getElementById('unresolvedOwnerTableBody');
|
|
tbody.innerHTML = list.map(a => `
|
|
<tr data-agent-id="${a.agent_id}">
|
|
<td><strong>${a.agent_name || '(no name)'}</strong></td>
|
|
<td><code>${a.agent_contact_person || '(empty)'}</code></td>
|
|
<td><input type="email" class="form-control form-control-sm reassign-email" placeholder="user@oliver.agency"></td>
|
|
<td><button class="btn btn-sm btn-primary reassign-btn">Reassign</button></td>
|
|
</tr>
|
|
`).join('');
|
|
tbody.querySelectorAll('.reassign-btn').forEach(btn => {
|
|
btn.addEventListener('click', async function() {
|
|
const tr = btn.closest('tr');
|
|
const agentId = tr.dataset.agentId;
|
|
const email = tr.querySelector('.reassign-email').value.trim();
|
|
if (!email) { alert('Enter an email'); return; }
|
|
btn.disabled = true; btn.textContent = '…';
|
|
try {
|
|
const r = await fetch(`{{ base_path }}/api/admin/agents/${agentId}/reassign-owner`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ new_contact_email: email }),
|
|
});
|
|
const data = await r.json();
|
|
if (!r.ok) { alert(data.detail || 'Failed'); btn.disabled = false; btn.textContent = 'Reassign'; return; }
|
|
tr.style.opacity = '0.5';
|
|
btn.textContent = '✓ Done';
|
|
} catch (e) {
|
|
alert('Error: ' + e.message);
|
|
btn.disabled = false; btn.textContent = 'Reassign';
|
|
}
|
|
});
|
|
});
|
|
} catch (e) { /* non-blocking */ }
|
|
}
|
|
|
|
function setupEventListeners() {
|
|
document.getElementById('refreshBtn').addEventListener('click', () => { loadAdminData(); loadAnalytics(); loadVerificationData(); loadAuditResults(); });
|
|
document.getElementById('userSearch').addEventListener('input', filterUsers);
|
|
document.getElementById('agentSearch').addEventListener('input', filterAgents);
|
|
document.getElementById('agentStatusFilter').addEventListener('change', filterAgents);
|
|
document.getElementById('agentDisciplineFilter')?.addEventListener('change', filterAgents);
|
|
document.getElementById('agentAuditFilter').addEventListener('change', filterAgents);
|
|
document.getElementById('agentBusinessEntityFilter')?.addEventListener('change', filterAgents);
|
|
document.getElementById('agentTypeFilter')?.addEventListener('change', filterAgents);
|
|
document.getElementById('agentAutonomyFilter')?.addEventListener('change', filterAgents);
|
|
document.getElementById('adminComplianceRiskBtn')?.addEventListener('click', function() {
|
|
adminComplianceRiskOnly = !adminComplianceRiskOnly;
|
|
this.classList.toggle('btn-outline-danger', !adminComplianceRiskOnly);
|
|
this.classList.toggle('btn-danger', adminComplianceRiskOnly);
|
|
this.innerHTML = adminComplianceRiskOnly
|
|
? '<i class="fas fa-times me-1"></i>Clear risks'
|
|
: '<i class="fas fa-shield-alt me-1"></i>Risks';
|
|
filterAgents();
|
|
});
|
|
document.getElementById('exportFilteredXlsxBtn')?.addEventListener('click', () => {
|
|
const params = new URLSearchParams();
|
|
const setIf = (k, v) => { if (v) params.set(k, v); };
|
|
setIf('status', document.getElementById('agentStatusFilter').value);
|
|
setIf('discipline', document.getElementById('agentDisciplineFilter')?.value);
|
|
setIf('audit', document.getElementById('agentAuditFilter').value);
|
|
setIf('business_entity', document.getElementById('agentBusinessEntityFilter')?.value);
|
|
setIf('agent_classification', document.getElementById('agentTypeFilter')?.value);
|
|
setIf('autonomy_level', document.getElementById('agentAutonomyFilter')?.value);
|
|
if (adminComplianceRiskOnly) params.set('risks_only', 'true');
|
|
setIf('search', document.getElementById('agentSearch').value.trim());
|
|
const qs = params.toString();
|
|
window.location.href = '{{ base_path }}/api/admin/agents/export/xlsx' + (qs ? '?' + qs : '');
|
|
});
|
|
document.getElementById('editUserForm').addEventListener('submit', handleEditUserSubmit);
|
|
document.getElementById('editAgentForm').addEventListener('submit', handleEditAgentSubmit);
|
|
document.getElementById('createUserForm').addEventListener('submit', handleCreateUserSubmit);
|
|
document.getElementById('resetPasswordForm').addEventListener('submit', handleResetPasswordSubmit);
|
|
document.getElementById('analyticsApplyBtn').addEventListener('click', loadAnalytics);
|
|
}
|
|
|
|
async function loadAdminData() {
|
|
try {
|
|
// Load users and agents in parallel
|
|
const [usersResponse, agentsResponse] = await Promise.all([
|
|
fetch('{{ base_path }}/api/admin/users', {
|
|
credentials: 'include'
|
|
}),
|
|
fetch('{{ base_path }}/api/admin/agents', {
|
|
credentials: 'include'
|
|
})
|
|
]);
|
|
|
|
if (usersResponse.status === 401 || agentsResponse.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
return;
|
|
}
|
|
|
|
if (usersResponse.status === 403 || agentsResponse.status === 403) {
|
|
alert('Admin access required');
|
|
window.location.href = '{{ base_path }}/dashboard';
|
|
return;
|
|
}
|
|
|
|
if (usersResponse.ok && agentsResponse.ok) {
|
|
allUsers = await usersResponse.json();
|
|
allAgents = await agentsResponse.json();
|
|
|
|
updateStatistics();
|
|
displayUsers(allUsers);
|
|
displayAgents(allAgents);
|
|
} else {
|
|
throw new Error('Failed to load admin data');
|
|
}
|
|
} catch (error) {
|
|
console.error('Error loading admin data:', error);
|
|
showError('Failed to load admin data');
|
|
}
|
|
}
|
|
|
|
function updateStatistics() {
|
|
// These two are still fed from the users/agents lists for the management tabs
|
|
// The stat cards are now primarily updated by loadAnalytics()
|
|
// Keep this as a fallback for basic counts from the existing load
|
|
}
|
|
|
|
function updateAnalyticsStats(data) {
|
|
document.getElementById('totalUsers').textContent = formatNumber(data.total_users || 0);
|
|
document.getElementById('totalAgents').textContent = formatNumber(data.total_agents || 0);
|
|
document.getElementById('totalMessages').textContent = formatNumber(data.summary.total_messages || 0);
|
|
document.getElementById('totalTokens').textContent = formatNumber(data.summary.total_tokens || 0);
|
|
document.getElementById('totalConversations').textContent = formatNumber(data.summary.conversation_count || 0);
|
|
document.getElementById('totalUniqueUsers').textContent = formatNumber(data.summary.unique_users || 0);
|
|
}
|
|
|
|
function formatNumber(n) {
|
|
if (n >= 1000000000) return (n / 1000000000).toFixed(1) + 'B';
|
|
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
return n.toString();
|
|
}
|
|
|
|
function displayUsers(users) {
|
|
const tbody = document.getElementById('usersTableBody');
|
|
|
|
if (users.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center py-4">No users found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const usersHtml = users.map(user => {
|
|
const userAgents = allAgents.filter(agent => agent.created_by === user.email).length;
|
|
const authProvider = user.auth_provider || 'local';
|
|
const isLocalAuth = authProvider === 'local';
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="user-avatar-sm me-2">
|
|
${(user.full_name || user.email)[0].toUpperCase()}
|
|
</div>
|
|
<div>
|
|
<div class="fw-medium">${user.full_name || 'No Name'}</div>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${user.email}</td>
|
|
<td>
|
|
<span class="badge ${isLocalAuth ? 'bg-secondary' : 'bg-info'}">
|
|
${isLocalAuth ? 'Local' : 'SSO'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
${(() => {
|
|
const role = user.role || (user.is_admin ? 'admin' : 'user');
|
|
if (role === 'admin') return '<span class="badge bg-danger">Admin</span>';
|
|
if (role === 'readonly_admin') return '<span class="badge bg-warning text-dark">Read-Only Admin</span>';
|
|
return '<span class="badge bg-primary">User</span>';
|
|
})()}
|
|
</td>
|
|
<td>
|
|
<span class="badge ${user.is_active ? 'bg-success' : 'bg-secondary'}">
|
|
${user.is_active ? 'Active' : 'Inactive'}
|
|
</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-info">${userAgents}</span>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-outline-primary btn-sm me-1" onclick="viewUserDetails('${user.email}')" title="View">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm me-1 admin-write-action" onclick="editUser('${user.email}')" title="Edit">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm admin-write-action" onclick="toggleUserStatus('${user.email}')" title="${user.is_active ? 'Deactivate' : 'Activate'}">
|
|
<i class="fas fa-${user.is_active ? 'ban' : 'check'}"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
tbody.innerHTML = usersHtml;
|
|
}
|
|
|
|
function displayAgents(agents) {
|
|
const tbody = document.getElementById('agentsTableBody');
|
|
|
|
if (agents.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="5" class="text-center py-4">No agents found</td></tr>';
|
|
return;
|
|
}
|
|
|
|
const agentsHtml = agents.map(agent => {
|
|
const owner = allUsers.find(u => u.email === agent.created_by);
|
|
const ownerName = owner?.full_name || agent.agent_contact_person || 'Unknown';
|
|
const ownerEmail = owner?.email || agent.created_by;
|
|
const avatarText = (owner?.full_name || agent.agent_contact_person || 'U')[0].toUpperCase();
|
|
|
|
return `
|
|
<tr>
|
|
<td>
|
|
<div class="fw-medium">${agent.agent_name}
|
|
${agent.verification_status === 'needs_verification' ? ' <span class="badge bg-warning text-dark" style="font-size:0.65em;">Needs Verification</span>' : ''}
|
|
${agent.verification_status === 'verified' ? ' <span class="badge bg-success" style="font-size:0.65em;">Verified</span>' : ''}
|
|
</div>
|
|
${agent.agent_description ? `<div class="text-muted small mt-1" style="max-height: 80px; overflow-y: auto; border: 1px solid #e2e8f0; border-radius: 4px; padding: 6px 8px; background: #f8f9fa;">${agent.agent_description}</div>` : '<small class="text-muted">No description</small>'}
|
|
${agent.instructions ? `<div class="small mt-1"><span class="badge bg-info text-dark" style="font-size:0.65em;">Instructions</span><div class="text-muted" style="max-height: 60px; overflow-y: auto; border: 1px solid #d1ecf1; border-radius: 4px; padding: 6px 8px; background: #e8f4f8; margin-top: 4px; white-space: pre-wrap; font-size: 0.8em;">${agent.instructions}</div></div>` : ''}
|
|
</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="user-avatar-sm me-2">
|
|
${avatarText}
|
|
</div>
|
|
<div>
|
|
<div class="fw-medium">${ownerName}</div>
|
|
<small class="text-muted">${ownerEmail}</small>
|
|
</div>
|
|
</div>
|
|
</td>
|
|
<td>${agent.agent_version || 'N/A'}</td>
|
|
<td>
|
|
<small class="text-muted">${formatDate(agent.agent_created_at)}</small>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-outline-primary btn-sm me-1" onclick="viewAgentDetails('${agent.agent_id}')">
|
|
<i class="fas fa-eye"></i>
|
|
</button>
|
|
<button class="btn btn-outline-warning btn-sm me-1 admin-write-action" onclick="editAgentAdmin('${agent.agent_id}')">
|
|
<i class="fas fa-edit"></i>
|
|
</button>
|
|
<button class="btn btn-outline-danger btn-sm admin-write-action" onclick="deleteAgentAdmin('${agent.agent_id}')">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}).join('');
|
|
|
|
tbody.innerHTML = agentsHtml;
|
|
}
|
|
|
|
function filterUsers() {
|
|
const searchTerm = document.getElementById('userSearch').value.toLowerCase();
|
|
const filtered = allUsers.filter(user =>
|
|
(user.full_name || '').toLowerCase().includes(searchTerm) ||
|
|
user.email.toLowerCase().includes(searchTerm)
|
|
);
|
|
displayUsers(filtered);
|
|
}
|
|
|
|
let adminComplianceRiskOnly = false;
|
|
|
|
function filterAgents() {
|
|
const searchTerm = document.getElementById('agentSearch').value.toLowerCase();
|
|
const statusFilter = document.getElementById('agentStatusFilter').value;
|
|
const disciplineFilter = document.getElementById('agentDisciplineFilter')?.value || '';
|
|
const auditFilter = document.getElementById('agentAuditFilter').value;
|
|
const entityFilter = document.getElementById('agentBusinessEntityFilter')?.value || '';
|
|
const typeFilter = document.getElementById('agentTypeFilter')?.value || '';
|
|
const autonomyFilter = document.getElementById('agentAutonomyFilter')?.value || '';
|
|
|
|
let filtered = allAgents.filter(agent => {
|
|
const matchesSearch = agent.agent_name.toLowerCase().includes(searchTerm) ||
|
|
(agent.agent_description || '').toLowerCase().includes(searchTerm);
|
|
const matchesStatus = !statusFilter || agent.agent_status === statusFilter;
|
|
const matchesDiscipline = !disciplineFilter || agent.discipline === disciplineFilter;
|
|
const matchesAudit = !auditFilter ||
|
|
(auditFilter === 'audited' && agent.quality_audit_status) ||
|
|
(auditFilter === 'not_audited' && !agent.quality_audit_status);
|
|
const matchesEntity = !entityFilter || agent.business_entity === entityFilter;
|
|
const matchesType = !typeFilter || agent.agent_classification === typeFilter;
|
|
const matchesAutonomy = !autonomyFilter || agent.autonomy_level === autonomyFilter;
|
|
const matchesCompliance = !adminComplianceRiskOnly || (
|
|
((agent.pii && agent.pii.handles_pii === true)) ||
|
|
(agent.ip_ownership === 'Shared/TBD') ||
|
|
(agent.autonomy_level === 'Autopilot')
|
|
);
|
|
return matchesSearch && matchesStatus && matchesDiscipline && matchesAudit
|
|
&& matchesEntity && matchesType && matchesAutonomy && matchesCompliance;
|
|
});
|
|
|
|
displayAgents(filtered);
|
|
}
|
|
|
|
function viewUserDetails(email) {
|
|
const user = allUsers.find(u => u.email === email);
|
|
const userAgents = allAgents.filter(agent => agent.created_by === email);
|
|
|
|
alert(`User Details:\nName: ${user.full_name || 'No Name'}\nEmail: ${user.email}\nType: ${user.is_admin ? 'Admin' : 'User'}\nStatus: ${user.is_active ? 'Active' : 'Inactive'}\nAgents Created: ${userAgents.length}`);
|
|
}
|
|
|
|
async function viewAgentDetails(agentId) {
|
|
try {
|
|
console.log('DEBUG: Admin fetching fresh agent details for ID:', agentId);
|
|
|
|
// Fetch fresh agent data from server
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
return;
|
|
} else if (response.status === 403) {
|
|
showError('Not authorized to view this agent');
|
|
return;
|
|
} else {
|
|
showError('Failed to load agent details');
|
|
return;
|
|
}
|
|
}
|
|
|
|
const agent = await response.json();
|
|
console.log('DEBUG: Admin fresh agent data loaded:', agent);
|
|
|
|
const qualityAuditText = agent.quality_audit_status ? 'Audited' : 'Not Audited';
|
|
const auditTrail = agent.quality_audit_updated_by_name && agent.quality_audit_updated_at ?
|
|
`\nQuality Audit: ${qualityAuditText} by ${agent.quality_audit_updated_by_name} on ${formatDate(agent.quality_audit_updated_at)}` :
|
|
`\nQuality Audit: ${qualityAuditText}`;
|
|
|
|
const lastEditedText = agent.last_edited_by ? `\nLast Edited By: ${agent.last_edited_by}` : '';
|
|
const instructionsText = agent.instructions ? `\n\nInstructions (System Prompt):\n${agent.instructions}` : '';
|
|
alert(`Agent Details:\nName: ${agent.agent_name}\nStatus: ${agent.agent_status || 'Development'}\nVersion: ${agent.agent_version || 'N/A'}\nDescription: ${agent.agent_description || 'No description'}\nOwner: ${agent.created_by}${lastEditedText}${auditTrail}${instructionsText}`);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading agent details:', error);
|
|
showError('Failed to load agent details. Please try again.');
|
|
}
|
|
}
|
|
|
|
function editUser(email) {
|
|
const user = allUsers.find(u => u.email === email);
|
|
if (!user) return;
|
|
|
|
const authProvider = user.auth_provider || 'local';
|
|
const isLocalAuth = authProvider === 'local';
|
|
|
|
// Determine role from user data
|
|
const userRole = user.role || (user.is_admin ? 'admin' : 'user');
|
|
|
|
// Populate the edit form
|
|
document.getElementById('editUserId').value = user.email;
|
|
document.getElementById('editUserEmail').value = user.email;
|
|
document.getElementById('editUserFullName').value = user.full_name || '';
|
|
document.getElementById('editUserIsActive').checked = user.is_active;
|
|
document.getElementById('editUserIsAdmin').checked = user.is_admin;
|
|
document.getElementById('editUserRole').value = userRole;
|
|
document.getElementById('editUserAuthProvider').value = authProvider;
|
|
|
|
// Update auth display
|
|
const authDisplay = document.getElementById('editUserAuthDisplay');
|
|
if (isLocalAuth) {
|
|
authDisplay.innerHTML = '<span class="badge bg-secondary"><i class="fas fa-key me-1"></i>Local (Email/Password)</span>';
|
|
} else {
|
|
authDisplay.innerHTML = '<span class="badge bg-info"><i class="fab fa-microsoft me-1"></i>Microsoft SSO</span>';
|
|
}
|
|
|
|
// Show/hide password reset section based on auth provider
|
|
const passwordResetSection = document.getElementById('passwordResetSection');
|
|
passwordResetSection.style.display = isLocalAuth ? 'block' : 'none';
|
|
|
|
// Show the modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editUserModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function toggleUserStatus(email) {
|
|
const user = allUsers.find(u => u.email === email);
|
|
if (!user) return;
|
|
|
|
const newStatus = !user.is_active;
|
|
const action = newStatus ? 'activate' : 'deactivate';
|
|
|
|
if (!confirm(`Are you sure you want to ${action} this user?`)) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/users/${encodeURIComponent(email)}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
full_name: user.full_name,
|
|
is_active: newStatus,
|
|
is_admin: user.is_admin,
|
|
role: user.role || (user.is_admin ? 'admin' : 'user')
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadAdminData();
|
|
showSuccess(`User ${action}d successfully`);
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || `Failed to ${action} user`);
|
|
}
|
|
} catch (error) {
|
|
showError(`Failed to ${action} user`);
|
|
}
|
|
}
|
|
|
|
async function deleteAgentAdmin(agentId) {
|
|
if (!confirm('Are you sure you want to delete this agent? This action cannot be undone.')) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
await loadAdminData();
|
|
showSuccess('Agent deleted successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to delete agent');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to delete agent');
|
|
}
|
|
}
|
|
|
|
function formatDate(dateString) {
|
|
if (!dateString) return 'N/A';
|
|
const date = new Date(dateString);
|
|
return date.toLocaleDateString();
|
|
}
|
|
|
|
function getRiskFactorLabel(riskFactor) {
|
|
const labels = {
|
|
1: '1 - Very Low Risk',
|
|
2: '2 - Low Risk',
|
|
3: '3 - Medium Risk',
|
|
4: '4 - High Risk',
|
|
5: '5 - Very High Risk'
|
|
};
|
|
return labels[riskFactor] || 'Not Set';
|
|
}
|
|
|
|
function toggleAdminRiskFactor() {
|
|
const qualityAuditStatus = document.getElementById('editQualityAuditStatus');
|
|
const riskFactorSection = document.getElementById('editRiskFactorSection');
|
|
const riskFactor = document.getElementById('editRiskFactor');
|
|
|
|
if (qualityAuditStatus.checked) {
|
|
riskFactorSection.style.display = 'block';
|
|
riskFactor.required = true;
|
|
} else {
|
|
riskFactorSection.style.display = 'none';
|
|
riskFactor.required = false;
|
|
riskFactor.value = '';
|
|
}
|
|
}
|
|
|
|
function showSuccess(message) {
|
|
alert(message);
|
|
}
|
|
|
|
function showError(message) {
|
|
alert('Error: ' + message);
|
|
}
|
|
|
|
async function handleEditUserSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const email = document.getElementById('editUserId').value;
|
|
const fullName = document.getElementById('editUserFullName').value;
|
|
const isActive = document.getElementById('editUserIsActive').checked;
|
|
const role = document.getElementById('editUserRole').value;
|
|
const isAdmin = (role === 'admin');
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/users/${encodeURIComponent(email)}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
full_name: fullName,
|
|
is_active: isActive,
|
|
is_admin: isAdmin,
|
|
role: role
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
|
|
modal.hide();
|
|
|
|
// Reload data and show success
|
|
await loadAdminData();
|
|
showSuccess('User updated successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to update user');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to update user');
|
|
}
|
|
}
|
|
|
|
async function editAgentAdmin(agentId) {
|
|
try {
|
|
console.log('DEBUG: Admin fetching fresh agent data for editing:', agentId);
|
|
|
|
// Fetch fresh agent data from server
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (!response.ok) {
|
|
if (response.status === 401) {
|
|
window.location.href = '{{ base_path }}/login';
|
|
return;
|
|
} else if (response.status === 403) {
|
|
showError('Not authorized to edit this agent');
|
|
return;
|
|
} else {
|
|
showError('Failed to load agent for editing');
|
|
return;
|
|
}
|
|
}
|
|
|
|
const agent = await response.json();
|
|
console.log('DEBUG: Admin fresh agent data for editing:', agent);
|
|
|
|
// Populate edit form
|
|
document.getElementById('editAgentId').value = agent.agent_id;
|
|
document.getElementById('editAgentName').value = agent.agent_name;
|
|
document.getElementById('editAgentTool').value = agent.agent_tool || '';
|
|
document.getElementById('editAgentStatus').value = agent.agent_status || 'Development';
|
|
document.getElementById('editAgentDescription').value = agent.agent_description || '';
|
|
document.getElementById('editAgentPurpose').value = agent.agent_purpose || '';
|
|
document.getElementById('editAgentVersion').value = agent.agent_version || '';
|
|
document.getElementById('editAgentLocation').value = agent.agent_location || '';
|
|
document.getElementById('editAgentDepartment').value = agent.agent_department || '';
|
|
document.getElementById('editAgentContact').value = agent.agent_contact_person || '';
|
|
document.getElementById('editAgentTags').value = agent.agent_tags ? agent.agent_tags.join(', ') : '';
|
|
document.getElementById('editAgentUserbase').value = agent.agent_userbase ? agent.agent_userbase.join(', ') : '';
|
|
document.getElementById('editAgentCapabilities').value = agent.agent_capabilities ? agent.agent_capabilities.join(', ') : '';
|
|
document.getElementById('editAgentDiscipline').value = agent.discipline || '';
|
|
|
|
// Handle Quality Audit field (admin always has access)
|
|
document.getElementById('editQualityAuditStatus').checked = agent.quality_audit_status || false;
|
|
|
|
// Update the note with audit trail information
|
|
let noteHtml = 'Check this box if the agent has passed quality audit review.';
|
|
if (agent.quality_audit_updated_by_name && agent.quality_audit_updated_at) {
|
|
const action = agent.quality_audit_status ? 'checked' : 'unchecked';
|
|
const date = formatDate(agent.quality_audit_updated_at);
|
|
noteHtml += `<br><br><small class="text-info"><i class="fas fa-history me-1"></i><strong>${agent.quality_audit_updated_by_name}</strong> ${action} Quality Audit on ${date}</small>`;
|
|
} else {
|
|
noteHtml += '<br><br><small class="text-muted"><i class="fas fa-info-circle me-1"></i>No quality audit changes recorded yet.</small>';
|
|
}
|
|
document.getElementById('editQualityAuditNote').innerHTML = noteHtml;
|
|
|
|
// Handle Risk Factor field
|
|
const riskFactorSection = document.getElementById('editRiskFactorSection');
|
|
const riskFactor = document.getElementById('editRiskFactor');
|
|
riskFactor.value = agent.risk_factor || '';
|
|
|
|
if (agent.quality_audit_status) {
|
|
riskFactorSection.style.display = 'block';
|
|
riskFactor.required = true;
|
|
} else {
|
|
riskFactorSection.style.display = 'none';
|
|
riskFactor.required = false;
|
|
}
|
|
|
|
// Show edit modal
|
|
const modal = new bootstrap.Modal(document.getElementById('editAgentModal'));
|
|
modal.show();
|
|
|
|
} catch (error) {
|
|
console.error('Error loading agent for editing:', error);
|
|
showError('Failed to load agent for editing. Please try again.');
|
|
}
|
|
}
|
|
|
|
async function handleEditAgentSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const agentId = document.getElementById('editAgentId').value;
|
|
const agentData = {
|
|
agent_name: document.getElementById('editAgentName').value,
|
|
agent_tool: document.getElementById('editAgentTool').value,
|
|
agent_status: document.getElementById('editAgentStatus').value,
|
|
agent_description: document.getElementById('editAgentDescription').value,
|
|
agent_purpose: document.getElementById('editAgentPurpose').value,
|
|
agent_version: document.getElementById('editAgentVersion').value,
|
|
agent_location: document.getElementById('editAgentLocation').value,
|
|
agent_department: document.getElementById('editAgentDepartment').value,
|
|
agent_contact_person: document.getElementById('editAgentContact').value,
|
|
agent_tags: document.getElementById('editAgentTags').value.split(',').map(s => s.trim()).filter(s => s),
|
|
agent_userbase: document.getElementById('editAgentUserbase').value.split(',').map(s => s.trim()).filter(s => s),
|
|
agent_capabilities: document.getElementById('editAgentCapabilities').value.split(',').map(s => s.trim()).filter(s => s),
|
|
discipline: document.getElementById('editAgentDiscipline').value || null,
|
|
quality_audit_status: document.getElementById('editQualityAuditStatus').checked
|
|
};
|
|
|
|
// Add Risk Factor
|
|
const riskFactorValue = document.getElementById('editRiskFactor').value;
|
|
agentData.risk_factor = riskFactorValue ? parseInt(riskFactorValue) : null;
|
|
|
|
// Validate Risk Factor if Quality Audit is checked
|
|
if (agentData.quality_audit_status && (!agentData.risk_factor || agentData.risk_factor < 1 || agentData.risk_factor > 5)) {
|
|
showError('Risk Factor is required when Quality Audit is checked!');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/agents/${agentId}`, {
|
|
method: 'PUT',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify(agentData)
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('editAgentModal'));
|
|
modal.hide();
|
|
|
|
// Reload data and show success
|
|
await loadAdminData();
|
|
showSuccess('Agent updated successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to update agent');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to update agent');
|
|
}
|
|
}
|
|
|
|
function logout() {
|
|
localStorage.removeItem('access_token');
|
|
window.location.href = '{{ base_path }}/';
|
|
}
|
|
|
|
// Create User Functions
|
|
function showCreateUserModal() {
|
|
// Clear the form
|
|
document.getElementById('createUserForm').reset();
|
|
|
|
// Show the modal
|
|
const modal = new bootstrap.Modal(document.getElementById('createUserModal'));
|
|
modal.show();
|
|
}
|
|
|
|
async function handleCreateUserSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const email = document.getElementById('createUserEmail').value;
|
|
const fullName = document.getElementById('createUserFullName').value;
|
|
const password = document.getElementById('createUserPassword').value;
|
|
const passwordConfirm = document.getElementById('createUserPasswordConfirm').value;
|
|
const isAdmin = document.getElementById('createUserIsAdmin').checked;
|
|
|
|
// Validate passwords match
|
|
if (password !== passwordConfirm) {
|
|
showError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (password.length < 8) {
|
|
showError('Password must be at least 8 characters');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/admin/users', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
email: email,
|
|
full_name: fullName || null,
|
|
password: password,
|
|
is_admin: isAdmin
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('createUserModal'));
|
|
modal.hide();
|
|
|
|
// Reload data and show success
|
|
await loadAdminData();
|
|
showSuccess('User created successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to create user');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to create user');
|
|
}
|
|
}
|
|
|
|
// Reset Password Functions
|
|
function showResetPasswordModal() {
|
|
const email = document.getElementById('editUserId').value;
|
|
|
|
// Set the email in the reset password modal
|
|
document.getElementById('resetPasswordEmail').value = email;
|
|
document.getElementById('resetPasswordUserEmail').textContent = email;
|
|
|
|
// Clear password fields
|
|
document.getElementById('newPassword').value = '';
|
|
document.getElementById('newPasswordConfirm').value = '';
|
|
|
|
// Hide edit modal and show reset password modal
|
|
const editModal = bootstrap.Modal.getInstance(document.getElementById('editUserModal'));
|
|
editModal.hide();
|
|
|
|
const resetModal = new bootstrap.Modal(document.getElementById('resetPasswordModal'));
|
|
resetModal.show();
|
|
}
|
|
|
|
async function handleResetPasswordSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const email = document.getElementById('resetPasswordEmail').value;
|
|
const newPassword = document.getElementById('newPassword').value;
|
|
const newPasswordConfirm = document.getElementById('newPasswordConfirm').value;
|
|
|
|
// Validate passwords match
|
|
if (newPassword !== newPasswordConfirm) {
|
|
showError('Passwords do not match');
|
|
return;
|
|
}
|
|
|
|
// Validate password length
|
|
if (newPassword.length < 8) {
|
|
showError('Password must be at least 8 characters');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/users/${encodeURIComponent(email)}/reset-password`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
credentials: 'include',
|
|
body: JSON.stringify({
|
|
new_password: newPassword
|
|
})
|
|
});
|
|
|
|
if (response.ok) {
|
|
// Hide the modal
|
|
const modal = bootstrap.Modal.getInstance(document.getElementById('resetPasswordModal'));
|
|
modal.hide();
|
|
|
|
showSuccess('Password reset successfully');
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to reset password');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to reset password');
|
|
}
|
|
}
|
|
|
|
// ===================== Analytics =====================
|
|
let analyticsCharts = {};
|
|
|
|
function destroyChart(key) {
|
|
if (analyticsCharts[key]) {
|
|
analyticsCharts[key].destroy();
|
|
analyticsCharts[key] = null;
|
|
}
|
|
}
|
|
|
|
const STATUS_COLORS = {
|
|
'Active': '#28a745',
|
|
'Development': '#ffc107',
|
|
'Inactive': '#dc3545',
|
|
'Deprecated': '#6c757d'
|
|
};
|
|
|
|
const DISCIPLINE_COLORS = [
|
|
'#FFCB05', '#1A1A1A', '#FF5C00', '#4e79a7', '#59a14f', '#af7aa1', '#76b7b2'
|
|
];
|
|
|
|
async function loadAnalytics() {
|
|
try {
|
|
const status = document.getElementById('analyticsStatusFilter').value;
|
|
const discipline = document.getElementById('analyticsDisciplineFilter').value;
|
|
const days = document.getElementById('analyticsTimeRange').value;
|
|
|
|
const params = new URLSearchParams();
|
|
if (status) params.set('status', status);
|
|
if (discipline) params.set('discipline', discipline);
|
|
params.set('days', days);
|
|
|
|
const qs = params.toString();
|
|
const url = '{{ base_path }}/api/admin/analytics' + (qs ? '?' + qs : '');
|
|
const response = await fetch(url, { credentials: 'include' });
|
|
if (!response.ok) return;
|
|
const data = await response.json();
|
|
|
|
// Update time range label in chart header
|
|
const label = document.getElementById('timeRangeLabel');
|
|
if (label) label.textContent = '(Last ' + days + ' Days)';
|
|
|
|
updateAnalyticsStats(data);
|
|
renderUsageTimeline(data.usage_timeline);
|
|
renderTokenBreakdown(data.summary.prompt_tokens, data.summary.completion_tokens);
|
|
renderStatusBreakdown(data.status_breakdown);
|
|
renderDisciplineBreakdown(data.discipline_breakdown);
|
|
renderRatingDistribution(data.rating_distribution);
|
|
renderTopTable('topMessagesTbody', data.top_by_messages, 'total_messages');
|
|
renderTopTable('topTokensTbody', data.top_by_tokens, 'total_tokens');
|
|
renderRecentlyActive(data.recently_active);
|
|
} catch (err) {
|
|
console.error('Failed to load analytics:', err);
|
|
}
|
|
}
|
|
|
|
function renderUsageTimeline(timeline) {
|
|
destroyChart('usageTimeline');
|
|
const canvas = document.getElementById('usageTimelineChart');
|
|
const emptyEl = document.getElementById('usageTimelineEmpty');
|
|
|
|
if (!timeline || timeline.length === 0) {
|
|
canvas.style.display = 'none';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
canvas.style.display = 'block';
|
|
emptyEl.style.display = 'none';
|
|
|
|
const labels = timeline.map(t => t.date);
|
|
const messages = timeline.map(t => t.message_count);
|
|
const tokens = timeline.map(t => t.token_count);
|
|
const hasTokens = tokens.some(v => v > 0);
|
|
|
|
const datasets = [{
|
|
label: 'Messages',
|
|
data: messages,
|
|
borderColor: '#1A1A1A',
|
|
backgroundColor: 'rgba(26,26,26,0.08)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
yAxisID: 'y'
|
|
}];
|
|
|
|
const scales = {
|
|
y: { beginAtZero: true, title: { display: true, text: 'Messages' }, position: 'left' },
|
|
x: { ticks: { maxTicksLimit: 12 } }
|
|
};
|
|
|
|
if (hasTokens) {
|
|
datasets.push({
|
|
label: 'Tokens',
|
|
data: tokens,
|
|
borderColor: '#FFCB05',
|
|
backgroundColor: 'rgba(255,203,5,0.18)',
|
|
fill: true,
|
|
tension: 0.3,
|
|
yAxisID: 'y1'
|
|
});
|
|
scales.y1 = { beginAtZero: true, title: { display: true, text: 'Tokens' }, position: 'right', grid: { drawOnChartArea: false } };
|
|
}
|
|
|
|
analyticsCharts.usageTimeline = new Chart(canvas, {
|
|
type: 'line',
|
|
data: { labels, datasets },
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
interaction: { mode: 'index', intersect: false },
|
|
plugins: { legend: { position: 'top' } },
|
|
scales
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderTokenBreakdown(prompt, completion) {
|
|
destroyChart('tokenBreakdown');
|
|
const canvas = document.getElementById('tokenBreakdownChart');
|
|
const emptyEl = document.getElementById('tokenBreakdownEmpty');
|
|
|
|
if (!prompt && !completion) {
|
|
canvas.style.display = 'none';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
canvas.style.display = 'block';
|
|
emptyEl.style.display = 'none';
|
|
|
|
analyticsCharts.tokenBreakdown = new Chart(canvas, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['Prompt (Input)', 'Completion (Output)'],
|
|
datasets: [{ data: [prompt, completion], backgroundColor: ['#1A1A1A', '#FFCB05'] }]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { position: 'bottom' } }
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderStatusBreakdown(statusData) {
|
|
destroyChart('statusBreakdown');
|
|
const canvas = document.getElementById('statusBreakdownChart');
|
|
const emptyEl = document.getElementById('statusBreakdownEmpty');
|
|
|
|
const entries = Object.entries(statusData || {}).filter(([, v]) => v > 0);
|
|
if (entries.length === 0) {
|
|
canvas.style.display = 'none';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
canvas.style.display = 'block';
|
|
emptyEl.style.display = 'none';
|
|
|
|
analyticsCharts.statusBreakdown = new Chart(canvas, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: entries.map(([k]) => k || 'Unknown'),
|
|
datasets: [{
|
|
data: entries.map(([, v]) => v),
|
|
backgroundColor: entries.map(([k]) => STATUS_COLORS[k] || '#adb5bd')
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { position: 'bottom' } }
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderDisciplineBreakdown(disciplineData) {
|
|
destroyChart('disciplineBreakdown');
|
|
const canvas = document.getElementById('disciplineBreakdownChart');
|
|
const emptyEl = document.getElementById('disciplineBreakdownEmpty');
|
|
|
|
const entries = Object.entries(disciplineData || {}).filter(([, v]) => v > 0);
|
|
if (entries.length === 0) {
|
|
canvas.style.display = 'none';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
canvas.style.display = 'block';
|
|
emptyEl.style.display = 'none';
|
|
|
|
analyticsCharts.disciplineBreakdown = new Chart(canvas, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: entries.map(([k]) => k),
|
|
datasets: [{
|
|
data: entries.map(([, v]) => v),
|
|
backgroundColor: entries.map((_, i) => DISCIPLINE_COLORS[i % DISCIPLINE_COLORS.length])
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { position: 'bottom' } }
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderRatingDistribution(ratingData) {
|
|
destroyChart('ratingDist');
|
|
const canvas = document.getElementById('ratingDistChart');
|
|
const emptyEl = document.getElementById('ratingDistEmpty');
|
|
|
|
const values = [1, 2, 3, 4, 5].map(k => (ratingData || {})[k] || 0);
|
|
if (values.every(v => v === 0)) {
|
|
canvas.style.display = 'none';
|
|
emptyEl.style.display = 'block';
|
|
return;
|
|
}
|
|
canvas.style.display = 'block';
|
|
emptyEl.style.display = 'none';
|
|
|
|
analyticsCharts.ratingDist = new Chart(canvas, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: ['1 Star', '2 Stars', '3 Stars', '4 Stars', '5 Stars'],
|
|
datasets: [{
|
|
label: 'Agents',
|
|
data: values,
|
|
backgroundColor: '#FFCB05',
|
|
borderRadius: 4
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: { legend: { display: false } },
|
|
scales: {
|
|
y: { beginAtZero: true, ticks: { stepSize: 1 } }
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function renderTopTable(tbodyId, agents, field) {
|
|
const tbody = document.getElementById(tbodyId);
|
|
if (!agents || agents.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="4" class="text-center text-muted py-3">No data</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = agents.map((a, i) => `
|
|
<tr>
|
|
<td><span class="rank-badge">${i + 1}</span></td>
|
|
<td class="fw-medium">${a.agent_name}</td>
|
|
<td><span class="badge status-${a.agent_status || 'Development'}">${a.agent_status || 'Development'}</span></td>
|
|
<td class="fw-bold">${formatNumber(a[field] || 0)}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
function renderRecentlyActive(agents) {
|
|
const tbody = document.getElementById('recentlyActiveTbody');
|
|
if (!agents || agents.length === 0) {
|
|
tbody.innerHTML = '<tr><td colspan="6" class="text-center text-muted py-3">No recently active agents</td></tr>';
|
|
return;
|
|
}
|
|
tbody.innerHTML = agents.map(a => `
|
|
<tr>
|
|
<td class="fw-medium">${a.agent_name}</td>
|
|
<td><span class="badge status-${a.agent_status || 'Development'}">${a.agent_status || 'Development'}</span></td>
|
|
<td>${a.discipline || '<span class="text-muted">-</span>'}</td>
|
|
<td>${formatNumber(a.total_messages || 0)}</td>
|
|
<td>${formatNumber(a.total_tokens || 0)}</td>
|
|
<td>${formatDate(a.last_used)}</td>
|
|
</tr>
|
|
`).join('');
|
|
}
|
|
|
|
|
|
// ===== Verification Tab Functions =====
|
|
|
|
async function loadVerificationData() {
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/admin/agents/pending-verification', {
|
|
credentials: 'include'
|
|
});
|
|
if (!response.ok) return;
|
|
verificationAgents = await response.json();
|
|
|
|
// Update badge count
|
|
const pending = verificationAgents.filter(a => a.verification_status === 'needs_verification');
|
|
const badge = document.getElementById('verificationPendingBadge');
|
|
if (pending.length > 0) {
|
|
badge.textContent = pending.length;
|
|
badge.style.display = 'inline';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
|
|
filterVerificationAgents();
|
|
} catch (error) {
|
|
console.error('Error loading verification data:', error);
|
|
}
|
|
}
|
|
|
|
function filterVerificationAgents() {
|
|
const filter = document.getElementById('verificationFilter').value;
|
|
let filtered = verificationAgents;
|
|
if (filter !== 'all') {
|
|
filtered = verificationAgents.filter(a => a.verification_status === filter);
|
|
}
|
|
displayVerificationAgents(filtered);
|
|
}
|
|
|
|
function displayVerificationAgents(agents) {
|
|
const tbody = document.getElementById('verificationTableBody');
|
|
if (!agents.length) {
|
|
tbody.innerHTML = '<tr><td colspan="7" class="text-center text-muted py-4">No agents to display</td></tr>';
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = agents.map(agent => {
|
|
const statusBadge = agent.verification_status === 'verified'
|
|
? '<span class="badge bg-success">Verified</span>'
|
|
: '<span class="badge bg-warning text-dark">Needs Verification</span>';
|
|
|
|
const actionBtn = agent.verification_status === 'needs_verification' && !isReadonlyAdmin
|
|
? `<button class="btn btn-success btn-sm admin-write-action" onclick="approveAgent('${agent.agent_id}')">
|
|
<i class="fas fa-check me-1"></i>Approve
|
|
</button>`
|
|
: (agent.verified_by ? `<small class="text-muted">by ${agent.verified_by}<br>${formatDate(agent.verified_date)}</small>` : '');
|
|
|
|
return `<tr>
|
|
<td class="fw-medium">${agent.agent_name || 'N/A'}</td>
|
|
<td>${agent.client_name || 'N/A'}</td>
|
|
<td>${agent.studio_name || 'N/A'}</td>
|
|
<td>${agent.created_by || 'N/A'}</td>
|
|
<td>${formatDate(agent.created_at)}</td>
|
|
<td>${statusBadge}</td>
|
|
<td>${actionBtn}</td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
async function approveAgent(agentId) {
|
|
if (!confirm('Are you sure you want to verify this agent?')) return;
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/agents/${agentId}/verify`, {
|
|
method: 'PUT',
|
|
credentials: 'include'
|
|
});
|
|
|
|
if (response.ok) {
|
|
showSuccess('Agent verified successfully');
|
|
await loadVerificationData();
|
|
await loadAdminData();
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to verify agent');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to verify agent');
|
|
}
|
|
}
|
|
|
|
// ==================== PROMPT AUDIT FUNCTIONS ====================
|
|
|
|
async function loadAuditResults() {
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/audit/results`, { credentials: 'include' });
|
|
if (!response.ok) return;
|
|
|
|
const data = await response.json();
|
|
auditAgents = data.agents || [];
|
|
|
|
// Config warning
|
|
const configWarning = document.getElementById('auditConfigWarning');
|
|
const runBtn = document.getElementById('runAuditBtn');
|
|
const runUnclassifiedBtn = document.getElementById('runAuditUnclassifiedBtn');
|
|
if (!data.config_status?.gemini_configured) {
|
|
configWarning.style.display = 'block';
|
|
if (runBtn) runBtn.style.display = 'none';
|
|
if (runUnclassifiedBtn) runUnclassifiedBtn.style.display = 'none';
|
|
} else {
|
|
configWarning.style.display = 'none';
|
|
}
|
|
|
|
updateAuditSummary();
|
|
displayAuditResults(auditAgents);
|
|
|
|
if (data.config_status?.gemini_configured) {
|
|
try {
|
|
const statusResp = await fetch(`{{ base_path }}/api/admin/audit/status`, { credentials: 'include' });
|
|
if (statusResp.ok) {
|
|
const state = await statusResp.json();
|
|
if (state.running) {
|
|
const progressEl = document.getElementById('auditProgress');
|
|
const progressText = document.getElementById('auditProgressText');
|
|
const runBtn = document.getElementById('runAuditBtn');
|
|
const runUnclassifiedBtn = document.getElementById('runAuditUnclassifiedBtn');
|
|
if (progressEl) progressEl.style.display = 'block';
|
|
if (runBtn) runBtn.disabled = true;
|
|
if (runUnclassifiedBtn) runUnclassifiedBtn.disabled = true;
|
|
pollAuditStatus(progressText, progressEl, runBtn, runUnclassifiedBtn);
|
|
}
|
|
}
|
|
} catch (e) { /* status check is best-effort */ }
|
|
}
|
|
} catch (error) {
|
|
console.error('Failed to load audit results:', error);
|
|
}
|
|
}
|
|
|
|
function updateAuditSummary() {
|
|
let audited = 0, flagged = 0, reviewed = 0, cleared = 0, clientDetected = 0, noPrompt = 0;
|
|
|
|
auditAgents.forEach(a => {
|
|
if (!a.instructions) { noPrompt++; return; }
|
|
if (!a.audit_status) return;
|
|
audited++;
|
|
if (a.audit_status === 'flagged') flagged++;
|
|
if (a.audit_status === 'reviewed') reviewed++;
|
|
if (a.audit_status === 'cleared') cleared++;
|
|
if (a.audit_is_client_work) clientDetected++;
|
|
});
|
|
|
|
document.getElementById('auditTotalCount').textContent = audited;
|
|
document.getElementById('auditFlaggedCount').textContent = flagged;
|
|
document.getElementById('auditReviewedCount').textContent = reviewed;
|
|
document.getElementById('auditClearedCount').textContent = cleared;
|
|
document.getElementById('auditClientCount').textContent = clientDetected;
|
|
document.getElementById('auditNoPromptCount').textContent = noPrompt;
|
|
|
|
const badge = document.getElementById('auditFlaggedBadge');
|
|
if (flagged > 0) {
|
|
badge.textContent = flagged;
|
|
badge.style.display = 'inline';
|
|
} else {
|
|
badge.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function getCatClass(cat) {
|
|
if (!cat) return '';
|
|
const c = String(cat).toLowerCase();
|
|
if (c === '1b') return 'cat-1b';
|
|
if (c === '1') return 'cat-1';
|
|
if (c === '2') return 'cat-2';
|
|
if (c === '3') return 'cat-3';
|
|
return '';
|
|
}
|
|
|
|
function getRiskClass(risk) {
|
|
if (!risk) return '';
|
|
return `risk-${risk}`;
|
|
}
|
|
|
|
function getStatusClass(status) {
|
|
if (!status) return '';
|
|
return `audit-status-${status}`;
|
|
}
|
|
|
|
function displayAuditResults(agents) {
|
|
const tbody = document.getElementById('auditTableBody');
|
|
const audited = agents.filter(a => a.audit_status);
|
|
|
|
if (audited.length === 0) {
|
|
tbody.innerHTML = `<tr><td colspan="10" class="text-center py-4 text-muted">No audit results yet. Click "Run Full Audit" to get started.</td></tr>`;
|
|
return;
|
|
}
|
|
|
|
tbody.innerHTML = audited.map(a => {
|
|
const catBadge = a.audit_category ? `<span class="badge ${getCatClass(a.audit_category)}">Cat ${a.audit_category}</span>` : '-';
|
|
const riskBadge = a.audit_risk_level ? `<span class="badge ${getRiskClass(a.audit_risk_level)}">${a.audit_risk_level}</span>` : '-';
|
|
const discipline = a.audit_discipline || a.discipline || '-';
|
|
const department = a.audit_department || a.agent_department || '-';
|
|
const clientBadge = a.audit_is_client_work ? '<span class="badge bg-warning text-dark">Client</span>' : '<span class="text-muted">-</span>';
|
|
const flags = (a.audit_flags || []).slice(0, 3).map(f => `<span class="badge bg-secondary badge-sm me-1">${f}</span>`).join('') + ((a.audit_flags || []).length > 3 ? `<span class="text-muted">+${a.audit_flags.length - 3}</span>` : '');
|
|
const statusBadge = a.audit_status ? `<span class="badge ${a.audit_status === 'flagged' ? 'bg-danger' : a.audit_status === 'reviewed' ? 'bg-warning text-dark' : 'bg-success'}">${a.audit_status}</span>` : '-';
|
|
const auditDate = a.audit_date ? new Date(a.audit_date).toLocaleDateString() : '-';
|
|
|
|
return `<tr class="${getStatusClass(a.audit_status)}" style="cursor:pointer;" onclick="showAuditDetail('${a._id}')">
|
|
<td class="fw-semibold">${a.agent_name || 'Unknown'}</td>
|
|
<td><small>${discipline}</small></td>
|
|
<td><small>${department}</small></td>
|
|
<td>${catBadge}</td>
|
|
<td>${riskBadge}</td>
|
|
<td>${clientBadge}</td>
|
|
<td>${flags || '-'}</td>
|
|
<td>${statusBadge}</td>
|
|
<td><small>${auditDate}</small></td>
|
|
<td><button class="btn btn-sm btn-outline-primary" onclick="event.stopPropagation(); showAuditDetail('${a._id}')"><i class="fas fa-eye"></i></button></td>
|
|
</tr>`;
|
|
}).join('');
|
|
}
|
|
|
|
function filterAuditResults() {
|
|
const category = document.getElementById('auditCategoryFilter').value;
|
|
const risk = document.getElementById('auditRiskFilter').value;
|
|
const discipline = document.getElementById('auditDisciplineFilter').value;
|
|
const status = document.getElementById('auditStatusFilter').value;
|
|
const client = document.getElementById('auditClientFilter').value;
|
|
const search = document.getElementById('auditSearchInput').value.toLowerCase();
|
|
|
|
const filtered = auditAgents.filter(a => {
|
|
if (status === 'none') { return !a.audit_status; }
|
|
if (status && a.audit_status !== status) return false;
|
|
if (!status && status !== 'none' && !a.audit_status) return false;
|
|
if (category && a.audit_category !== category) return false;
|
|
if (risk && a.audit_risk_level !== risk) return false;
|
|
if (discipline && (a.audit_discipline || a.discipline) !== discipline) return false;
|
|
if (client === 'yes' && !a.audit_is_client_work) return false;
|
|
if (client === 'no' && a.audit_is_client_work) return false;
|
|
if (search && !(a.agent_name || '').toLowerCase().includes(search) && !(a.created_by || '').toLowerCase().includes(search)) return false;
|
|
return true;
|
|
});
|
|
|
|
displayAuditResults(filtered);
|
|
}
|
|
|
|
let auditPollTimer = null;
|
|
|
|
async function runAudit(unclassifiedOnly) {
|
|
const progressEl = document.getElementById('auditProgress');
|
|
const progressText = document.getElementById('auditProgressText');
|
|
const runBtn = document.getElementById('runAuditBtn');
|
|
const runUnclassifiedBtn = document.getElementById('runAuditUnclassifiedBtn');
|
|
|
|
if (progressEl) progressEl.style.display = 'block';
|
|
if (progressText) progressText.textContent = unclassifiedOnly
|
|
? 'Starting audit on unclassified agents...'
|
|
: 'Starting full audit on all agents...';
|
|
if (runBtn) runBtn.disabled = true;
|
|
if (runUnclassifiedBtn) runUnclassifiedBtn.disabled = true;
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/audit/run`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ unclassified_only: unclassifiedOnly })
|
|
});
|
|
|
|
const text = await response.text();
|
|
let result;
|
|
try { result = JSON.parse(text); } catch { result = { detail: text || 'Unexpected response' }; }
|
|
|
|
if (!response.ok) {
|
|
showError(result.detail || `Audit failed (HTTP ${response.status})`);
|
|
finalizeAuditUi(progressEl, runBtn, runUnclassifiedBtn);
|
|
return;
|
|
}
|
|
|
|
pollAuditStatus(progressText, progressEl, runBtn, runUnclassifiedBtn);
|
|
} catch (error) {
|
|
showError('Audit request failed: ' + error.message);
|
|
finalizeAuditUi(progressEl, runBtn, runUnclassifiedBtn);
|
|
}
|
|
}
|
|
|
|
function finalizeAuditUi(progressEl, runBtn, runUnclassifiedBtn) {
|
|
if (auditPollTimer) { clearTimeout(auditPollTimer); auditPollTimer = null; }
|
|
if (progressEl) progressEl.style.display = 'none';
|
|
if (runBtn) runBtn.disabled = false;
|
|
if (runUnclassifiedBtn) runUnclassifiedBtn.disabled = false;
|
|
}
|
|
|
|
async function pollAuditStatus(progressText, progressEl, runBtn, runUnclassifiedBtn) {
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/audit/status`, { credentials: 'include' });
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
const state = await response.json();
|
|
|
|
const done = state.audited + state.failed + state.skipped;
|
|
const total = state.total || 0;
|
|
const cur = state.current_agent ? ` — ${state.current_agent}` : '';
|
|
|
|
if (state.running) {
|
|
if (progressText) progressText.textContent =
|
|
`Auditing… ${done}/${total} (${state.audited} ok, ${state.failed} failed, ${state.skipped} skipped)${cur}`;
|
|
auditPollTimer = setTimeout(() => pollAuditStatus(progressText, progressEl, runBtn, runUnclassifiedBtn), 3000);
|
|
return;
|
|
}
|
|
|
|
if (state.error) {
|
|
showError('Audit failed: ' + state.error);
|
|
} else {
|
|
showSuccess(`Audit complete: ${state.audited} audited, ${state.skipped} skipped, ${state.failed} failed`);
|
|
}
|
|
await loadAuditResults();
|
|
await loadVerificationData();
|
|
finalizeAuditUi(progressEl, runBtn, runUnclassifiedBtn);
|
|
} catch (error) {
|
|
showError('Could not read audit status: ' + error.message);
|
|
finalizeAuditUi(progressEl, runBtn, runUnclassifiedBtn);
|
|
}
|
|
}
|
|
|
|
function showAuditDetail(agentId) {
|
|
const agent = auditAgents.find(a => a._id === agentId);
|
|
if (!agent) return;
|
|
|
|
currentAuditAgentId = agentId;
|
|
|
|
const body = document.getElementById('auditDetailBody');
|
|
const catClass = getCatClass(agent.audit_category);
|
|
const riskClass = getRiskClass(agent.audit_risk_level);
|
|
|
|
const flagsHtml = (agent.audit_flags || []).map(f => `<span class="badge bg-secondary me-1 mb-1">${f}</span>`).join('') || '<span class="text-muted">None</span>';
|
|
|
|
const clientSection = agent.audit_is_client_work
|
|
? `<div class="alert alert-warning py-2"><i class="fas fa-exclamation-triangle me-2"></i><strong>Client Work Detected</strong>
|
|
${agent.audit_client_name_detected ? `<br>Client: <strong>${agent.audit_client_name_detected}</strong>` : ''}
|
|
<br><small>${agent.audit_client_work_reasoning || ''}</small></div>`
|
|
: '<span class="text-muted">Not detected as client work</span>';
|
|
|
|
const reviewerSection = agent.audit_reviewer
|
|
? `<div class="audit-section"><h6>Review History</h6>
|
|
<p><strong>${agent.audit_reviewer}</strong> on ${agent.audit_reviewed_date ? new Date(agent.audit_reviewed_date).toLocaleString() : 'N/A'}
|
|
${agent.audit_reviewer_notes ? `<br><em>"${agent.audit_reviewer_notes}"</em>` : ''}</p></div>`
|
|
: '';
|
|
|
|
body.innerHTML = `
|
|
<h5>${agent.agent_name || 'Unknown Agent'}</h5>
|
|
<p class="text-muted mb-3">${agent.agent_description || 'No description'} | Created by: ${agent.created_by || 'Unknown'}</p>
|
|
|
|
<div class="row mb-3">
|
|
<div class="col-md-3">
|
|
<div class="audit-section">
|
|
<h6>Category</h6>
|
|
<span class="badge ${catClass} fs-6">Cat ${agent.audit_category || '?'}</span>
|
|
<p class="mt-1 small text-muted">${agent.audit_category_reasoning || ''}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="audit-section">
|
|
<h6>Risk Level</h6>
|
|
<span class="badge ${riskClass} fs-6">${agent.audit_risk_level || '?'}</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="audit-section">
|
|
<h6>Discipline</h6>
|
|
<span class="badge bg-info text-dark">${agent.audit_discipline || agent.discipline || 'Unknown'}</span>
|
|
<p class="mt-1 small text-muted">${agent.audit_discipline_reasoning || ''}</p>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="audit-section">
|
|
<h6>Department</h6>
|
|
<span class="fw-semibold">${agent.audit_department || agent.agent_department || 'Not determined'}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="audit-section">
|
|
<h6>Client Work Detection</h6>
|
|
${clientSection}
|
|
</div>
|
|
|
|
<div class="audit-section">
|
|
<h6>Summary</h6>
|
|
<p>${agent.audit_summary || 'No summary available'}</p>
|
|
</div>
|
|
|
|
<div class="audit-section">
|
|
<h6>Flags</h6>
|
|
<div>${flagsHtml}</div>
|
|
</div>
|
|
|
|
<div class="audit-section">
|
|
<h6>Recommendations</h6>
|
|
<p>${agent.audit_recommendations || 'None'}</p>
|
|
</div>
|
|
|
|
${reviewerSection}
|
|
|
|
<div class="audit-section">
|
|
<h6>Instructions <a class="small" data-bs-toggle="collapse" href="#auditInstructionsCollapse">(show/hide)</a></h6>
|
|
<div class="collapse" id="auditInstructionsCollapse">
|
|
<pre class="audit-instructions-pre">${(agent.instructions || 'No instructions stored').replace(/</g, '<').replace(/>/g, '>')}</pre>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Set review dropdown to current status
|
|
document.getElementById('auditReviewStatus').value = agent.audit_status || 'flagged';
|
|
document.getElementById('auditReviewNotes').value = '';
|
|
|
|
new bootstrap.Modal(document.getElementById('auditDetailModal')).show();
|
|
}
|
|
|
|
async function submitAuditReview() {
|
|
if (!currentAuditAgentId) return;
|
|
|
|
const status = document.getElementById('auditReviewStatus').value;
|
|
const notes = document.getElementById('auditReviewNotes').value;
|
|
|
|
try {
|
|
const response = await fetch(`{{ base_path }}/api/admin/audit/${currentAuditAgentId}/review`, {
|
|
method: 'PUT',
|
|
credentials: 'include',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ audit_status: status, reviewer_notes: notes })
|
|
});
|
|
|
|
if (response.ok) {
|
|
showSuccess(`Audit status updated to ${status}`);
|
|
bootstrap.Modal.getInstance(document.getElementById('auditDetailModal')).hide();
|
|
await loadAuditResults();
|
|
} else {
|
|
const error = await response.json();
|
|
showError(error.detail || 'Failed to update audit');
|
|
}
|
|
} catch (error) {
|
|
showError('Failed to update audit review');
|
|
}
|
|
}
|
|
|
|
async function uploadCsv(input) {
|
|
if (!input.files || !input.files[0]) return;
|
|
const formData = new FormData();
|
|
formData.append('file', input.files[0]);
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/admin/agents/import/csv', {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include',
|
|
});
|
|
const result = await response.json();
|
|
if (response.ok && result.success) {
|
|
let msg = `Import complete!\nImported: ${result.imported}\nSkipped: ${result.skipped}\nErrors: ${result.errors}`;
|
|
if (result.errors > 0) {
|
|
msg += `\n\nFirst few errors:\n${result.error_details.join('\n')}`;
|
|
}
|
|
alert(msg);
|
|
window.location.reload();
|
|
} else {
|
|
alert('Import failed: ' + (result.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
alert('Error uploading CSV: ' + error.message);
|
|
}
|
|
input.value = '';
|
|
}
|
|
|
|
async function deleteCsv(input) {
|
|
if (!input.files || !input.files[0]) return;
|
|
if (!confirm('WARNING: This will permanently delete all agents found in the CSV file. Are you sure you want to proceed?')) {
|
|
input.value = '';
|
|
return;
|
|
}
|
|
const formData = new FormData();
|
|
formData.append('file', input.files[0]);
|
|
try {
|
|
const response = await fetch('{{ base_path }}/api/admin/agents/delete/csv', {
|
|
method: 'POST',
|
|
body: formData,
|
|
credentials: 'include',
|
|
});
|
|
const result = await response.json();
|
|
if (response.ok && result.success) {
|
|
let msg = `Deletion complete!\nDeleted: ${result.deleted}\nNot Found: ${result.not_found}\nErrors: ${result.errors}`;
|
|
if (result.errors > 0) {
|
|
msg += `\n\nFirst few errors:\n${result.error_details.join('\n')}`;
|
|
}
|
|
alert(msg);
|
|
window.location.reload();
|
|
} else {
|
|
alert('Deletion failed: ' + (result.detail || 'Unknown error'));
|
|
}
|
|
} catch (error) {
|
|
alert('Error deleting via CSV: ' + error.message);
|
|
}
|
|
input.value = '';
|
|
}
|
|
|
|
</script>
|
|
{% endblock %} |