Files
daveadmin 2f05b84b0f feat(tools): DB-backed LLM engine admin (owner-only)
Add an owner-gated dashboard to remap any tool/tier's model live without a
code push. Overrides live in dbn_tool_engine_config (dobetternorge_maindb)
and are consulted by dbnToolsResolveToolRun() + dbnToolsReviewerModel();
no row = unchanged code/.env behaviour (fully back-compatible).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 14:55:17 +02:00

144 lines
5.4 KiB
JavaScript

(function () {
'use strict';
const d = window.DBN_DASHBOARD || {};
const apiBase = (d.apiBase || '/api/dashboard') + '/engines.php';
const $context = document.getElementById('engContext');
const $tier = document.getElementById('engTierPanel');
const $persona = document.getElementById('engPersonaPanel');
const $refresh = document.getElementById('engRefresh');
const safe = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[c]));
let tierEngines = [];
let reviewerModels = [];
function optionsHtml(list, selected) {
return list.map(o =>
'<option value="' + safe(o.key) + '"' + (o.key === selected ? ' selected' : '') + '>' + safe(o.label) + '</option>'
).join('');
}
function toolLabel(row) {
if (row.tool_slug === '*') {
return row.scope.indexOf('persona') === 0 ? 'All personas' : 'All tier tools';
}
return row.tool_slug;
}
function rowHtml(row, list) {
const list2 = list;
const sel = row.override || row.code_default;
const badge = row.override
? '<span class="eng-badge">override</span>'
: '<span class="eng-badge eng-badge--default">default</span>';
const meta = row.override && row.updated_by
? '<div class="eng-meta">by ' + safe(row.updated_by) + (row.updated_at ? ' · ' + safe(row.updated_at) : '') + '</div>'
: '';
return '<div class="eng-row" data-tool="' + safe(row.tool_slug) + '" data-scope="' + safe(row.scope) + '">' +
'<div><span class="eng-row__tool">' + safe(toolLabel(row)) + '</span>' +
'<div class="eng-row__def">default: ' + safe(row.code_default || '—') + '</div>' + meta + '</div>' +
'<div class="eng-row__scope">' + safe(row.scope) + '</div>' +
'<div><select class="eng-select">' + optionsHtml(list2, sel) + '</select></div>' +
'<div class="eng-row__actions">' + badge +
(row.override ? '<button class="dash-btn eng-clear" type="button">Clear</button>' : '') +
'</div>' +
'</div>';
}
function headHtml() {
return '<div class="eng-head"><div>Tool</div><div>Scope</div><div>Engine</div><div></div></div>';
}
function render(data) {
tierEngines = data.tier_engines || [];
reviewerModels = data.reviewer_models || [];
const ctx = data.context || {};
$context.innerHTML = '<strong>Bedrock: ' + (ctx.bedrock_enabled ? 'ON' : 'OFF') + '</strong> — ' + safe(ctx.note || '');
const rows = data.rows || [];
const tierRows = rows.filter(r => r.scope.indexOf('persona') !== 0);
const personaRows = rows.filter(r => r.scope.indexOf('persona') === 0);
$tier.innerHTML = headHtml() + tierRows.map(r => rowHtml(r, tierEngines)).join('');
$persona.innerHTML = headHtml() + personaRows.map(r => rowHtml(r, reviewerModels)).join('');
bind($tier);
bind($persona);
}
function bind(panel) {
panel.querySelectorAll('.eng-row').forEach(rowEl => {
const tool = rowEl.getAttribute('data-tool');
const scope = rowEl.getAttribute('data-scope');
const select = rowEl.querySelector('.eng-select');
const clearBtn = rowEl.querySelector('.eng-clear');
if (select) {
select.addEventListener('change', () => save(tool, scope, select.value, rowEl));
}
if (clearBtn) {
clearBtn.addEventListener('click', () => clear(tool, scope, rowEl));
}
});
}
function setBusy(rowEl, busy) {
rowEl.querySelectorAll('select, button').forEach(el => { el.disabled = busy; });
}
async function post(action, body) {
const r = await fetch(apiBase + '?action=' + action, {
method: 'POST',
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
const data = await r.json();
if (!data.ok) throw new Error(data.message || 'Request failed');
return data;
}
async function save(tool, scope, engine, rowEl) {
setBusy(rowEl, true);
try {
await post('set', { tool_slug: tool, scope: scope, engine: engine });
await load();
} catch (e) {
alert('Could not save: ' + e.message);
setBusy(rowEl, false);
}
}
async function clear(tool, scope, rowEl) {
setBusy(rowEl, true);
try {
await post('clear', { tool_slug: tool, scope: scope });
await load();
} catch (e) {
alert('Could not clear: ' + e.message);
setBusy(rowEl, false);
}
}
async function load() {
$tier.innerHTML = '<div class="dms-loading"></div>';
$persona.innerHTML = '<div class="dms-loading"></div>';
try {
const r = await fetch(apiBase, { credentials: 'same-origin' });
const data = await r.json();
if (!data.ok) throw new Error(data.message || 'Could not load engine map');
render(data);
} catch (e) {
$context.textContent = 'Could not load: ' + e.message;
$tier.innerHTML = '';
$persona.innerHTML = '';
}
}
if ($refresh) $refresh.addEventListener('click', load);
load();
})();