From b0baf72f0a59db9d4ced29e7ec3ec558d1334c02 Mon Sep 17 00:00:00 2001 From: xCyanGrizzly Date: Sat, 4 Jul 2026 09:14:33 +0200 Subject: [PATCH] docs: spec for send-all-from-creator button on STL page Co-Authored-By: Claude Opus 4.8 (1M context) --- ...2026-07-04-send-all-from-creator-design.md | 170 ++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md diff --git a/docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md b/docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md new file mode 100644 index 0000000..3b7535c --- /dev/null +++ b/docs/superpowers/specs/2026-07-04-send-all-from-creator-design.md @@ -0,0 +1,170 @@ +# Send All From Creator — Design + +**Date:** 2026-07-04 +**Status:** Approved for planning + +## Summary + +Add a "Send all from [Creator]" button to the STL packages page that queues every +sendable package from a given creator for delivery to the user's Telegram. The +button appears in the packages-tab toolbar only when the list is filtered by a +single creator (`?creator=`). + +This reuses the existing single-send infrastructure (`BotSendRequest` + +`pg_notify('bot_send', id)` + the bot's `send-listener`) and closely mirrors the +existing `sendAllInGroupAction`. No bot, DB schema, or send-listener changes are +required. + +## Context + +Relevant existing pieces: + +- **Creator field:** `Package.creator` is a nullable, indexed `String` on the + `Package` model (`prisma/schema.prisma:476-524`). Filtering by exact creator is + cheap. +- **Creator filter:** The packages list already supports `?creator=` + (`src/app/(app)/stls/page.tsx:22,38`), rendered by the packages column as a + clickable value (`package-columns.tsx`). +- **Single send:** `send-to-telegram-button.tsx` → `POST /api/telegram/bot/send` + creates a `BotSendRequest` (status `PENDING`) and fires + `pg_notify('bot_send', requestId)`; the button then polls the request to + completion. +- **Existing bulk send (group):** `sendAllInGroupAction(groupId)` in + `src/app/(app)/stls/actions.ts:515-591` fetches the group's packages, filters to + those with `destChannelId` + `destMessageId`, skips any with an existing + `PENDING`/`SENDING` request for the same package + telegram link, creates a + `BotSendRequest` per remaining package, and fires `pg_notify` per package. It is + wired into the table via `handleSendAllInGroup` (`stl-table.tsx:264-278`) with a + `confirm()` dialog and a success toast — no polling. +- **Bot side:** `bot/src/send-listener.ts` processes every `BotSendRequest` + uniformly; nothing there needs to change. + +## Requirements + +1. When the packages tab is filtered by a single creator (`?creator=` is + present), show a toolbar button labeled **"Send all from [Creator]"**. +2. When no creator filter is active, the button is not rendered. +3. Clicking the button shows a confirmation dialog before sending (consistent with + the single-send and group-send flows, since this pushes files to Telegram). +4. On confirm, queue **all** sendable packages from that creator across all pages — + not just the current page. +5. Skip packages that are not yet uploaded (missing `destChannelId` / + `destMessageId`) and packages that already have a `PENDING`/`SENDING` request for + the same package + telegram link (dedup). +6. Report real counts back to the user via toast, e.g. + "Queued 12 packages from [Creator]" and, when applicable, a skipped count. +7. No polling — the bulk action queues and returns; sends process in the background + via the bot listener (same behavior as group send). + +## Design + +### Server action — `sendAllFromCreatorAction(creatorName: string)` + +New action in `src/app/(app)/stls/actions.ts`, mirroring `sendAllInGroupAction`: + +1. Auth check (`auth()`), return `Unauthorized` if no session. +2. Load the user's `TelegramLink`; if none, return + "No linked Telegram account. Link one in Settings." +3. Reject an empty/blank `creatorName` (guard against sending the entire "no + creator" set — see Edge cases). +4. Fetch sendable packages directly with a `where` filter (no in-memory filtering): + ```ts + prisma.package.findMany({ + where: { + creator: creatorName, + destChannelId: { not: null }, + destMessageId: { not: null }, + }, + select: { id: true }, + }) + ``` +5. If none, return "No uploaded packages found for this creator." +6. For each package, skip if an existing `PENDING`/`SENDING` `BotSendRequest` + exists for that package + telegram link; otherwise create a `BotSendRequest` + (status `PENDING`) and fire `pg_notify('bot_send', id)` (best-effort, wrapped in + try/catch — the bot also polls). +7. `revalidatePath("/stls")`. +8. Return counts so the UI can show them. This is a small improvement over + `sendAllInGroupAction`, which currently returns `{ success: true, data: undefined }`. + Shape: + ```ts + { success: true, data: { queued: number, skipped: number } } + ``` + where `skipped` counts packages that already had a live request. (Packages not + yet uploaded are excluded by the query and not counted as skipped.) + +### UI — toolbar button in `stl-table.tsx` + +- Derive the active creator from the URL: `const activeCreator = + searchParams.get("creator") ?? "";` (parallel to the existing + `activeTag` at `stl-table.tsx:445`). +- In the packages-tab toolbar (`stl-table.tsx:478` `flex flex-wrap items-center + gap-2` row), render the button only when `activeCreator` is non-empty: + ```tsx + {activeCreator && ( + + )} + ``` + (Reuse the icon already used by the send action; place near the Upload / Group + buttons.) +- Add `handleSendAllFromCreator`, modeled on `handleSendAllInGroup` + (`stl-table.tsx:264-278`): + ```ts + const handleSendAllFromCreator = useCallback(() => { + if (!confirm(`Send all packages from "${activeCreator}" to your Telegram?`)) return; + startTransition(async () => { + const result = await sendAllFromCreatorAction(activeCreator); + if (result.success) { + const { queued, skipped } = result.data; + toast.success( + `Queued ${queued} package${queued === 1 ? "" : "s"} from ${activeCreator}` + + (skipped ? ` (${skipped} already queued)` : "") + ); + router.refresh(); + } else { + toast.error(result.error); + } + }); + }, [activeCreator, router]); + ``` +- Import `sendAllFromCreatorAction` alongside the existing `sendAllInGroupAction` + import (`stl-table.tsx:51`). + +### Data flow + +``` +User (filtered by creator) → clicks button → confirm dialog + → sendAllFromCreatorAction(creatorName) + → for each sendable, un-queued package: create BotSendRequest + pg_notify + → returns { queued, skipped } → toast + router.refresh() + +(background) bot send-listener consumes each BotSendRequest → delivers to user +``` + +## Edge cases + +- **Blank creator:** The action rejects an empty/whitespace `creatorName` so a + stray `?creator=` never queues every package. The UI already gates the button on + a non-empty `activeCreator`, so this is defense-in-depth. +- **No linked Telegram account:** Returns an error toast, queues nothing. +- **No uploaded packages for creator:** Returns an error toast, queues nothing. +- **All packages already queued:** `queued: 0`, `skipped: N` → toast reflects it. +- **pg_notify failure:** Swallowed per-package (best-effort); the bot's periodic + poll picks the request up. Matches existing behavior. + +## Out of scope / non-goals + +- No per-row "send all from this creator" action (toolbar-only, per decision). +- No progress polling / live completion tracking for the bulk send. +- No changes to the bot, `send-listener`, `BotSendRequest` schema, or API routes. +- No batching/throttling beyond what the bot listener already does. + +## Files touched + +- `src/app/(app)/stls/actions.ts` — add `sendAllFromCreatorAction`. +- `src/app/(app)/stls/_components/stl-table.tsx` — derive `activeCreator`, add + `handleSendAllFromCreator`, render the toolbar button, import the new action.