Missions reset with the Central day: a find blocks a mission only for that day (member id or wallet); tag reads Done today

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-20 07:13:46 -05:00
parent 85f710f322
commit f1e1886f48
5 changed files with 19 additions and 9 deletions
+11 -5
View File
@@ -38,11 +38,17 @@ function paidToday(day) {
return store.read('payouts', []).filter(p => p.day === day && p.status !== 'failed' && !p.prize).reduce((n, p) => n + p.pol, 0);
}
// has this member completed this mission already?
// by member id, and also by wallet: two accounts sharing a wallet get one drip per mission between them
// has this member completed this mission TODAY (Central)? Missions reset at midnight Central with the codes and
// the pool (Marty, 2026-09-20: yesterday's finds showed as done today). Checked by member id, and also by wallet:
// two accounts sharing a wallet get one drip per mission per day between them.
function completed(memberId, missionId, wallet) {
const w = wallet ? String(wallet).toLowerCase() : null;
return store.read('payouts', []).some(p => p.missionId === missionId && p.status !== 'failed' && (p.memberId === Number(memberId) || (w && p.wallet && String(p.wallet).toLowerCase() === w)));
const w = wallet ? String(wallet).toLowerCase() : null; const day = ctDay();
return store.read('payouts', []).some(p => p.missionId === missionId && p.day === day && p.status !== 'failed' && !p.prize && (p.memberId === Number(memberId) || (w && p.wallet && String(p.wallet).toLowerCase() === w)));
}
// the mission ids this member (or wallet) has done today
function doneToday(memberId, wallet) {
const w = wallet ? String(wallet).toLowerCase() : null; const day = ctDay();
return new Set(store.read('payouts', []).filter(p => p.day === day && p.status !== 'failed' && !p.prize && (p.memberId === Number(memberId) || (w && p.wallet && String(p.wallet).toLowerCase() === w))).map(p => p.missionId));
}
// record a completion; decide paid-today vs queued
@@ -96,4 +102,4 @@ function totals() {
return { paid: paid.length, pol: Math.round(paid.reduce((n, p) => n + p.pol, 0) * 10000) / 10000, queued: all.filter(p => p.status === 'queued').length, today: paidToday(ctDay()), hunters: new Set(paid.map(p => p.memberId)).size };
}
module.exports = { settings, setSettings, draw, grant, completed, payable, mark, ledger, mine, totals, paidToday, pool, nextResetAt, daily, findsToday };
module.exports = { settings, setSettings, draw, grant, completed, doneToday, payable, mark, ledger, mine, totals, paidToday, pool, nextResetAt, daily, findsToday };
+1 -1
View File
@@ -46,6 +46,6 @@
<footer class="foot">Rewards are for completed missions, not income. The daily pool is limited; when it is spent, claims close until midnight Central. Cryptocurrency involves risk of loss.</footer>
</div>
<div class="modal" id="shareModal" hidden><div class="modal-card"><button class="modal-x" id="shareClose" type="button" aria-label="Close">×</button><div id="shareBody"></div></div></div>
<script src="/app.js?v=8"></script>
<script src="/app.js?v=9"></script>
</body>
</html>
+1 -1
View File
@@ -31,7 +31,7 @@
const isOpen = open && open.missionId === m.id;
const r = m.reward || {};
return '<div class="card' + (isOpen ? ' mission-open' : '') + '" data-id="' + esc(m.id) + '">'
+ '<span class="tag' + (m.done ? ' done' : '') + '">' + (m.done ? 'Completed' : esc(m.site)) + '</span>'
+ '<span class="tag' + (m.done ? ' done' : '') + '">' + (m.done ? 'Done today' : esc(m.site)) + '</span>'
+ '<h3>' + esc(m.name) + '</h3><p>' + esc(m.brief) + '</p>'
+ '<p style="margin-top:10px"><span class="range">' + r.minPol + ' to ' + r.maxPol + ' POL</span> <span style="color:var(--dim);font-size:12px">· ' + m.dwell + 's on the site</span></p>'
+ (m.done ? '' : (board.pool && board.pool.spent && !isOpen) ? '<div style="margin-top:14px"><span class="tag gold">Closed until midnight Central</span></div>' : (board.daily && !board.daily.left && !isOpen) ? '<div style="margin-top:14px"><span class="tag">Back tomorrow</span></div>' : isOpen
+1 -2
View File
@@ -195,8 +195,7 @@ const server = http.createServer(async (req, res) => {
if (p.startsWith('/api/my/')) {
const me = sso.fromRequest(req); if (!me) return json(res, 401, { error: 'Open PolHunter from your InstantAdPay dashboard to sign in.' });
if (p === '/api/my/board') {
const done = new Set(rewards.mine(me.memberId).map(x => x.missionId));
const wdone = me.wallet ? new Set(store.read('payouts', []).filter(x => x.wallet && x.wallet.toLowerCase() === me.wallet && x.status !== 'failed').map(x => x.missionId)) : new Set();
const done = rewards.doneToday(me.memberId, me.wallet); const wdone = done; // today's finds, by member id or wallet
return json(res, 200, { me: { memberId: me.memberId, username: me.username, wallet: me.wallet }, missions: missions.forMember(me.memberId).map(m => Object.assign(pubMission(m), { done: done.has(m.id) || wdone.has(m.id) })), drips: rewards.mine(me.memberId).slice(0, 20), faucet: { on: faucetOn }, pool: rewards.pool(), daily: rewards.daily(me.memberId),
badges: social.badgesFor(me.memberId), badgeCards: (() => { const got = new Set(social.badgesFor(me.memberId).map(b => b.id)); const who = social.nameOf(me); return social.BADGES.map(b => ({ id: b.id, name: b.name, icon: b.icon, why: b.why, art: '/badges/badge-' + b.id + '.jpg', earned: got.has(b.id), page: got.has(b.id) ? SITE + '/b/' + who + '/' + b.id : null, image: got.has(b.id) ? SITE + '/badge-img/' + who + '/' + b.id + '.jpg' : null })); })(), rank: { week: social.rankOf(me.memberId, 'week'), month: social.rankOf(me.memberId, 'month'), all: social.rankOf(me.memberId, 'all') },
share: { link: social.shareLink(SITE, me), joinUrl: 'https://instantadpay.com/join/' + encodeURIComponent(String(me.username || me.memberId)) } });
+5
View File
@@ -62,6 +62,11 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
eq([ok.status, ok.body.pol >= 0.05 && ok.body.pol <= 1, ok.body.queued, ok.body.unlocked], [200, true, false, ['first', 'sweep']], 'the right code pays a drip in range, not queued, and unlocks First Find (and Full Sweep: the test has one site)');
const twice = await call('/api/my/submit', { method: 'POST', body: JSON.stringify({ token: t, code: code.code }) }); eq(twice.status, 400, 'the same mission cannot be claimed twice');
const b2 = await call('/api/my/board'); eq([b2.body.missions[0].done, b2.body.drips.length, b2.body.drips[0].status], [true, 1, 'due'], 'board: done, one drip due (faucet off, so it waits)');
// missions reset with the Central day: a find dated yesterday no longer blocks the mission today
{ const fs3 = require('fs'); const pf = path.join(DIR, 'payouts.json'); const all = JSON.parse(fs3.readFileSync(pf, 'utf8')); const yd = new Date(Date.now() - 86400000).toLocaleDateString('en-CA', { timeZone: 'America/Chicago' }); all[0].day = yd; fs3.writeFileSync(pf, JSON.stringify(all));
const b2b = await call('/api/my/board'); eq(b2b.body.missions[0].done, false, 'yesterdays find does not mark the mission done today');
const again2 = await call('/api/my/start', { method: 'POST', body: JSON.stringify({ missionId: 'test-1' }) }); eq(again2.status, 200, 'the mission can be started again the next day');
all[0].day = new Date().toLocaleDateString('en-CA', { timeZone: 'America/Chicago' }); fs3.writeFileSync(pf, JSON.stringify(all)); }
// the daily pool: once today's committed POL reaches the cap, the board says so and new starts are
// refused until the next Central midnight (Marty, 2026-09-19)