Campaigns table: click sources stacked so the Clicks column stays narrow, card layout up to 900px; audit alert-day persisted, rounding-scale diffs ignored
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,7 @@ async function run() {
|
|||||||
const rows = await db.q('SELECT campaign_id, SUM(n) n FROM camp_hours GROUP BY campaign_id'); const h = {}; for (const r of rows) h[r.campaign_id] = Number(r.n);
|
const rows = await db.q('SELECT campaign_id, SUM(n) n FROM camp_hours GROUP BY campaign_id'); const h = {}; for (const r of rows) h[r.campaign_id] = Number(r.n);
|
||||||
const issues = [];
|
const issues = [];
|
||||||
// only campaigns that started after the hourly log did
|
// only campaigns that started after the hourly log did
|
||||||
for (const c of camps.filter(c => ['banner', 'text'].includes(c.type) && Number(c.created) >= logStartMs)) { const l = h[c.id] || 0; if (Math.abs(Number(c.imps) - l) > 2) issues.push('#' + c.id + ' ' + c.name.slice(0, 30) + ': ' + c.imps + ' views vs ' + l + ' in the hourly log'); }
|
for (const c of camps.filter(c => ['banner', 'text'].includes(c.type) && Number(c.created) >= logStartMs)) { const l = h[c.id] || 0; if (Math.abs(Number(c.imps) - l) > Math.max(5, Math.round(0.02 * Math.max(Number(c.imps), l)))) issues.push('#' + c.id + ' ' + c.name.slice(0, 30) + ': ' + c.imps + ' views vs ' + l + ' in the hourly log'); }
|
||||||
add('banner/text views vs hourly log', issues, camps.filter(c => ['banner', 'text'].includes(c.type) && Number(c.created) >= logStartMs).length + ' campaigns compared (started since the hourly log began ' + (logStart || '') + ')');
|
add('banner/text views vs hourly log', issues, camps.filter(c => ['banner', 'text'].includes(c.type) && Number(c.created) >= logStartMs).length + ' campaigns compared (started since the hourly log began ' + (logStart || '') + ')');
|
||||||
}
|
}
|
||||||
// 3. login: days charged vs days shown (a day may only be charged after the ad was shown)
|
// 3. login: days charged vs days shown (a day may only be charged after the ad was shown)
|
||||||
@@ -58,11 +58,14 @@ async function run() {
|
|||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
let lastAlertDay = '';
|
// the "once a day" memory lives on the volume: a redeploy restarts the process, and Marty got an alert after every deploy (2026-09-15)
|
||||||
|
const STATE = () => path.join(R.dataDir, 'audit-state.json');
|
||||||
|
function lastAlertDay() { try { return JSON.parse(fs.readFileSync(STATE(), 'utf8')).lastAlertDay || ''; } catch (e) { return ''; } }
|
||||||
|
function setAlertDay(d) { try { fs.writeFileSync(STATE(), JSON.stringify({ lastAlertDay: d })); } catch (e) {} }
|
||||||
async function dailyTick() {
|
async function dailyTick() {
|
||||||
try {
|
try {
|
||||||
const r = await run(); const bad = r.checks.filter(c => !c.ok); const day = new Date().toISOString().slice(0, 10);
|
const r = await run(); const bad = r.checks.filter(c => !c.ok); const day = new Date().toISOString().slice(0, 10);
|
||||||
if (bad.length && lastAlertDay !== day && R && R.notify) { lastAlertDay = day; R.notify('⚠️ InstantAdPay counter audit found ' + bad.length + ' issue' + (bad.length === 1 ? '' : 's') + ': ' + bad.map(c => c.name + ' (' + c.issues.length + ')').join('; ') + '. Admin > Reports > Counters audit.'); }
|
if (bad.length && lastAlertDay() !== day && R && R.notify) { setAlertDay(day); R.notify('⚠️ InstantAdPay counter audit found ' + bad.length + ' issue' + (bad.length === 1 ? '' : 's') + ': ' + bad.map(c => c.name + ' (' + c.issues.length + ')').join('; ') + '. Admin > Reports > Counters audit.'); }
|
||||||
return r;
|
return r;
|
||||||
} catch (e) { console.error('audit', e.message); return null; }
|
} catch (e) { console.error('audit', e.message); return null; }
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -809,7 +809,7 @@
|
|||||||
+ '<td data-l="Type">' + c.type + (c.type === 'banner' && c.width ? ' <span class="muted small">' + c.width + '×' + c.height + '</span>' : '') + (c.dailyCap ? ' <span class="muted small" title="daily cap: ' + c.dailyCap + ' credits, ' + (c.daySpent || 0) + ' spent today">cap ' + c.dailyCap + '/day</span>' : '') + (c.geo ? ' <span class="muted small" title="shown only to viewers in these country tiers">tier ' + esc(c.geo.replace(/,/g, '+')) + '</span>' : '') + schedChips(c) + '</td>'
|
+ '<td data-l="Type">' + c.type + (c.type === 'banner' && c.width ? ' <span class="muted small">' + c.width + '×' + c.height + '</span>' : '') + (c.dailyCap ? ' <span class="muted small" title="daily cap: ' + c.dailyCap + ' credits, ' + (c.daySpent || 0) + ' spent today">cap ' + c.dailyCap + '/day</span>' : '') + (c.geo ? ' <span class="muted small" title="shown only to viewers in these country tiers">tier ' + esc(c.geo.replace(/,/g, '+')) + '</span>' : '') + schedChips(c) + '</td>'
|
||||||
+ '<td class="num" data-l="Views here">' + c.imps.toLocaleString() + '</td>'
|
+ '<td class="num" data-l="Views here">' + c.imps.toLocaleString() + '</td>'
|
||||||
+ '<td class="num" data-l="Network views">' + (['banner', 'text'].includes(c.type) ? (c.impsNas || 0).toLocaleString() : '<span class="muted small" title="only banner and text ads syndicate to the network">n/a</span>') + '</td>'
|
+ '<td class="num" data-l="Network views">' + (['banner', 'text'].includes(c.type) ? (c.impsNas || 0).toLocaleString() : '<span class="muted small" title="only banner and text ads syndicate to the network">n/a</span>') + '</td>'
|
||||||
+ '<td class="num" data-l="Clicks">' + c.clicks + (r.clickSources && r.clickSources[c.id] ? '<div class="muted small" style="white-space:nowrap" title="where the clicks happened">' + Object.entries(r.clickSources[c.id]).sort((a, b) => b[1] - a[1]).map(([k, v]) => esc(k) + ' ' + v).join(' · ') + (c.impsNas ? ' · network: see Network views' : '') + '</div>' : '') + '</td>'
|
+ '<td class="num" data-l="Clicks">' + c.clicks + (r.clickSources && r.clickSources[c.id] ? '<div class="muted small clk-src" title="where the clicks happened">' + Object.entries(r.clickSources[c.id]).sort((a, b) => b[1] - a[1]).map(([k, v]) => '<span>' + esc(k) + ' ' + v + '</span>').join('') + (c.impsNas ? ' · network: see Network views' : '') + '</div>' : '') + '</td>'
|
||||||
+ '<td class="num" data-l="Spent">' + c.spent + '</td><td class="num" data-l="Budget">' + c.budget + '</td>'
|
+ '<td class="num" data-l="Spent">' + c.spent + '</td><td class="num" data-l="Budget">' + c.budget + '</td>'
|
||||||
+ '<td data-l="Status">' + (c.status === 'out' ? '<span class="badge amber">budget spent</span>' : c.status === 'done' ? '<span class="muted">ended</span>' : c.scheduled ? '<span class="badge">scheduled</span>' : c.status) + '</td>'
|
+ '<td data-l="Status">' + (c.status === 'out' ? '<span class="badge amber">budget spent</span>' : c.status === 'done' ? '<span class="muted">ended</span>' : c.scheduled ? '<span class="badge">scheduled</span>' : c.status) + '</td>'
|
||||||
+ '<td class="act">' + (c.status === 'active' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="pause">Pause</button>'
|
+ '<td class="act">' + (c.status === 'active' ? '<button class="btn small sec" data-camp="' + c.id + '" data-act="pause">Pause</button>'
|
||||||
|
|||||||
@@ -713,8 +713,10 @@ img{max-width:100%}
|
|||||||
.tk-table{width:100%;border-collapse:collapse;font-size:13px} .tk-table th,.tk-table td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)} .tk-table th{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)} .tk-table td.n{font-variant-numeric:tabular-nums;text-align:right} .tk-table th.n{text-align:right} .tk-table tr.best td{color:var(--mint)}
|
.tk-table{width:100%;border-collapse:collapse;font-size:13px} .tk-table th,.tk-table td{text-align:left;padding:6px 8px;border-bottom:1px solid var(--line)} .tk-table th{font-size:11px;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)} .tk-table td.n{font-variant-numeric:tabular-nums;text-align:right} .tk-table th.n{text-align:right} .tk-table tr.best td{color:var(--mint)}
|
||||||
.tk-team{border:1px solid var(--line);border-radius:10px;padding:8px 12px;margin:6px 0;font-size:13px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:space-between} .tk-team .who b{font-weight:600} .tk-team .who span{color:var(--muted);font-size:12px} .tk-team.stalled{border-color:rgba(255,209,92,.45)}
|
.tk-team{border:1px solid var(--line);border-radius:10px;padding:8px 12px;margin:6px 0;font-size:13px;display:flex;gap:10px;flex-wrap:wrap;align-items:center;justify-content:space-between} .tk-team .who b{font-weight:600} .tk-team .who span{color:var(--muted);font-size:12px} .tk-team.stalled{border-color:rgba(255,209,92,.45)}
|
||||||
.tk-nudge{width:100%;margin:8px 0 0;padding:8px 12px;border:1px dashed var(--line);border-radius:10px;font-size:13px}
|
.tk-nudge{width:100%;margin:8px 0 0;padding:8px 12px;border:1px dashed var(--line);border-radius:10px;font-size:13px}
|
||||||
/* campaigns list on phones: each row becomes a card, buttons at the bottom, no sideways scroll (Jim, 2026-09-15) */
|
/* click sources stack one per line so the Clicks column stays narrow (Marty, 2026-09-15) */
|
||||||
@media (max-width:720px){
|
.clk-src span{display:block;white-space:nowrap} .camp-table td{white-space:normal} .camp-table td:first-child{max-width:260px}
|
||||||
|
/* campaigns list on phones and tablets: each row becomes a card, buttons at the bottom, no sideways scroll (Jim, 2026-09-15) */
|
||||||
|
@media (max-width:900px){
|
||||||
.tablewrap:has(.camp-table){overflow:visible}
|
.tablewrap:has(.camp-table){overflow:visible}
|
||||||
.camp-table{display:block;width:100%;border-collapse:separate}
|
.camp-table{display:block;width:100%;border-collapse:separate}
|
||||||
.camp-table thead{display:none}
|
.camp-table thead{display:none}
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@
|
|||||||
<title>Member area | InstantAdPay</title>
|
<title>Member area | InstantAdPay</title>
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Sora:wght@600;700;800&display=swap">
|
||||||
<link rel="icon" type="image/png" href="/logo-icon.png">
|
<link rel="icon" type="image/png" href="/logo-icon.png">
|
||||||
<link rel="stylesheet" href="/assets/site.css?v=20260915a">
|
<link rel="stylesheet" href="/assets/site.css?v=20260915b">
|
||||||
</head>
|
</head>
|
||||||
<body class="bo-body">
|
<body class="bo-body">
|
||||||
|
|
||||||
@@ -984,7 +984,7 @@
|
|||||||
<script src="/assets/common.js?v=20260914a"></script>
|
<script src="/assets/common.js?v=20260914a"></script>
|
||||||
<script src="/assets/wallet.js?v=20260911a"></script>
|
<script src="/assets/wallet.js?v=20260911a"></script>
|
||||||
<script src="/assets/promo.js?v=20260911a"></script>
|
<script src="/assets/promo.js?v=20260911a"></script>
|
||||||
<script src="/assets/my.js?v=20260915a"></script>
|
<script src="/assets/my.js?v=20260915b"></script>
|
||||||
<script src="/assets/chat.js?v=20260907l"></script>
|
<script src="/assets/chat.js?v=20260907l"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user