feat: audit log, usage tracking UI, OpenAI orchestrator compaction, onboarding + docs

Tool audit log:
- Every orchestrator tool call logged to home/{user}/tool_audit/YYYY-MM-DD.jsonl
- Files panel sidebar: audit log group (collapsed), date-linked read-only table
- Admin endpoints: /api/audit/files, /api/audit/day, /api/audit/recent, /api/audit/stats
- Engine and model name recorded per entry

OpenAI orchestrator improvements:
- Context budget enforcement: 75% of model context_k (min 16k)
- Message compaction: truncates old tool results when approaching budget
- max_rounds respected per model config (intersected with server cap)

OpenRouter onboarding (setup.html, onboarding.py, app.js, settings.html):
- Step 3 of 3: /setup/model with curated model picker
- Chat banner for users on server-default model (informational, not alarmist)
- Settings quick-link card; /setup/model works standalone for existing users

Model registry + session store:
- set_role_config / get_role_config for per-role tool lists and system_append
- session_store: session rename, session name backfill endpoint

UI updates (app.js, index.html, style.css, local_llm.html):
- Role toggle in context panel
- Off-the-record mode
- Agent notes read-only viewer
- OPERATIONS.md loaded at T2+ in context

Documentation:
- HELP.md: full tool table, per-role tool sets, Agent Notes, usage tracking
- TOOLS.md: Agent Notes section, count corrected to 44
- ARCH__SYSTEM.md, ARCH__BACKENDS.md, MASTER.md updated to match reality
- CLAUDE.md: onboarding flow, documentation philosophy sections
- README.md: stack in practice, DeepSeek TUI mention, architecture diagram updated
- TODO__Agents.md: onboarding task completed with deviation notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Scott Idem
2026-05-08 21:26:43 -04:00
parent c02d2462b0
commit f8f7cd75da
25 changed files with 1088 additions and 151 deletions

View File

@@ -423,6 +423,18 @@
</div>
<!-- Browser cache -->
<!-- Usage summary -->
<div class="section" id="usage-section">
<h2>Usage</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;">
Token consumption tracked for API-backed models (Gemini API, local OpenAI-compatible).
Claude CLI calls are not metered.
</p>
<div id="usage-table-wrap" style="overflow-x:auto;">
<p style="font-size:0.8rem; color:var(--pg-muted);">Loading…</p>
</div>
</div>
<div class="section">
<h2>Browser Cache</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;">
@@ -443,6 +455,25 @@
<!-- Model Registry link -->
<div class="section">
<h2>Model Registry</h2>
<!-- Quick-start card: shown only when no model is configured for chat role -->
<div id="openrouter-quickstart" style="display:none; background:#1c1a0a; border:1px solid #78350f;
border-radius:8px; padding:1rem; margin-bottom:1rem;">
<p style="font-size:0.82rem; color:#fbbf24; font-weight:600; margin-bottom:0.4rem;">
⚡ You're on the server default model
</p>
<p style="font-size:0.8rem; color:#d97706; margin-bottom:0.75rem; line-height:1.5;">
You can chat now, but adding your own model gives you more choices, lets you pick
role-specific models, and tracks your usage separately.
OpenRouter is the easiest way to get started — one key, many models.
</p>
<a href="/setup/model"
style="display:inline-block; padding:0.5rem 0.9rem; background:#92400e; border-radius:6px;
color:#fef3c7; font-size:0.85rem; font-weight:600; text-decoration:none;">
Set up OpenRouter →
</a>
</div>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;">
Configure AI providers (Anthropic, Google), local hosts (Open WebUI, Ollama, OpenRouter, etc.),
and assign models to roles — chat, orchestrator, distill, and more.
@@ -479,6 +510,22 @@
</div>
<!-- Personas -->
<!-- Sessions -->
<div class="section">
<h2>Sessions</h2>
<p style="font-size:0.8rem; color:var(--pg-muted); margin-bottom:0.85rem; line-height:1.55;">
Auto-name any sessions that still show a random ID, using their first message as the name.
Only unnamed sessions are affected — existing names are left alone.
</p>
<button type="button" id="backfill-names-btn"
style="padding:0.5rem 1rem; background:none; border:1px solid var(--pg-border); border-radius:6px;
color:var(--pg-muted); font-size:0.88rem; font-weight:500; cursor:pointer;
transition:border-color 0.15s, color 0.15s;">
Auto-name old sessions
</button>
<span id="backfill-names-ok" style="display:none; margin-left:0.75rem; font-size:0.8rem; color:#4ade80;"></span>
</div>
<div class="section">
<h2>Personas</h2>
<ul class="persona-list">
@@ -532,6 +579,84 @@
document.getElementById('clear-ls-ok').style.display = 'inline';
});
// Show OpenRouter quick-start card if no model is configured
(async () => {
try {
const d = await fetch('/backend').then(r => r.json());
const roles = d.available_roles || [];
if (roles.length === 0) {
document.getElementById('openrouter-quickstart').style.display = 'block';
}
} catch (_) {}
})();
// Usage summary table
(async () => {
const wrap = document.getElementById('usage-table-wrap');
try {
const resp = await fetch('/api/usage/summary');
if (!resp.ok) throw new Error(resp.statusText);
const rows_data = await resp.json();
if (!rows_data.length) {
wrap.innerHTML = '<p style="font-size:0.8rem;color:var(--pg-muted);">No usage recorded yet.</p>';
return;
}
const fmt = n => n >= 1000 ? (n / 1000).toFixed(1) + 'k' : String(n);
const rows = rows_data.map(d => {
const labelCell = d.label !== d.key
? `<span title="${d.key}">${d.label}</span>`
: `<span>${d.key}</span>`;
return `<tr>
<td style="padding:0.4rem 0.75rem 0.4rem 0; font-size:0.82rem; color:var(--pg-text); white-space:nowrap;">${labelCell}</td>
<td style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${d.calls}</td>
<td style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${fmt(d.prompt_tokens)}</td>
<td style="padding:0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-muted); text-align:right;">${fmt(d.completion_tokens)}</td>
<td style="padding:0.4rem 0 0.4rem 0.5rem; font-size:0.82rem; color:var(--pg-text); text-align:right; font-weight:600;">${fmt(d.total_tokens)}</td>
</tr>`;
}).join('');
wrap.innerHTML = `<table style="border-collapse:collapse; width:100%; min-width:360px;">
<thead>
<tr style="border-bottom:1px solid var(--pg-border);">
<th style="padding:0.35rem 0.75rem 0.35rem 0; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:left;">Model</th>
<th style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Calls</th>
<th style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Prompt</th>
<th style="padding:0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Output</th>
<th style="padding:0.35rem 0 0.35rem 0.5rem; font-size:0.75rem; color:var(--pg-muted); font-weight:600; text-align:right;">Total</th>
</tr>
</thead>
<tbody>${rows}</tbody>
</table>`;
} catch (e) {
wrap.innerHTML = `<p style="font-size:0.8rem;color:var(--pg-muted);">Could not load usage data.</p>`;
}
})();
// Auto-name old sessions backfill
document.getElementById('backfill-names-btn').addEventListener('click', async () => {
const btn = document.getElementById('backfill-names-btn');
const ok = document.getElementById('backfill-names-ok');
btn.disabled = true;
btn.textContent = 'Working…';
try {
const params = new URLSearchParams(window.location.search);
const user = params.get('user') || document.querySelector('input[value]')?.value || '';
const persona = params.get('persona') || '';
const qs = user ? `?user=${encodeURIComponent(user)}&persona=${encodeURIComponent(persona)}` : '';
const res = await fetch(`/api/sessions/backfill-names${qs}`, { method: 'POST' });
const data = await res.json();
if (!res.ok) throw new Error(data.detail || res.statusText);
const n = data.named ?? 0;
ok.textContent = `Named ${n} session${n !== 1 ? 's' : ''}.`;
ok.style.display = 'inline';
} catch (e) {
ok.textContent = 'Error — check console.';
ok.style.color = '#f87171';
ok.style.display = 'inline';
}
btn.textContent = 'Auto-name old sessions';
btn.disabled = false;
});
// Persona rename toggle
document.querySelectorAll('.persona-rename-toggle').forEach(btn => {
btn.addEventListener('click', () => {