Complete Flask → FastAPI migration with: - FastAPI app with session auth, Azure AD SSO, rate limiting - SQLite-backed session store (survives restarts) - Bulk AI metadata generation with SSE progress - Admin panel (user management, audit log, AI usage) - Subpath deployment support (ROOT_PATH config) - Docker + deploy.sh for production deployment - Test suite (auth, upload, templates, imports, admin, sessions) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
187 lines
8.3 KiB
HTML
187 lines
8.3 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Admin - Oliver Metadata Tool</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
<link rel="stylesheet" href="{{ request.scope.get('root_path', '') }}/static/css/app.css">
|
|
<link rel="stylesheet" href="{{ request.scope.get('root_path', '') }}/static/css/admin.css">
|
|
</head>
|
|
<body>
|
|
{% set base = request.scope.get('root_path', '') %}
|
|
<div class="container">
|
|
<div class="header">
|
|
<h1>Admin Dashboard</h1>
|
|
<p>Oliver Metadata Tool - Administration</p>
|
|
<div style="position: absolute; top: 15px; right: 20px; font-size: 13px; color: #6b7280;">
|
|
{{ username }} |
|
|
<a href="{{ base }}/" style="color: #FFC407;">Home</a> |
|
|
<a href="{{ base }}/logout" style="color: #FFC407;">Logout</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="content">
|
|
<!-- Stats Cards -->
|
|
<div class="admin-stats">
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statActiveUsers">{{ stats.active_users | default(0) }}</div>
|
|
<div class="stat-label">Active Users</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statActiveSessions">{{ stats.active_sessions | default(0) }}</div>
|
|
<div class="stat-label">Active Sessions</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statAuditEntries">{{ stats.recent_activity | default(0) }}</div>
|
|
<div class="stat-label">Activity (24h)</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="stat-value" id="statTotalTokens">{{ stats.ai_usage.total_tokens | default(0) }}</div>
|
|
<div class="stat-label">AI Tokens Used</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Tabs -->
|
|
<div class="admin-tabs">
|
|
<button class="admin-tab active" onclick="switchTab('users')">Users</button>
|
|
<button class="admin-tab" onclick="switchTab('audit')">Audit Log</button>
|
|
<button class="admin-tab" onclick="switchTab('ai-usage')">AI Usage</button>
|
|
</div>
|
|
|
|
<!-- Users Tab -->
|
|
<div class="admin-panel" id="usersPanel">
|
|
<div class="panel-header">
|
|
<h3>User Management</h3>
|
|
<button class="btn btn-sm" onclick="showCreateUserModal()">+ Create User</button>
|
|
</div>
|
|
<div class="admin-table-container">
|
|
<table class="admin-table" id="usersTable">
|
|
<thead>
|
|
<tr>
|
|
<th>ID</th>
|
|
<th>Username</th>
|
|
<th>Email</th>
|
|
<th>Role</th>
|
|
<th>Auth</th>
|
|
<th>Last Login</th>
|
|
<th>Status</th>
|
|
<th>Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="usersTableBody">
|
|
<tr><td colspan="8" style="text-align: center; color: #6b7280;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Audit Log Tab -->
|
|
<div class="admin-panel" id="auditPanel" style="display: none;">
|
|
<div class="panel-header">
|
|
<h3>Audit Log</h3>
|
|
<div class="audit-filters">
|
|
<select id="auditUserFilter" onchange="loadAuditLog()">
|
|
<option value="">All Users</option>
|
|
</select>
|
|
<button class="btn btn-sm" onclick="loadAuditLog()">Refresh</button>
|
|
</div>
|
|
</div>
|
|
<div class="admin-table-container">
|
|
<table class="admin-table" id="auditTable">
|
|
<thead>
|
|
<tr>
|
|
<th>Time</th>
|
|
<th>User</th>
|
|
<th>Action</th>
|
|
<th>Details</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="auditTableBody">
|
|
<tr><td colspan="4" style="text-align: center; color: #6b7280;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- AI Usage Tab -->
|
|
<div class="admin-panel" id="aiUsagePanel" style="display: none;">
|
|
<div class="panel-header">
|
|
<h3>AI Usage Statistics</h3>
|
|
<button class="btn btn-sm" onclick="loadAiUsage()">Refresh</button>
|
|
</div>
|
|
<div class="ai-stats-grid" id="aiStatsGrid">
|
|
<!-- Populated by JS -->
|
|
</div>
|
|
<h4 style="margin: 20px 0 10px;">Usage by User</h4>
|
|
<div class="admin-table-container">
|
|
<table class="admin-table" id="aiUsageTable">
|
|
<thead>
|
|
<tr>
|
|
<th>User</th>
|
|
<th>Requests</th>
|
|
<th>Total Tokens</th>
|
|
<th>Last Used</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="aiUsageTableBody">
|
|
<tr><td colspan="4" style="text-align: center; color: #6b7280;">Loading...</td></tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="footer">
|
|
Oliver Metadata Tool v4.0.0 | Admin Dashboard
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Create User Modal -->
|
|
<div id="createUserModal" class="modal">
|
|
<div class="modal-content" style="max-width: 500px;">
|
|
<div class="modal-header">
|
|
<h3>Create New User</h3>
|
|
<span class="close-modal" onclick="closeCreateUserModal()">×</span>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newUsername">Username *</label>
|
|
<input type="text" id="newUsername" placeholder="username" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newEmail">Email</label>
|
|
<input type="email" id="newEmail" placeholder="user@example.com">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newFullName">Full Name</label>
|
|
<input type="text" id="newFullName" placeholder="Full Name">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newPassword">Password (for local auth)</label>
|
|
<input type="password" id="newPassword" placeholder="Leave empty for SSO-only">
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newRole">Role</label>
|
|
<select id="newRole">
|
|
<option value="user">User</option>
|
|
<option value="admin">Admin</option>
|
|
</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label for="newAuthMethod">Auth Method</label>
|
|
<select id="newAuthMethod">
|
|
<option value="local">Local (Password)</option>
|
|
<option value="sso">SSO (Microsoft)</option>
|
|
</select>
|
|
</div>
|
|
<div style="display: flex; gap: 10px; margin-top: 20px;">
|
|
<button class="btn" onclick="createUser()">Create User</button>
|
|
<button class="btn" style="background: #6c757d;" onclick="closeCreateUserModal()">Cancel</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>const BASE_PATH = "{{ base }}";</script>
|
|
<script src="{{ base }}/static/js/admin.js"></script>
|
|
</body>
|
|
</html>
|