Track grading spend per user
Attribution comes from the proxy's basic-auth username, which nginx already forwards. Recorded as a ledger of billed calls rather than a column on the grade: a regrade overwrites the grade row, so a per-row cost would forget what earlier runs cost, and deleting a card would erase that it was ever paid for. Splits server-key spend (money the host actually owes) from own-key spend (billed to the visitor) — the two must never be summed. Attribution only, never authorization: the app is reachable directly on the LAN, so these headers are not trustworthy for access control.
This commit is contained in:
parent
762b394c8a
commit
9d5c68b093
4 changed files with 186 additions and 0 deletions
|
|
@ -301,6 +301,7 @@ async function runGrade() {
|
|||
status.hidden = true;
|
||||
$('#grade-label').value = '';
|
||||
loadHistory().catch(() => {});
|
||||
loadUsage().catch(() => {});
|
||||
} catch (err) {
|
||||
status.textContent = `Grading failed: ${err.message}`;
|
||||
} finally {
|
||||
|
|
@ -419,6 +420,43 @@ function renderHistory() {
|
|||
</tr>`).join('');
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------- spend */
|
||||
|
||||
function money(v) {
|
||||
const n = Number(v || 0);
|
||||
// Sub-cent totals are the norm early on; rounding those to $0.00 would
|
||||
// make the panel look broken rather than cheap.
|
||||
return n > 0 && n < 0.01 ? '<$0.01' : `$${n.toFixed(2)}`;
|
||||
}
|
||||
|
||||
async function loadUsage() {
|
||||
let data;
|
||||
try {
|
||||
data = await api('/api/usage');
|
||||
} catch (_) {
|
||||
return; // never let this block the rest of the page
|
||||
}
|
||||
const users = data.users || [];
|
||||
const panel = $('#usage-panel');
|
||||
panel.hidden = users.length === 0;
|
||||
if (!users.length) return;
|
||||
|
||||
const t = data.totals || {};
|
||||
$('#usage-total').textContent =
|
||||
`${t.grades || 0} grading call(s) · ${money(t.server_cost).replace('<', 'under ')} on the server key`;
|
||||
|
||||
$('#usage-body').innerHTML = users.map((u) => {
|
||||
const isYou = data.you && u.username === data.you;
|
||||
return `<tr>
|
||||
<td class="cardcell-name">${esc(u.username)}${isYou ? ' <span class="pill pill-raw">you</span>' : ''}</td>
|
||||
<td class="num">${u.grades}</td>
|
||||
<td class="num">${money(u.server_cost)}</td>
|
||||
<td class="num cardcell-meta">${money(u.own_cost)}</td>
|
||||
<td class="cardcell-meta cell-when">${esc(fmtWhen(u.last_used))}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderEntryModal(g) {
|
||||
$('#entry-modal').innerHTML = `
|
||||
<div class="panel-head">
|
||||
|
|
@ -473,6 +511,7 @@ async function runRegrade(id, body) {
|
|||
try {
|
||||
const data = await api(`/api/history/${id}/regrade`, { method: 'POST', body });
|
||||
applyRegradeResult(data.grade);
|
||||
loadUsage().catch(() => {}); // a regrade is billed too
|
||||
banner('Regraded.');
|
||||
} catch (err) {
|
||||
banner(`Regrade failed: ${err.message}`, true);
|
||||
|
|
@ -677,6 +716,7 @@ async function load() {
|
|||
try { state.modelGuide = await api('/api/vision-models'); } catch (_) { state.modelGuide = {}; }
|
||||
updateModelCostHint();
|
||||
await loadHistory();
|
||||
loadUsage().catch(() => {});
|
||||
if (!state.settings.server_key_configured && !myApiKey()) {
|
||||
banner('Add your Anthropic API key in Settings before grading a card.');
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue