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

@@ -18,6 +18,11 @@
const settings_dd_el = document.getElementById('settings-dropdown');
const sessionsBackdrop = document.getElementById('sessions-backdrop');
// ── Utilities ─────────────────────────────────────────────────
function escapeHtml(str) {
return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Close all panels/dropdowns (mutual exclusion) ─────────────
function closeAllPanels() {
if (mode_dropdown_el) mode_dropdown_el.classList.remove('open');
@@ -435,8 +440,32 @@
availableRoles = d.available_roles || [];
roleIdx = 0;
setRoleToggleUI(availableRoles[0] || null);
_maybeShowNoBanner(availableRoles);
});
function _maybeShowNoBanner(roles) {
const key = 'cx_no_model_banner_dismissed';
if (roles.length > 0) { localStorage.removeItem(key); return; }
if (localStorage.getItem(key)) return;
const banner = document.createElement('div');
banner.id = 'no-model-banner';
banner.style.cssText = [
'background:#1c1a0a','border-bottom:1px solid #78350f',
'color:#fbbf24','font-size:0.82rem','padding:0.55rem 1rem',
'display:flex','align-items:center','gap:0.75rem','flex-shrink:0',
].join(';');
banner.innerHTML = `
<span style="flex:1">⚡ Using server default model — add your own for more choices and to track your usage.</span>
<a href="/setup/model" style="color:#fbbf24;font-weight:600;white-space:nowrap;">Set up OpenRouter →</a>
<button onclick="localStorage.setItem('${key}','1');document.getElementById('no-model-banner').remove();"
style="background:none;border:none;color:#78350f;cursor:pointer;font-size:1rem;line-height:1;padding:0 0.2rem;"
title="Dismiss">✕</button>
`;
// Insert at the top of #chat-col (or body if not found)
const col = document.getElementById('chat-col') || document.body.firstElementChild;
col.insertBefore(banner, col.firstChild);
}
backendToggle.addEventListener('click', () => {
if (availableRoles.length <= 1) return;
roleIdx = (roleIdx + 1) % availableRoles.length;
@@ -1067,6 +1096,19 @@
sessionId = data.session_id;
sessionEl.textContent = `session: ${sessionId}`;
persist_session();
// Auto-name the session from the first user message
if (wasNewSession) {
const autoName = text.slice(0, 60).trimEnd() + (text.length > 60 ? '…' : '');
fetch(`/sessions/${sessionId}?${_fileParams}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: autoName }),
}).then(() => {
sessionEl.textContent = `session: ${autoName}`;
sessionNames.set(sessionId, autoName);
}).catch(() => {});
}
thinkingDiv.className = 'message assistant';
setMessageText(thinkingDiv, 'assistant', data.response);
const assistHistIdx = currentHistory.length;
@@ -1133,6 +1175,8 @@
const text = inputEl.value.trim();
if (!text || activeController) return;
const wasNewSession = !sessionId;
inputEl.value = '';
syncHeight();
sendBtn.style.display = 'none';
@@ -1357,6 +1401,7 @@
{ label: 'Memory', files: ['MEMORY_LONG.md', 'MEMORY_MID.md', 'MEMORY_SHORT.md'] },
{ label: 'Profile', files: ['USER.md', 'HELP.md'] },
{ label: 'Settings', files: ['email_allowlist.json'] },
{ label: 'Agent Notes (read-only)', files: ['AGENT_NOTES.bak1.md', 'AGENT_NOTES.bak2.md', 'AGENT_NOTES.bak3.md'], collapsed: true },
];
function fmtSize(bytes) {
@@ -1394,7 +1439,7 @@
fileSidebar.innerHTML = '';
for (const group of FILE_GROUPS) {
const { groupEl, items } = _makeFileGroup(group.label);
const { groupEl, items } = _makeFileGroup(group.label, group.collapsed || false);
for (const fname of group.files) {
const f = byName[fname];
@@ -1490,12 +1535,20 @@
// Restore editor/preview buttons hidden by audit view
fileRawBtn.style.display = '';
filePreviewBtn.style.display = '';
fileSaveBtn.style.display = '';
const res = await fetch(`/files/${encodeURIComponent(name)}?${_fileParams}`);
if (!res.ok) { mdEditor.setValue(`Error loading ${name}`); return; }
const data = await res.json();
mdEditor.setValue(data.content);
mdEditor.clearHistory();
if (data.readonly) {
mdEditor.setOption('readOnly', 'nocursor');
fileSaveBtn.style.display = 'none';
document.getElementById('file-modal-title').textContent = name + ' (read-only)';
} else {
mdEditor.setOption('readOnly', false);
fileSaveBtn.style.display = '';
document.getElementById('file-modal-title').textContent = name;
}
setFileMode(fileMode);
}
@@ -1794,11 +1847,13 @@
let memMid = localStorage.getItem('mem-mid') !== 'false';
let memShort = localStorage.getItem('mem-short') !== 'false';
const TIER_LABELS = { 1: 'Min', 2: 'Std', 3: 'Ext', 4: 'Full' };
function updateTierUI() {
document.querySelectorAll('.ctx-btn[data-tier]').forEach(btn => {
btn.classList.toggle('active', parseInt(btn.dataset.tier) === currentTier);
});
ctxOpenBtn.querySelector('.tier-badge').textContent = currentTier;
ctxOpenBtn.querySelector('.tier-badge').textContent = TIER_LABELS[currentTier] || currentTier;
}
function updateMemUI() {
@@ -1870,33 +1925,46 @@
memShort = !memShort; localStorage.setItem('mem-short', memShort); updateMemUI();
});
const _distillBtns = () => document.querySelectorAll(
'#distill-short-btn, #distill-mid-btn, #distill-long-btn, #distill-all-btn, #distill-rebuild-btn'
);
function showDistillStatus(msg, isErr) {
distillStatus.textContent = msg;
distillStatus.classList.toggle('err', !!isErr);
distillStatus.classList.add('show');
setTimeout(() => distillStatus.classList.remove('show'), 5000);
setTimeout(() => distillStatus.classList.remove('show'), isErr ? 8000 : 5000);
}
async function runDistill(endpoint) {
showDistillStatus('distilling…', false);
async function runDistill(endpoint, label) {
_distillBtns().forEach(b => { b.disabled = true; });
showDistillStatus(`${label || endpoint} running…`, false);
try {
const res = await fetch(`/distill/${endpoint}?${_fileParams}`, { method: 'POST' });
const d = await res.json();
if (!res.ok || d.ok === false) {
const err = d.error || d.mid?.error || d.long?.error || `HTTP ${res.status}`;
if (res.status === 409 || res.status === 429) {
showDistillStatus(` ${d.detail}`, true);
} else if (!res.ok || d.ok === false) {
const err = d.detail || d.error || d.mid?.error || d.long?.error || `HTTP ${res.status}`;
showDistillStatus(`${err}`, true);
} else {
showDistillStatus(`${endpoint} done`, false);
showDistillStatus(`${label || endpoint} complete`, false);
}
} catch (err) {
showDistillStatus(`${err.message}`, true);
} finally {
_distillBtns().forEach(b => { b.disabled = false; });
}
}
document.getElementById('distill-short-btn').addEventListener('click', () => runDistill('short'));
document.getElementById('distill-mid-btn').addEventListener('click', () => runDistill('mid'));
document.getElementById('distill-long-btn').addEventListener('click', () => runDistill('long'));
document.getElementById('distill-all-btn').addEventListener('click', () => runDistill('all'));
document.getElementById('distill-short-btn').addEventListener('click', () => runDistill('short', 'Short distill'));
document.getElementById('distill-mid-btn').addEventListener('click', () => runDistill('mid', 'Mid distill'));
document.getElementById('distill-long-btn').addEventListener('click', () => runDistill('long', 'Long distill'));
document.getElementById('distill-all-btn').addEventListener('click', () => runDistill('all', 'Full distill'));
document.getElementById('distill-rebuild-btn').addEventListener('click', () => {
if (!confirm('Rebuild memory from scratch?\n\nThis will wipe MEMORY_MID and MEMORY_LONG (backups kept) then regenerate them from session logs. Any hand-edited content will be replaced.\n\nContinue?')) return;
runDistill('rebuild', 'Memory rebuild');
});
updateTierUI();
updateMemUI();