feat(tools): owner feedback review surface + tool_feedback migration
Adds the missing migration for the tool_feedback table (dobetternorge_maindb) that the in-result feedback widget writes to, repoints api/feedback.php to dbnmDb() for consistency with the engine-config table, and adds an owner-only dashboard (page + read API + nav) summarising ratings and notes by tool/engine. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const d = window.DBN_DASHBOARD || {};
|
||||
const api = (d.apiBase || '/api/dashboard') + '/feedback.php';
|
||||
|
||||
const $totals = document.getElementById('fbTotals');
|
||||
const $byTool = document.getElementById('fbByTool');
|
||||
const $byEngine = document.getElementById('fbByEngine');
|
||||
const $recent = document.getElementById('fbRecent');
|
||||
const $refresh = document.getElementById('fbRefresh');
|
||||
|
||||
const safe = s => String(s == null ? '' : s).replace(/[&<>"]/g, c => ({ '&':'&','<':'<','>':'>','"':'"' }[c]));
|
||||
|
||||
function pct(pos, total) {
|
||||
return total > 0 ? Math.round((pos / total) * 100) : 0;
|
||||
}
|
||||
|
||||
function totalsHtml(t) {
|
||||
return '' +
|
||||
'<div class="fb-stat"><div class="fb-stat__num">' + t.total + '</div><div class="fb-stat__lbl">Total</div></div>' +
|
||||
'<div class="fb-stat fb-stat--pos"><div class="fb-stat__num">' + t.positive + '</div><div class="fb-stat__lbl">Positive</div></div>' +
|
||||
'<div class="fb-stat fb-stat--neg"><div class="fb-stat__num">' + t.negative + '</div><div class="fb-stat__lbl">Negative</div></div>' +
|
||||
'<div class="fb-stat"><div class="fb-stat__num">' + pct(t.positive, t.total) + '%</div><div class="fb-stat__lbl">Approval</div></div>';
|
||||
}
|
||||
|
||||
function rowsHtml(rows, nameKey, withMeta) {
|
||||
const head = '<div class="fb-head"><div>' + (nameKey === 'tool' ? 'Tool' : 'Engine') +
|
||||
'</div><div>Total</div><div>Up</div><div>Down</div><div>' + (withMeta ? 'Last' : 'Approval') + '</div></div>';
|
||||
if (!rows.length) return head + '<div class="fb-empty">No feedback yet.</div>';
|
||||
const body = rows.map(r => {
|
||||
const p = pct(r.positive, r.total);
|
||||
const right = withMeta
|
||||
? '<div class="fb-meta">' + safe(r.last_at || '—') + '</div>'
|
||||
: '<div><div class="fb-bar"><div class="fb-bar__fill" style="width:' + p + '%"></div></div>' +
|
||||
'<div class="fb-meta">' + p + '%</div></div>';
|
||||
return '<div class="fb-row">' +
|
||||
'<div class="fb-row__name">' + safe(r[nameKey]) + '</div>' +
|
||||
'<div>' + r.total + '</div>' +
|
||||
'<div class="fb-pos">' + r.positive + '</div>' +
|
||||
'<div class="fb-neg">' + r.negative + '</div>' +
|
||||
right +
|
||||
'</div>';
|
||||
}).join('');
|
||||
return head + body;
|
||||
}
|
||||
|
||||
function recentHtml(rows) {
|
||||
if (!rows.length) return '<div class="fb-empty">No notes yet.</div>';
|
||||
return rows.map(n => {
|
||||
const isPos = n.rating === 'positive';
|
||||
return '<div class="fb-note">' +
|
||||
'<div class="fb-note__head">' +
|
||||
'<span class="fb-note__tool">' + safe(n.tool) + '</span>' +
|
||||
'<span class="fb-note__rating ' + (isPos ? 'is-pos' : 'is-neg') + '">' + (isPos ? '\u{1F44D}' : '\u{1F44E}') + '</span>' +
|
||||
(n.engine ? '<span class="fb-meta">' + safe(n.engine) + '</span>' : '') +
|
||||
'<span class="fb-meta">' + safe(n.created_at || '') + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="fb-note__text">' + safe(n.missed_or_wrong) + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function render(data) {
|
||||
$totals.innerHTML = totalsHtml(data.totals || { total: 0, positive: 0, negative: 0 });
|
||||
$byTool.innerHTML = rowsHtml(data.by_tool || [], 'tool', true);
|
||||
$byEngine.innerHTML = rowsHtml(data.by_engine || [], 'engine', false);
|
||||
$recent.innerHTML = recentHtml(data.recent || []);
|
||||
}
|
||||
|
||||
async function load() {
|
||||
$totals.innerHTML = '<div class="dms-loading"></div>';
|
||||
try {
|
||||
const r = await fetch(api, { credentials: 'same-origin' });
|
||||
const data = await r.json();
|
||||
if (!data.ok) throw new Error(data.message || 'Could not load feedback');
|
||||
render(data);
|
||||
} catch (e) {
|
||||
$totals.innerHTML = '<div class="fb-empty">Could not load: ' + safe(e.message) + '</div>';
|
||||
$byTool.innerHTML = '';
|
||||
$byEngine.innerHTML = '';
|
||||
$recent.innerHTML = '';
|
||||
}
|
||||
}
|
||||
|
||||
if ($refresh) $refresh.addEventListener('click', load);
|
||||
load();
|
||||
})();
|
||||
Reference in New Issue
Block a user