Files
casino/cmd/arcade/static/costs.js
drjones ca39e8bad9 feat(ops): costs page, 5-minute backups, warm standby, kernel tuning
The costs page states both deductions and, for each game, the maths
return alongside what players actually receive. It is rendered from
/api/fees, which the server generates from the same schedule it charges,
so the published terms cannot drift from the behaviour.

Backups every five minutes with a rolling 24 hours. Each dump is checked
for size and format before replacing the previous one — a backup script
that reports success on a truncated file is worse than none, because it
turns a recoverable outage into silent loss found only when needed. A
nightly job restores the newest snapshot and asserts the ledger balances.

The standby continuously restores into a shadow database and swaps only
after verifying the books, so it is never mid-restore when needed and
never promotes a corrupt copy. Promotion does not contact the dead
machine, and it refuses to start if the ledger does not balance.

Kernel tuning is tied to measured limits, not copied defaults. Alby Hub
is explicitly excluded from snapshot restore: publishing a stale channel
state can lose the channel balance outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 23:40:27 +00:00

63 lines
1.9 KiB
JavaScript

/* The costs page.
*
* Every figure is fetched from /api/fees, which the server renders from the
* same schedule it charges. Nothing here is written by hand, so the published
* terms cannot drift from the behaviour — if the operator changes the rake,
* this page changes with it. */
const $ = (id) => document.getElementById(id);
function el(tag, attrs, ...children) {
const node = document.createElement(tag);
for (const [k, v] of Object.entries(attrs || {})) {
if (k === 'class') node.className = v;
else if (k === 'text') node.textContent = v;
else node.setAttribute(k, v);
}
for (const c of children) {
if (c == null) continue;
node.appendChild(typeof c === 'string' ? document.createTextNode(c) : c);
}
return node;
}
async function load() {
let d;
try {
d = await (await fetch('/api/fees')).json();
} catch {
$('schedule').textContent = 'Could not load the fee schedule.';
return;
}
const s = d.schedule || {};
const grid = $('schedule');
const rows = [
['taken from winnings', s.rake_percent],
['payouts rounded to', s.rounding_unit],
['most rounding can cost', s.worst_case_rounding_per_payout],
];
for (const [k, v] of rows) {
grid.appendChild(el('div', {},
el('span', { class: 'k', text: k }),
el('span', { class: 'v', text: v || '—' })));
}
$('worst').textContent = s.worst_case_rounding_per_payout || '—';
const table = $('rtp-table');
const crash = d.crash_games || {};
table.appendChild(el('tr', {},
el('td', { text: 'Crash games' }),
el('td', { text: crash.game_rtp_percent || '—' }),
el('td', { class: 'rtp', text: crash.effective_percent || '—' })));
for (const t of d.scratch_tickets || []) {
table.appendChild(el('tr', {},
el('td', { text: t.ticket }),
el('td', { text: t.game_rtp_percent }),
el('td', { class: 'rtp', text: t.effective_percent })));
}
}
load();