Badge cards: seven AI artworks for the hunting achievements, the hunter's name stamped on the ribbon server-side (ffmpeg), public share pages /b/<who>/<id> with OG cards, badge strip + share modal + unlock celebration on the board, Telegram photo post gated by OUTBOUND

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
martbost
2026-09-19 20:31:20 -05:00
parent 6338af9efe
commit 8eecc7c0cb
19 changed files with 156 additions and 14 deletions
+10 -2
View File
@@ -59,7 +59,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
// claim
const wrong = await call('/api/my/submit', { method: 'POST', body: JSON.stringify({ token: t, code: 'ZZZZZZ' }) }); eq(wrong.status, 400, 'a wrong code is refused');
const ok = await call('/api/my/submit', { method: 'POST', body: JSON.stringify({ token: t, code: code.code }) });
eq([ok.status, ok.body.pol >= 0.05 && ok.body.pol <= 1, ok.body.queued], [200, true, false], 'the right code pays a drip in range, not queued');
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)');
@@ -80,7 +80,15 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
// social: the find is on the leaderboard with a badge, the board carries rank and a share link,
// and a share link visit sets the referral cookie that turns sign-ups into that member's IAP join link
const lb = await call('/api/leaders?period=all'); eq([lb.status, lb.body.rows.length >= 1, lb.body.rows[0].memberId, lb.body.rows[0].finds >= 1, lb.body.rows[0].badges.length >= 1], [200, true, 42, true, true], 'leaderboard: hunter 42 leads all time with a badge');
const bd = await call('/api/badges'); eq(bd.body.badges.length, 7, 'seven badges are defined');
const bd = await call('/api/badges'); eq([bd.body.badges.length, bd.body.badges[0].art], [7, '/badges/badge-first.jpg'], 'seven badges are defined, each with artwork');
// badge cards: the board lists every card with lock state and share URLs; the share page and the
// stamped image exist only for badges the hunter holds
const bc = await call('/api/my/board'); const fc = bc.body.badgeCards.find(x => x.id === 'first'), tc = bc.body.badgeCards.find(x => x.id === 'tracker');
eq([bc.body.badgeCards.length, fc.earned, fc.page, tc.earned, tc.page], [7, true, B + '/b/hunter42/first', false, null], 'board: badge cards with lock state and share page');
const sp = await fetch(B + '/b/hunter42/first'); const spb = await sp.text(); eq([sp.status, /og:image" content="[^"]*\/badge-img\/hunter42\/first\.jpg"/.test(spb), /unlocked/.test(spb)], [200, true, true], 'share page serves with the stamped card as its OG image');
const sp404 = await fetch(B + '/b/hunter42/tracker'); eq(sp404.status, 404, 'no share page for a badge not held');
const bi = await fetch(B + '/badge-img/hunter42/first.jpg'); eq([bi.status, bi.headers.get('content-type')], [200, 'image/jpeg'], 'stamped badge image serves as JPEG');
const bi404 = await fetch(B + '/badge-img/nobody/first.jpg'); eq(bi404.status, 404, 'no card for an unknown hunter');
// weekly prizes: below the minimum nobody wins; with the minimum at 1 the leader gets 3 POL as a due prize drip
// that does not count as a find or against the pool, and the week cannot be awarded twice
const social = require('../lib/social'); const thisMonday = social.weekOf(new Date().toLocaleDateString('en-CA', { timeZone: 'America/Chicago' }));