Admin > Reports: counters audit (views vs delivery logs, login days charged vs shown, featured views, clicks vs views, spend vs budget), daily with Telegram alert

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-15 05:32:13 -05:00
parent 86a7f83874
commit 84a9063bbe
4 changed files with 90 additions and 2 deletions
+7 -1
View File
@@ -270,6 +270,12 @@
</div>
<div class="pane" id="pane-reports" hidden>
<div class="card">
<div class="card-head"><h3>Counters audit</h3><span class="sub" id="audSub">views vs delivery logs, charges vs shows</span></div>
<p class="small muted" style="margin:0 0 8px">Every format's recorded views are reconciled against the log that proves delivery, login days charged against days shown, and featured bookings checked for views. Runs daily and alerts you on Telegram; run it any time here.</p>
<div style="margin:0 0 8px"><button type="button" class="btn small sec" id="audRun">Run now</button></div>
<div class="tablewrap"><table class="adm-table" id="audTable"></table></div>
</div>
<div class="card">
<div class="card-head"><h3>Ad reports</h3><span class="sub">members flagging ads</span></div>
<div class="tablewrap"><table class="adm-table" id="repTable"></table></div>
@@ -481,6 +487,6 @@
</div>
<script src="/assets/common.js?v=20260914a"></script>
<script src="/assets/admin.js?v=20260914d"></script>
<script src="/assets/admin.js?v=20260915a"></script>
</body>
</html>
+12
View File
@@ -635,7 +635,19 @@
document.querySelectorAll('#pnlPeriods [data-days]').forEach(b => b.addEventListener('click', () => { pnlDays = Number(b.dataset.days); document.querySelectorAll('#pnlPeriods [data-days]').forEach(x => x.classList.toggle('on', x === b)); loadPnl().catch(e => IAP.status(e.message, 'bad')); }));
if ($('pnlFixedSave')) $('pnlFixedSave').addEventListener('click', async () => { try { await api('/api/admin/site', { pnlFixedMonthlyUsd: Number($('pnlFixed').value) || 0 }, 'PATCH'); IAP.status('Saved.', 'ok'); loadPnl(); } catch (e) { IAP.status(e.message, 'bad'); } });
if ($('burnerRun')) $('burnerRun').addEventListener('click', async () => { try { const r = await api('/api/admin/burner/run', {}); IAP.status('Burner ran: ' + (r.burned || 0) + ' burned.', 'ok'); loadPnl(); } catch (e) { IAP.status(e.message, 'bad'); } });
async function loadAudit() {
if (!$('audTable')) return;
try {
$('audSub').textContent = 'checking…';
const a = await api('/api/admin/audit');
const bad = a.checks.filter(c => !c.ok).length;
$('audSub').textContent = (bad ? bad + ' issue' + (bad === 1 ? '' : 's') : 'all counters reconcile') + ' · checked ' + new Date(a.checkedAt).toLocaleTimeString();
$('audTable').innerHTML = '<tr><th>Check</th><th>Status</th><th>Detail</th></tr>' + a.checks.map(c => '<tr><td>' + esc(c.name) + '</td><td>' + (c.ok ? '<span class="badge">ok</span>' : '<span class="badge amber">' + c.issues.length + ' issue' + (c.issues.length === 1 ? '' : 's') + '</span>') + '</td><td class="small">' + esc(c.detail) + (c.issues.length ? '<br>' + c.issues.map(esc).join('<br>') : '') + '</td></tr>').join('');
} catch (e) { $('audSub').textContent = e.message; }
}
if ($('audRun')) $('audRun').addEventListener('click', busy($('audRun'), loadAudit));
async function loadReports() {
loadAudit();
const [r, b] = await Promise.all([api('/api/admin/reports'), api('/api/admin/burns')]);
const reps = r.reports || [];
$('repTable').innerHTML = reps.length ? '<tr><th>When</th><th>Campaign</th><th>Reason</th><th>Note</th><th>By</th><th></th></tr>'