From e9af56aff478d9426afef163043b7d8705d0ee1a Mon Sep 17 00:00:00 2001 From: Devin Zuczek Date: Sat, 10 Oct 2026 01:05:42 -0400 Subject: [PATCH] [econ] add button for supporter boosts --- apps/econ/README.md | 4 + apps/econ/src/discord-role-gift.ts | 6 +- apps/www/README.md | 9 +++ apps/www/src/client/App.tsx | 94 ++++++++++++++++++++++- apps/www/src/staff.ts | 24 ++++++ apps/www/src/test/integration/api.test.ts | 48 +++++++++++- apps/www/src/www.app.ts | 3 + 7 files changed, 180 insertions(+), 8 deletions(-) diff --git a/apps/econ/README.md b/apps/econ/README.md index 5a8bcca..31feeb7 100644 --- a/apps/econ/README.md +++ b/apps/econ/README.md @@ -614,6 +614,10 @@ role that role's RecCenterTokens in a gift box, every time it fires. The map is account links its Discord, so a new supporter gets one box now rather than after the next run. `www` decides "first" from the link table; this module keeps no ledger either way, and the cron pays that account again on its next run like everyone else. +- **A whole run can be paid by hand.** `www`'s staff panel ("Bonus reward!", developer-only, + `POST /api/staff/discord-roles/supporter-gift`) calls `grantDiscordRoleGifts` with its own + bindings — the same map, audience and box as the cron — and answers with the run's summary. + No ledger here either: it pays everyone again on top of the schedule, which is the point. One D1 read for the links, then a handful of writes and two hub calls per box, sequentially. A cron invocation has a fixed subrequest budget (50 free, 1000 paid); a community whose diff --git a/apps/econ/src/discord-role-gift.ts b/apps/econ/src/discord-role-gift.ts index b888df2..2fb69a2 100644 --- a/apps/econ/src/discord-role-gift.ts +++ b/apps/econ/src/discord-role-gift.ts @@ -205,9 +205,13 @@ export interface RoleGiftSummary { * `startingTokens` is the signup grant to seed before the credit — the credit upserts the * balance row, so a never-touched balance would otherwise start from this gift instead of * the grant plus this gift. + * + * Takes the {@link RoleGiftEnv} pick, not econ's whole `Env`: `www`'s staff panel runs this + * same function on a button (`POST /api/staff/discord-roles/supporter-gift`) with its own + * bindings, and the three it reads are declared on both workers. */ export async function grantDiscordRoleGifts( - env: Env, + env: RoleGiftEnv, startingTokens: number ): Promise { const summary: RoleGiftSummary = { diff --git a/apps/www/README.md b/apps/www/README.md index 197940f..4f4a2a1 100644 --- a/apps/www/README.md +++ b/apps/www/README.md @@ -278,6 +278,15 @@ leaving a silent log line on a worker with observability off. Use it after chang bot token or inviting the bot, or when the role drop needs this morning's roles. The run is written to `audit_log` as `sync_discord_roles` against the developer who pressed it. +A developer can also pay `econ`'s scheduled supporter gift again, now, with +`POST /api/staff/discord-roles/supporter-gift` (developer role required, empty body) — the +"Bonus reward!" card on the account page's Token drop tab. It runs econ's +`grantDiscordRoleGifts` with www's own bindings: every account whose Discord link holds a +role mapped in `DISCORD_ROLE_TOKENS` is boxed that role's tokens, exactly as the cron does, +and the response is the run's summary — `{ skipped, roles, links, granted, tokens, failed }`. +The gift keeps no ledger, so each press pays everyone again on top of the schedule (the panel +asks first). The run is written to `audit_log` as `run_discord_role_gift`. + ## Development ### Run in dev mode diff --git a/apps/www/src/client/App.tsx b/apps/www/src/client/App.tsx index 76b6b05..1d91bbb 100644 --- a/apps/www/src/client/App.tsx +++ b/apps/www/src/client/App.tsx @@ -1606,7 +1606,8 @@ function halloweenActive(): boolean { } /** The sprite, 23x25 native pixels, drawn at 3x with hard edges. */ -const PUMPKIN_SRC = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAZCAYAAADaILXQAAAFD0lEQVR42pWVX2xcxRXGf2fu3Lu7jh0bO3FsnMQ2idP8gZoIGkootZsIGrUlVEVCTVW1qVREUPtSiTckYgQPqEIg1FZU6gtP8EJVAaZpIqE4UtrQOCSIkj+YVgRsp3E2mzisnb27OzOnD+uuE2yJZJ7u6J755jvf+eYc4SbX7qHBbNyjpe5VS5k4Nw3VZj4evcrFiWn+9fZxuTbW3Cw43WcxBMBCNYtYRYwiujDU3ijmwK++rWv7G7BSpLdrGf8Zv8TY+54kCuQ/v4IsckZuBPg7Twzq2ntq1IwXjvzlCz4cPv6VZxcN2LttTfXh9phKLPxtMmVnW7BqAiPnU2aipTzUFnjzonMiFhFn9747vihO9P+PoW29Dz7Yt3LD/sdl3dRl/9NmLRu1GJPzpqfFk2SV6ZIhGwlrVoBP1MQO0xQrQ/1N76/qXL7u4KeXxxbV3Jjwuveu1USGH98XOH44EIopu+7PECJBXGBmVpFKhr71JfqAD0cTVMGY5G1MukAJC/DM9l597IEmVsRTAJz4u2XrG+cxQGnPHuyOP6L797AxOYh1KdGmnYAhHPsrrjrLlq05dNTCtm71Jd82dGTiUh3cKDV7ac2ZAUP60moEB6ffQvfvQeucQt3Bd+3eAZlb+OCV1+n/Zpl3hpsAP8/86QdW6VM/tEjlCmWf8sizGcaiHB9thYQZgg+sfngYMPxuR46VpkS/exMRRQWE5bWL1fD9LsuhcV/Yu707febdz3I29gapBMg0467maG5pI3JlitaSAZaEIk1tHbXqNyxBowTT4IjK0zW72Qoyl7GmJdTPu8SC4dg/Krw6GrhcbeUDrYIY7v+th4uTnHxuaT3NR157j0CVvoN3cvo303PyBPRLjz3MSWiDwMvvrQACEpRg5v6oQVs3sfHFgEpg7NzxufRhbPIE0ZNw+NG1xDbHvnxA/+mR+kVz4Af9p/G31vjq5NkGyppjszpOYFFVQBERfmkNfvVmHIqo4e5cBBiy4RIVyhiRmhnUAG6+cY2M4FzSMNyQc9XWlgoiwi0mkAhkgO4AagUTZVkeWZZloSnyeHzNTdfIMeWgHMmBGbtkX12o5w+ceWj5ra7Y1j4LQK8qwZfRNGVbDgKWINCei+kVT1+Yt6XB4VGEwNEZoZKaXS8cOPWjBV1RCKAWpMLPlrYg1at4CWzf/DkXTt7K16qeFhNxVCI6br/Avrwl+Dw/6Grl5zMrWNbXQFquwpGJhf1cTJYnt16ou8BVpxm8q7bvvCPPW50T/L6rQMcd+TmPQ5rt4RezLdz7aCNrtwi5ZlMYeGKgdF0v2NR/W8FFcatUS/ykI8POzhasVf787/N8rzNi31TKRyu7KSyJicXw30/ykFiCEZbd1k6aFvn6fRmSOEsZ5ZPDbl4Wn2QxCmQa0VCiWrlMKjE+00g5MlQyEaXY4GyEw+EaDZFP8FJG1WGTLGhMpeJAPS5yNeYb7t543ZA6fexUPaP139ioEULcnqGtu5mKKOu3eFDDn84eihiiXt7BXw/o4HdXYnIlxicK12uuaHotMMCZ0VMSVClPpRTOzlL4rMjYUaGnq5nHewb8Y68O1ImpKucu5BmfrNXkpgb0lUK+oz3X1IRa1MiCoSwnTay2dEmI+fio1jRXleFIwYupLgZqggwDJMXkKhtwEEhLji+uKEhdFUZGRtztu3YeMBI1Znzxxgb0tWtw92BWGyitu8dzZlQxCof+cHhRnP8B5aopRAfi2ZcAAAAASUVORK5CYII=' +const PUMPKIN_SRC = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAZCAYAAADaILXQAAAFD0lEQVR42pWVX2xcxRXGf2fu3Lu7jh0bO3FsnMQ2idP8gZoIGkootZsIGrUlVEVCTVW1qVREUPtSiTckYgQPqEIg1FZU6gtP8EJVAaZpIqE4UtrQOCSIkj+YVgRsp3E2mzisnb27OzOnD+uuE2yJZJ7u6J755jvf+eYc4SbX7qHBbNyjpe5VS5k4Nw3VZj4evcrFiWn+9fZxuTbW3Cw43WcxBMBCNYtYRYwiujDU3ijmwK++rWv7G7BSpLdrGf8Zv8TY+54kCuQ/v4IsckZuBPg7Twzq2ntq1IwXjvzlCz4cPv6VZxcN2LttTfXh9phKLPxtMmVnW7BqAiPnU2aipTzUFnjzonMiFhFn9747vihO9P+PoW29Dz7Yt3LD/sdl3dRl/9NmLRu1GJPzpqfFk2SV6ZIhGwlrVoBP1MQO0xQrQ/1N76/qXL7u4KeXxxbV3Jjwuveu1USGH98XOH44EIopu+7PECJBXGBmVpFKhr71JfqAD0cTVMGY5G1MukAJC/DM9l597IEmVsRTAJz4u2XrG+cxQGnPHuyOP6L797AxOYh1KdGmnYAhHPsrrjrLlq05dNTCtm71Jd82dGTiUh3cKDV7ac2ZAUP60moEB6ffQvfvQeucQt3Bd+3eAZlb+OCV1+n/Zpl3hpsAP8/86QdW6VM/tEjlCmWf8sizGcaiHB9thYQZgg+sfngYMPxuR46VpkS/exMRRQWE5bWL1fD9LsuhcV/Yu707febdz3I29gapBMg0467maG5pI3JlitaSAZaEIk1tHbXqNyxBowTT4IjK0zW72Qoyl7GmJdTPu8SC4dg/Krw6GrhcbeUDrYIY7v+th4uTnHxuaT3NR157j0CVvoN3cvo303PyBPRLjz3MSWiDwMvvrQACEpRg5v6oQVs3sfHFgEpg7NzxufRhbPIE0ZNw+NG1xDbHvnxA/+mR+kVz4Af9p/G31vjq5NkGyppjszpOYFFVQBERfmkNfvVmHIqo4e5cBBiy4RIVyhiRmhnUAG6+cY2M4FzSMNyQc9XWlgoiwi0mkAhkgO4AagUTZVkeWZZloSnyeHzNTdfIMeWgHMmBGbtkX12o5w+ceWj5ra7Y1j4LQK8qwZfRNGVbDgKWINCei+kVT1+Yt6XB4VGEwNEZoZKaXS8cOPWjBV1RCKAWpMLPlrYg1at4CWzf/DkXTt7K16qeFhNxVCI6br/Avrwl+Dw/6Grl5zMrWNbXQFquwpGJhf1cTJYnt16ou8BVpxm8q7bvvCPPW50T/L6rQMcd+TmPQ5rt4RezLdz7aCNrtwi5ZlMYeGKgdF0v2NR/W8FFcatUS/ykI8POzhasVf787/N8rzNi31TKRyu7KSyJicXw30/ykFiCEZbd1k6aFvn6fRmSOEsZ5ZPDbl4Wn2QxCmQa0VCiWrlMKjE+00g5MlQyEaXY4GyEw+EaDZFP8FJG1WGTLGhMpeJAPS5yNeYb7t543ZA6fexUPaP139ioEULcnqGtu5mKKOu3eFDDn84eihiiXt7BXw/o4HdXYnIlxicK12uuaHotMMCZ0VMSVClPpRTOzlL4rMjYUaGnq5nHewb8Y68O1ImpKucu5BmfrNXkpgb0lUK+oz3X1IRa1MiCoSwnTay2dEmI+fio1jRXleFIwYupLgZqggwDJMXkKhtwEEhLji+uKEhdFUZGRtztu3YeMBI1Znzxxgb0tWtw92BWGyitu8dzZlQxCof+cHhRnP8B5aopRAfi2ZcAAAAASUVORK5CYII=' const PUMPKIN_SCALE = 3 const PUMPKIN_W = 23 * PUMPKIN_SCALE const PUMPKIN_H = 25 * PUMPKIN_SCALE @@ -1842,8 +1843,12 @@ function HalloweenOverlay() { {!off && (