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:
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user