Files
Crumb-Core-v.1/app/templates/pages/admin_logs.html

204 lines
6.1 KiB
HTML

{% extends "base.html" %}
{% block content %}
<!-- Header -->
<section class="mb-2">
<div class="grid">
<div>
<h1>System Logs & Stats</h1>
<p class="text-muted">Monitor chat interactions and token usage.</p>
</div>
<div style="text-align: right;">
<a href="/admin" role="button" class="secondary outline">← Back to Dashboard</a>
</div>
</div>
</section>
<!-- Stats Grid -->
<section class="grid" id="stats-container">
<article class="card">
<header>Total Interactions</header>
<h2 id="stat-total-interactions">-</h2>
</article>
<article class="card">
<header>Vector Coverage</header>
<h2 id="stat-vector-coverage">-</h2>
<small class="text-muted" id="stat-vector-count">-</small>
</article>
<article class="card">
<header>Estimated Cost</header>
<h2 id="stat-cost">-</h2>
<small class="text-muted" id="stat-tokens">-</small>
</article>
<article class="card">
<header>Log Size</header>
<h2 id="stat-file-size">-</h2>
</article>
</section>
<!-- Role Stats -->
<section>
<h3>Role Usage</h3>
<div id="role-stats" class="grid-4">
<!-- Will be populated by JS -->
<article class="loading">Loading stats...</article>
</div>
</section>
<!-- Logs Table -->
<section style="margin-top: 2rem;">
<div class="grid">
<h3>Recent Interactions</h3>
<div style="text-align: right;">
<button onclick="loadData()" class="outline small">↻ Refresh</button>
</div>
</div>
<figure>
<table role="grid">
<thead>
<tr>
<th scope="col">Time</th>
<th scope="col">Role</th>
<th scope="col">User</th>
<th scope="col">Interaction</th>
<th scope="col">Tokens</th>
<th scope="col">Model</th>
</tr>
</thead>
<tbody id="logs-table-body">
<tr>
<td colspan="6" style="text-align: center;">Loading logs...</td>
</tr>
</tbody>
</table>
</figure>
</section>
<script>
async function loadData() {
try {
await Promise.all([loadStats(), loadLogs()]);
} catch (e) {
console.error("Error loading data:", e);
}
}
async function loadStats() {
const res = await fetch('/admin/logs/stats');
if (!res.ok) return;
const data = await res.json();
document.getElementById('stat-total-interactions').innerText = data.total_interactions;
// Vector Coverage
const rate = data.context_hit_rate_percent;
document.getElementById('stat-vector-coverage').innerText = rate + "%";
document.getElementById('stat-vector-count').innerText = `${data.context_found_count} hits`;
// Cost & Tokens
document.getElementById('stat-cost').innerText = "$" + data.estimated_cost_usd.toFixed(4);
document.getElementById('stat-tokens').innerText = `~${data.total_tokens_estimated} tokens`;
document.getElementById('stat-file-size').innerText = data.file_size_mb + " MB";
// Render Role Stats
const roleContainer = document.getElementById('role-stats');
roleContainer.innerHTML = '';
// Sort roles by usage (desc)
const sortedRoles = Object.entries(data.characters).sort((a, b) => b[1] - a[1]);
sortedRoles.forEach(([roleId, count]) => {
const tokens = data.tokens_by_role[roleId] || 0;
const article = document.createElement('article');
article.className = 'card small-card';
article.innerHTML = `
<header class="capitalize"><strong>${roleId}</strong></header>
<div class="stat-value">${count} <small>chats</small></div>
<div class="stat-sub">${tokens} <small>tokens</small></div>
`;
roleContainer.appendChild(article);
});
}
async function loadLogs() {
const res = await fetch('/admin/logs/data?limit=50');
if (!res.ok) return;
const data = await res.json();
const tbody = document.getElementById('logs-table-body');
tbody.innerHTML = '';
data.logs.forEach(log => {
const row = document.createElement('tr');
// Format time
const date = new Date(log.timestamp);
const timeStr = date.toLocaleString();
// Truncate text
const q = log.interaction.question;
const qShort = q.length > 50 ? q.substring(0, 50) + '...' : q;
row.innerHTML = `
<td><small>${timeStr}</small></td>
<td class="capitalize">${log.character.name}</td>
<td>${log.user.email || log.user.id || 'Anonymous'}</td>
<td>
<strong>Q:</strong> ${escapeHtml(qShort)}<br>
<small class="text-muted">Sources: ${log.rag.sources_count}</small>
</td>
<td>${log.tokens_estimated}</td>
<td><small>${log.ai.model}</small></td>
`;
tbody.appendChild(row);
});
}
function escapeHtml(text) {
if (!text) return '';
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
// Initial load
document.addEventListener('DOMContentLoaded', loadData);
// Auto-refresh every 30s
setInterval(loadData, 30000);
</script>
<style>
.capitalize {
text-transform: capitalize;
}
.small-card {
padding: 1rem;
text-align: center;
}
.stat-value {
font-size: 1.5rem;
font-weight: bold;
}
.stat-sub {
font-size: 0.9rem;
color: var(--muted-color);
}
/* Grid 4 for role stats */
.grid-4 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 1rem;
}
</style>
{% endblock %}