Files
dragonsstash/docs/superpowers/plans/2026-07-04-send-all-from-creator.md
2026-07-04 09:15:58 +02:00

8.9 KiB

Send All From Creator Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Add a toolbar button on the STL packages page that queues every sendable package from the currently-filtered creator for delivery to the user's Telegram.

Architecture: A new server action sendAllFromCreatorAction(creatorName) mirrors the existing sendAllInGroupAction: it fetches sendable packages for the creator, dedups against live BotSendRequests, creates a BotSendRequest per package, and fires pg_notify('bot_send', id). The bot's existing send-listener consumes these unchanged. The client table renders a "Send all from [Creator]" button in the packages-tab toolbar only when ?creator=<name> is active, wired to the action with a confirm dialog and a count toast.

Tech Stack: Next.js 16 App Router, TypeScript, Prisma v7, server actions, pg_notify, sonner toasts, lucide-react icons.

Testing note: This repo has no test framework (CLAUDE.md: "Testing is manual"). Verification for each task is npm run lint + npm run build, plus manual UI checks at the end. Do not scaffold a test harness.


Task 1: Add sendAllFromCreatorAction server action

Files:

  • Modify: src/app/(app)/stls/actions.ts (append new action after sendAllInGroupAction, which ends at line 591)

Reference implementation to mirror: sendAllInGroupAction at src/app/(app)/stls/actions.ts:515-591. Existing imports already present in the file (do not re-add): auth from @/lib/auth, prisma from @/lib/prisma, ActionResult from @/types/api.types, revalidatePath from next/cache. ActionResult is generic: ActionResult<T = void> (src/types/api.types.ts).

  • Step 1: Append the new action

Add this to the end of src/app/(app)/stls/actions.ts:

export async function sendAllFromCreatorAction(
  creatorName: string
): Promise<ActionResult<{ queued: number; skipped: number }>> {
  const session = await auth();
  if (!session?.user?.id) return { success: false, error: "Unauthorized" };

  const creator = creatorName.trim();
  if (!creator) {
    return { success: false, error: "No creator specified" };
  }

  try {
    const telegramLink = await prisma.telegramLink.findUnique({
      where: { userId: session.user.id },
    });

    if (!telegramLink) {
      return { success: false, error: "No linked Telegram account. Link one in Settings." };
    }

    const sendablePackages = await prisma.package.findMany({
      where: {
        creator,
        destChannelId: { not: null },
        destMessageId: { not: null },
      },
      select: { id: true },
    });

    if (sendablePackages.length === 0) {
      return { success: false, error: "No uploaded packages found for this creator" };
    }

    let queued = 0;
    let skipped = 0;
    for (const pkg of sendablePackages) {
      // Only create if no existing PENDING/SENDING request for this package+link combo
      const existing = await prisma.botSendRequest.findFirst({
        where: {
          packageId: pkg.id,
          telegramLinkId: telegramLink.id,
          status: { in: ["PENDING", "SENDING"] },
        },
      });

      if (existing) {
        skipped++;
        continue;
      }

      const sendRequest = await prisma.botSendRequest.create({
        data: {
          packageId: pkg.id,
          telegramLinkId: telegramLink.id,
          requestedByUserId: session.user.id,
          status: "PENDING",
        },
      });

      // Notify the bot via pg_notify
      try {
        await prisma.$queryRawUnsafe(
          `SELECT pg_notify('bot_send', $1)`,
          sendRequest.id
        );
      } catch {
        // Best-effort — the bot also polls periodically
      }

      queued++;
    }

    revalidatePath("/stls");
    return { success: true, data: { queued, skipped } };
  } catch {
    return { success: false, error: "Failed to send creator packages" };
  }
}
  • Step 2: Verify lint + typecheck pass

Run: npm run lint Expected: no new errors in src/app/(app)/stls/actions.ts.

  • Step 3: Commit
git add src/app/\(app\)/stls/actions.ts
git commit -m "feat(stls): add sendAllFromCreatorAction server action"

Task 2: Wire up the toolbar button in the STL table

Files:

  • Modify: src/app/(app)/stls/_components/stl-table.tsx

    • lucide import (line 6)
    • actions import block (lines 44-54)
    • handler (after handleSendAllInGroup, which ends at line 278)
    • activeCreator derivation (near activeTag at line 445)
    • toolbar JSX (packages-tab toolbar row starting at line 478)
  • Step 1: Add the Send icon to the lucide import

Change line 6 from:

import { Search, Layers, Upload } from "lucide-react";

to:

import { Search, Layers, Upload, Send } from "lucide-react";
  • Step 2: Import the new action

In the actions import block (lines 44-54), add sendAllFromCreatorAction next to sendAllInGroupAction:

import {
  updatePackageCreator,
  updatePackageTags,
  renameGroupAction,
  dissolveGroupAction,
  createGroupAction,
  removeFromGroupAction,
  sendAllInGroupAction,
  sendAllFromCreatorAction,
  updateGroupPreviewAction,
  mergeGroupsAction,
} from "../actions";
  • Step 3: Derive the active creator from the URL

Immediately after the activeTag line (src/app/(app)/stls/_components/stl-table.tsx:445):

  const activeTag = searchParams.get("tag") ?? "";

add:

  const activeCreator = searchParams.get("creator") ?? "";
  • Step 4: Add the click handler

After handleSendAllInGroup (which ends at line 278), add:

  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]);
  • Step 5: Render the toolbar button

In the packages-tab toolbar (flex flex-wrap items-center gap-2 row at line 478), add the button right after the "Upload Files" button (which closes at line 507, before the selectedPackages.size >= 2 block at line 508):

            {activeCreator && (
              <Button
                variant="outline"
                size="sm"
                className="h-9 gap-1.5"
                onClick={handleSendAllFromCreator}
              >
                <Send className="h-3.5 w-3.5" />
                Send all from {activeCreator}
              </Button>
            )}
  • Step 6: Verify lint + build pass

Run: npm run lint && npm run build Expected: no new errors; build completes.

  • Step 7: Manual verification
  1. npm run dev, open /stls.
  2. Click a creator name in the Creator column to apply ?creator=<name> (or navigate to /stls?creator=<known creator>).
  3. Confirm the "Send all from [Creator]" button appears in the toolbar.
  4. Remove the creator filter → confirm the button disappears.
  5. Click the button → confirm the dialog appears; on confirm, a toast reports the queued count.
  6. (If a linked Telegram account + uploaded packages exist) confirm packages arrive via the bot.
  • Step 8: Commit
git add src/app/\(app\)/stls/_components/stl-table.tsx
git commit -m "feat(stls): add 'send all from creator' toolbar button"

Self-Review

Spec coverage:

  • Req 1 (button when filtered by creator) → Task 2 Step 5 ({activeCreator && ...}).
  • Req 2 (hidden when no filter) → Task 2 Step 5 conditional.
  • Req 3 (confirm dialog) → Task 2 Step 4 confirm(...).
  • Req 4 (all pages, not current page) → Task 1 queries prisma.package.findMany by creator, unpaginated.
  • Req 5 (skip not-uploaded + dedup live requests) → Task 1 where filter on destChannelId/destMessageId + existing check.
  • Req 6 (report counts) → Task 1 returns { queued, skipped }; Task 2 Step 4 toast.
  • Req 7 (no polling) → Task 2 handler queues + router.refresh(), no poll loop.
  • Blank-creator guard → Task 1 creator.trim() check.

Placeholder scan: No TBD/TODO/placeholder steps; all code shown in full.

Type consistency: Action returns ActionResult<{ queued: number; skipped: number }>; handler destructures result.data.{queued,skipped} inside the result.success branch (where data is typed). sendAllFromCreatorAction name matches between Task 1 definition, Task 2 import, and Task 2 call site. activeCreator defined once (Step 3) and used in Steps 4-5.