GraphResearcher / app /product /admin_ui.py
yugbirla's picture
Add hidden admin panel with role protected admin APIs
7b53c85
Raw
History Blame Contribute Delete
11.7 kB
def get_admin_panel_html() -> str:
return """
<!DOCTYPE html>
<html>
<head>
<title>GraphResearcher Admin</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
* { box-sizing: border-box; }
body {
margin: 0;
font-family: Inter, Arial, sans-serif;
background: #f8fafc;
color: #0f172a;
}
.layout {
display: grid;
grid-template-columns: 280px 1fr;
min-height: 100vh;
}
.sidebar {
background: #0f172a;
color: white;
padding: 20px;
}
.brand {
font-weight: 900;
font-size: 24px;
margin-bottom: 6px;
}
.sub {
color: #94a3b8;
font-size: 13px;
margin-bottom: 22px;
}
input {
width: 100%;
padding: 11px;
border-radius: 9px;
border: 1px solid #cbd5e1;
margin-bottom: 10px;
}
button {
border: none;
background: #2563eb;
color: white;
border-radius: 10px;
padding: 11px 13px;
cursor: pointer;
font-weight: 800;
width: 100%;
margin-bottom: 9px;
}
button:hover { background: #1d4ed8; }
button.dark { background: #334155; }
button.green { background: #059669; }
button.red { background: #dc2626; }
.main {
padding: 26px;
}
.top {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.status-pill {
display: inline-block;
padding: 7px 11px;
background: #e0f2fe;
color: #075985;
border-radius: 999px;
font-size: 13px;
font-weight: 800;
}
.grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 20px;
}
.card {
background: white;
border: 1px solid #e5e7eb;
border-radius: 18px;
padding: 18px;
box-shadow: 0 1px 4px rgba(0,0,0,0.04);
}
.card h3 {
margin-top: 0;
}
.metric {
font-size: 30px;
font-weight: 900;
color: #1d4ed8;
}
pre {
white-space: pre-wrap;
word-break: break-word;
background: #0f172a;
color: #e5e7eb;
padding: 16px;
border-radius: 14px;
max-height: 560px;
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
background: white;
border-radius: 12px;
overflow: hidden;
}
th, td {
border-bottom: 1px solid #e5e7eb;
text-align: left;
padding: 10px;
font-size: 13px;
}
th {
background: #f1f5f9;
font-weight: 900;
}
.warning {
background: #fff7ed;
border: 1px solid #fed7aa;
color: #9a3412;
border-radius: 12px;
padding: 12px;
margin-bottom: 16px;
font-size: 14px;
}
.hidden {
display: none;
}
@media(max-width: 950px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
position: static;
}
.grid {
grid-template-columns: 1fr;
}
}
</style>
</head>
<body>
<div class="layout">
<aside class="sidebar">
<div class="brand">Admin Panel</div>
<div class="sub">GraphResearcher internal tools</div>
<label>Admin email</label>
<input id="adminEmail" value="2006yugb@gmail.com">
<button onclick="saveAdmin()">Use Admin Email</button>
<button onclick="loadStatus()" class="green">Dashboard</button>
<button onclick="loadUsers()">Users</button>
<button onclick="loadDocuments()">Documents</button>
<button onclick="loadConversations()">Conversations</button>
<button onclick="loadSystem()">System</button>
<button onclick="loadHealth()" class="dark">Deployment Health</button>
<button onclick="loadLLM()" class="dark">LLM Status</button>
<hr style="border-color: rgba(255,255,255,0.18); margin: 18px 0;">
<button onclick="window.open('/app','_blank')" class="dark">Open User App</button>
<button onclick="window.open('/docs','_blank')" class="dark">Open API Docs</button>
<button onclick="window.open('/graphrag-demo','_blank')" class="dark">Open GraphRAG Console</button>
<button onclick="logout()" class="red">Clear Admin Session</button>
</aside>
<main class="main">
<div class="top">
<div>
<h1>GraphResearcher Admin</h1>
<p style="color:#64748b;margin-top:-8px;">Hidden admin workspace for monitoring users, documents, system health, and developer tools.</p>
</div>
<span id="statusPill" class="status-pill">Not checked</span>
</div>
<div class="warning">
This page is hidden from the normal user app. The backend APIs still check admin role using the admin email.
Later Google OAuth will replace this temporary email-header login.
</div>
<div id="dashboardCards" class="grid">
<div class="card">
<h3>Users</h3>
<div id="usersMetric" class="metric">-</div>
</div>
<div class="card">
<h3>Documents</h3>
<div id="docsMetric" class="metric">-</div>
</div>
<div class="card">
<h3>Conversations</h3>
<div id="convMetric" class="metric">-</div>
</div>
<div class="card">
<h3>Status</h3>
<div id="systemMetric" class="metric">-</div>
</div>
</div>
<div class="card">
<h3 id="outputTitle">Output</h3>
<div id="tableOutput"></div>
<pre id="rawOutput">{}</pre>
</div>
</main>
</div>
<script>
function getAdminEmail() {
return localStorage.getItem("graphrag_admin_email") || document.getElementById("adminEmail").value.trim();
}
function saveAdmin() {
const email = document.getElementById("adminEmail").value.trim();
localStorage.setItem("graphrag_admin_email", email);
setStatus("Admin email saved");
loadStatus();
}
function logout() {
localStorage.removeItem("graphrag_admin_email");
setStatus("Admin cleared");
document.getElementById("rawOutput").textContent = "{}";
document.getElementById("tableOutput").innerHTML = "";
}
function headers() {
return {
"X-User-Email": getAdminEmail(),
"X-User-Name": "Admin"
};
}
function setStatus(text) {
document.getElementById("statusPill").textContent = text;
}
function showRaw(title, data) {
document.getElementById("outputTitle").textContent = title;
document.getElementById("rawOutput").textContent = JSON.stringify(data, null, 2);
}
async function fetchJson(url, useAdminHeaders = true) {
const response = await fetch(url, {
headers: useAdminHeaders ? headers() : {}
});
const data = await response.json();
if (!response.ok) {
throw new Error(JSON.stringify(data));
}
return data;
}
function renderTable(rows) {
const box = document.getElementById("tableOutput");
if (!rows || rows.length === 0) {
box.innerHTML = "<p>No rows.</p>";
return;
}
const columns = Object.keys(rows[0]).slice(0, 8);
let html = "<table><thead><tr>";
columns.forEach(col => {
html += `<th>${col}</th>`;
});
html += "</tr></thead><tbody>";
rows.forEach(row => {
html += "<tr>";
columns.forEach(col => {
html += `<td>${String(row[col] ?? "").slice(0, 80)}</td>`;
});
html += "</tr>";
});
html += "</tbody></table>";
box.innerHTML = html;
}
async function loadStatus() {
try {
setStatus("Loading...");
const data = await fetchJson("/admin/status");
showRaw("Admin Status", data);
const counts = data.database?.table_counts || {};
document.getElementById("usersMetric").textContent = counts.users ?? "-";
document.getElementById("docsMetric").textContent = counts.user_documents ?? "-";
document.getElementById("convMetric").textContent = counts.conversations ?? "-";
document.getElementById("systemMetric").textContent = data.status || "ok";
document.getElementById("tableOutput").innerHTML = "";
setStatus("Admin verified");
} catch (error) {
setStatus("Access denied");
showRaw("Error", { error: error.message });
}
}
async function loadUsers() {
try {
setStatus("Loading users...");
const data = await fetchJson("/admin/users");
showRaw("Users", data);
renderTable(data.users || []);
setStatus("Users loaded");
} catch (error) {
setStatus("Error");
showRaw("Error", { error: error.message });
}
}
async function loadDocuments() {
try {
setStatus("Loading documents...");
const data = await fetchJson("/admin/documents");
showRaw("Documents", data);
renderTable(data.documents || []);
setStatus("Documents loaded");
} catch (error) {
setStatus("Error");
showRaw("Error", { error: error.message });
}
}
async function loadConversations() {
try {
setStatus("Loading conversations...");
const data = await fetchJson("/admin/conversations");
showRaw("Conversations", data);
renderTable(data.conversations || []);
setStatus("Conversations loaded");
} catch (error) {
setStatus("Error");
showRaw("Error", { error: error.message });
}
}
async function loadSystem() {
try {
setStatus("Loading system...");
const data = await fetchJson("/admin/system");
showRaw("System", data);
document.getElementById("tableOutput").innerHTML = "";
setStatus("System loaded");
} catch (error) {
setStatus("Error");
showRaw("Error", { error: error.message });
}
}
async function loadHealth() {
try {
setStatus("Loading health...");
const data = await fetchJson("/deployment/health", false);
showRaw("Deployment Health", data);
document.getElementById("tableOutput").innerHTML = "";
setStatus("Health loaded");
} catch (error) {
setStatus("Error");
showRaw("Error", { error: error.message });
}
}
async function loadLLM() {
try {
setStatus("Loading LLM...");
const data = await fetchJson("/llm/status", false);
showRaw("LLM Status", data);
document.getElementById("tableOutput").innerHTML = "";
setStatus("LLM loaded");
} catch (error) {
setStatus("Error");
showRaw("Error", { error: error.message });
}
}
document.getElementById("adminEmail").value = getAdminEmail();
loadStatus();
</script>
</body>
</html>
"""