Skip to content

feat: implement maintainer dashboard actions for multi-winner milestone bounties#265

Merged
Benjtalkshow merged 9 commits into
boundlessfi:mainfrom
Jopsan-gm:issue-205-maintainer-dashboard-actions
May 31, 2026
Merged

feat: implement maintainer dashboard actions for multi-winner milestone bounties#265
Benjtalkshow merged 9 commits into
boundlessfi:mainfrom
Jopsan-gm:issue-205-maintainer-dashboard-actions

Conversation

@Jopsan-gm
Copy link
Copy Markdown
Contributor

@Jopsan-gm Jopsan-gm commented May 30, 2026

Title feat: implement maintainer dashboard actions for multi-winner milestone bounties

Closes

Closes #205

Description

This PR implements the requested actions for the Model 4 (Multi-Winner Milestone) Maintainer Dashboard, transitioning it from a static UI to a fully functional interface with optimistic updates.

Changes Made:

  • Replaced the generic handleAction stub with dedicated mutation hooks using @tanstack/react-query (releasePayment, advanceContributor, removeContributor, sendMessage).
  • Integrated sonner toasts to provide immediate success feedback after an action is executed.
  • Cleaned up the UI by removing the [Coming soon] suffix from action buttons.
  • Implemented real Dialog components for the View Submissions and Message actions.
  • Passed bountyId appropriately to ensure the mutations target the correct bounty scope.
  • Fixed strict TypeScript and ESLint type inference errors related to the extended/mocked contributorProgress fields.

Screenshots

image

Summary by CodeRabbit

  • New Features

    • Maintainer dashboard: full contributor management — send messages, view submissions, release milestone payments, advance contributors, and remove contributors.
    • Per-contributor action states and success toasts; message dialog validates input; submissions dialog shows an empty-state when none exist.
    • Actions now operate scoped to the selected bounty for consistent behavior.
  • Chores

    • CI workflow updated (Node.js/runtime and E2E install adjustments).

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

@Jopsan-gm is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d805808f-d604-4729-9d3b-a2a00f7b9fe5

📥 Commits

Reviewing files that changed from the base of the PR and between 601a0c0 and c00beee.

📒 Files selected for processing (2)
  • components/bounty-detail/bounty-detail-client.tsx
  • hooks/use-bounty-application.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • components/bounty-detail/bounty-detail-client.tsx
  • hooks/use-bounty-application.ts

📝 Walkthrough

Walkthrough

Adds bounty-scoped mutation hooks and EscrowService.releasePayment, wires Model4MaintainerDashboard to use those hooks with per-contributor loading and two dialogs, and updates CI Node version and Playwright install step.

Changes

Model 4 Maintainer Dashboard Actions Implementation

Layer / File(s) Summary
Bounty mutation hooks and escrow release
hooks/use-bounty-application.ts, lib/services/escrow.ts
Adds delay helper and ExtendedBountyQuery type, module-level recordedMessages, and exports useReleasePayment(bountyId), useAdvanceContributor(bountyId), useRemoveContributor(bountyId), useSendMessage(bountyId). Implements optimistic cache updates/rollbacks and invalidations; adds EscrowService.releasePayment(poolId, amount).
Dashboard wiring, handlers, and dialogs
components/bounty-detail/model4-maintainer-dashboard.tsx, components/bounty-detail/bounty-detail-client.tsx
Model4MaintainerDashboard now requires bountyId, initializes bounty-scoped hooks, maintains selected contributor and dialog state, implements per-contributor handlers (release, advance, remove, open submissions/message, send message) with sonner toasts, contributor-specific loading spinners, and two Dialogs (submissions + message). Parent passes bountyId down.
CI job node/version and install tweak
.github/workflows/ci.yml
Change Node matrix to 22.x for build-and-lint and test-e2e, adjust timeout-minutes, and update Playwright Chromium install command to pnpm exec playwright install chromium.

Sequence Diagram(s)

sequenceDiagram
  participant User as Maintainer (UI)
  participant Dashboard as Model4MaintainerDashboard
  participant Hook as useReleasePayment / useAdvanceContributor / useRemoveContributor / useSendMessage
  participant QueryClient as ReactQueryCache
  participant Escrow as EscrowService

  User->>Dashboard: click action (Release/Advance/Remove/Message/View Submissions)
  Dashboard->>Hook: mutate({ contributorId, ... }) or open dialog
  Hook->>QueryClient: cancel/read/update optimistic cache (bountyDetail/escrow)
  alt releasePayment
    Hook->>Escrow: releasePayment(poolId, amount)
    Escrow-->>Hook: updated EscrowPool
    Hook->>QueryClient: update escrow cache + invalidate bountyDetail + escrow queries
  else advance/remove
    Hook->>QueryClient: optimistic update contributorProgress or remove contributor -> invalidate on success
  else sendMessage
    Hook->>Hook: store message in recordedMessages (no cache change)
  end
  Hook-->>Dashboard: mutation success/failure (toast, close dialog)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related issues

  • #205: Implements the maintainer dashboard actions and meets the listed acceptance criteria for Model 4 multi-winner milestone bounties.
  • boundlessfi/bounties#142: Related to Model‑4 multi-winner milestone feature work and overlaps with action/hook changes.

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

🐰 I hopped by code with a twitch and a grin,

Buttons now buzz and dialogs begin.
Spinners whirl, toasts cheer — contributors play,
Escrow opens gates and progress finds its way.
Hooray — maintainer tools are bright today!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes: types/bounty.ts contains 23 lines (BountyApplication interface and fields) from PR #263/issue #211, which the reviewer explicitly requested be reverted as the dashboard does not depend on them. Revert types/bounty.ts to upstream to remove the BountyApplication interface and related fields (applications, claimCount, maxParticipants, assignedContributorId) that belong to PR #263/issue #211.
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the primary change: implementing the maintainer dashboard actions for multi-winner milestone bounties.
Linked Issues check ✅ Passed The PR addresses all five required actions from issue #205: Release Payment (with escrow update), Advance, Remove (with slot decrement), View Submissions (modal), and Message (dialog with persistence). All acceptance criteria are met with spinner indicators and toast notifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (2)
hooks/use-bounty-application.ts (2)

32-88: ⚡ Quick win

Missing onSettled invalidation for optimistic mutations.

advanceContributor and removeContributor use optimistic updates but never reconcile with the server after the mutation completes. If the server state differs from the optimistic update (e.g., another maintainer made changes), the cache will remain stale until the user manually refreshes.

Add onSettled to invalidate and refetch
   const advanceContributor = useMutation({
     // ... existing code ...
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
     },
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey: bountyKeys.detail(bountyId) });
+    },
   });

   const removeContributor = useMutation({
     // ... existing code ...
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
     },
+    onSettled: () => {
+      queryClient.invalidateQueries({ queryKey: bountyKeys.detail(bountyId) });
+    },
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` around lines 32 - 88, The optimistic update
in advanceContributor (and similarly in removeContributor) never reconciles with
the server; add an onSettled handler to the useMutation config for
advanceContributor that calls
queryClient.invalidateQueries(bountyKeys.detail(bountyId)) (and optionally
queryClient.refetchQueries) to ensure the bounty detail cache is refetched after
success or error; locate the useMutation for advanceContributor (and the
removeContributor mutation) and append an onSettled: (_data, _error, _vars,
_context) => { queryClient.invalidateQueries(bountyKeys.detail(bountyId)); } to
properly refresh server state.

113-117: TODO comment indicates incomplete implementation for totalSlotsOccupied.

The comment acknowledges that totalSlotsOccupied should be decremented but isn't implemented. Per issue #205, removing a contributor should "decrement totalSlotsOccupied (visible removal and slot count update)."

Would you like me to help implement the totalSlotsOccupied decrement, or should this be tracked as a follow-up issue?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` around lines 113 - 117, The TODO notes that
totalSlotsOccupied isn't decremented when removing a contributor; update the
same update patch that builds the new state (the object containing
contributorProgress: contributorProgress.filter((c)=>c.userId!==contributorId))
to also adjust totalSlotsOccupied: read the current totalSlotsOccupied value
and, if it's a number, set totalSlotsOccupied to Math.max(0, totalSlotsOccupied
- 1) (preserving immutability), otherwise leave it undefined or omit it; ensure
you reference the same update object where contributorProgress is modified so
the removal and slot decrement happen atomically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/bounty-detail/model4-maintainer-dashboard.tsx`:
- Around line 60-71: Add onError handlers to the mutation calls so failures
surface to users; for each mutate invocation (e.g., releasePayment.mutate in
handleReleasePayment and the other mutate calls referenced around the same area
like approveContribution.mutate and rejectContribution.mutate), pass an onError
callback that calls toast.error with a clear message that includes the error
message or a fallback (e.g., `toast.error(\`Failed to release payment:
${error?.message ?? 'unknown error'}\`)`). Ensure the onError is paired with the
existing onSuccess handlers and uses the mutation-specific context
(contributor.userName or relevant IDs) to make the toast informative.
- Around line 104-115: handleSendMessage currently calls sendMessage.mutate
without an onError handler; update handleSendMessage to pass an onError callback
to sendMessage.mutate (alongside the existing onSuccess) that shows an error
toast (e.g., toast.error with a descriptive message and optional error.message)
and keeps the message UI open or resets state appropriately. Locate the
sendMessage.mutate call inside the handleSendMessage function and add the
onError option to mirror other mutation handlers in this file.
- Around line 268-276: The Advance button currently renders only Loader2 when
advanceContributor.isPending for the contributor, removing the "Advance" text
and ArrowRight icon; update the JSX in the Advance button (the block using
advanceContributor, contributor.userId, Loader2 and ArrowRight) to always render
the "Advance" label and ArrowRight icon and conditionally render Loader2
alongside them when advanceContributor.isPending (e.g., show Loader2
before/after the label and keep the label+icon visible), preserving the existing
conditional check on advanceContributor.variables?.contributorId to target the
correct contributor.

In `@hooks/use-bounty-application.ts`:
- Around line 83-88: The optimistic mutation handlers (advanceContributor and
removeContributor) currently only rollback via queryClient.setQueryData in their
onError callbacks, leaving users uninformed; update both mutation definitions to
add an onError that (1) rolls back as now using context.previous (keep
queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous)), and
(2) triggers a user-facing error toast/message with the error details (use the
mutation onError params to extract the error) so the UI both reverts and shows a
clear failure message to the user; reference the existing onError blocks for
these mutations and reuse bountyId, bountyKeys.detail, and the context.previous
logic.
- Around line 60-78: The cache update path assumes there is a next milestone but
doesn't handle the case when milestoneIndex === milestones.length - 1, so the
mutation reports success with no state change; update the mutation to guard
against already-at-final-milestone by checking milestoneIndex against
milestones.length - 1 (using the same variables: milestoneIndex, milestones,
contributorIndex, contributorProgress) and either return early or throw an error
from the mutationFn so the success path (which calls queryClient.setQueryData on
bountyKeys.detail(bountyId)) and the success toast are not executed; ensure the
guard is placed before the code that constructs newProgress and calls
queryClient.setQueryData to prevent misleading UX.

---

Nitpick comments:
In `@hooks/use-bounty-application.ts`:
- Around line 32-88: The optimistic update in advanceContributor (and similarly
in removeContributor) never reconciles with the server; add an onSettled handler
to the useMutation config for advanceContributor that calls
queryClient.invalidateQueries(bountyKeys.detail(bountyId)) (and optionally
queryClient.refetchQueries) to ensure the bounty detail cache is refetched after
success or error; locate the useMutation for advanceContributor (and the
removeContributor mutation) and append an onSettled: (_data, _error, _vars,
_context) => { queryClient.invalidateQueries(bountyKeys.detail(bountyId)); } to
properly refresh server state.
- Around line 113-117: The TODO notes that totalSlotsOccupied isn't decremented
when removing a contributor; update the same update patch that builds the new
state (the object containing contributorProgress:
contributorProgress.filter((c)=>c.userId!==contributorId)) to also adjust
totalSlotsOccupied: read the current totalSlotsOccupied value and, if it's a
number, set totalSlotsOccupied to Math.max(0, totalSlotsOccupied - 1)
(preserving immutability), otherwise leave it undefined or omit it; ensure you
reference the same update object where contributorProgress is modified so the
removal and slot decrement happen atomically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d68301e8-8fd6-473d-99a3-57712b9a2bfb

📥 Commits

Reviewing files that changed from the base of the PR and between 227aa7a and 5e53aa8.

📒 Files selected for processing (3)
  • components/bounty-detail/bounty-detail-client.tsx
  • components/bounty-detail/model4-maintainer-dashboard.tsx
  • hooks/use-bounty-application.ts

Comment on lines +60 to +71
const handleReleasePayment = (contributor: ContributorProgress) => {
releasePayment.mutate(
{
contributorId: contributor.userId,
milestoneId: contributor.currentMilestoneId,
},
{
onSuccess: () =>
toast.success(`Payment released for ${contributor.userName}`),
},
);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing onError callbacks on mutation calls.

The mutate calls only handle onSuccess but don't handle onError. If the hook-level onError doesn't show a toast (which it currently doesn't), users won't know their action failed.

Add onError callbacks to show error toasts
   const handleReleasePayment = (contributor: ContributorProgress) => {
     releasePayment.mutate(
       {
         contributorId: contributor.userId,
         milestoneId: contributor.currentMilestoneId,
       },
       {
         onSuccess: () =>
           toast.success(`Payment released for ${contributor.userName}`),
+        onError: () =>
+          toast.error(`Failed to release payment for ${contributor.userName}`),
       },
     );
   };

   const handleAdvance = (contributor: ContributorProgress) => {
     advanceContributor.mutate(
       { contributorId: contributor.userId },
       {
         onSuccess: () =>
           toast.success(`${contributor.userName} advanced to next milestone`),
+        onError: () =>
+          toast.error(`Failed to advance ${contributor.userName}`),
       },
     );
   };

   const handleRemove = (contributor: ContributorProgress) => {
     removeContributor.mutate(
       { contributorId: contributor.userId },
       {
         onSuccess: () =>
           toast.success(`${contributor.userName} removed from bounty`),
+        onError: () =>
+          toast.error(`Failed to remove ${contributor.userName}`),
       },
     );
   };

Also applies to: 73-81, 83-91

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/model4-maintainer-dashboard.tsx` around lines 60 -
71, Add onError handlers to the mutation calls so failures surface to users; for
each mutate invocation (e.g., releasePayment.mutate in handleReleasePayment and
the other mutate calls referenced around the same area like
approveContribution.mutate and rejectContribution.mutate), pass an onError
callback that calls toast.error with a clear message that includes the error
message or a fallback (e.g., `toast.error(\`Failed to release payment:
${error?.message ?? 'unknown error'}\`)`). Ensure the onError is paired with the
existing onSuccess handlers and uses the mutation-specific context
(contributor.userName or relevant IDs) to make the toast informative.

Comment on lines +104 to 115
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
},
);
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Missing onError handler for sendMessage mutation.

Similar to other handlers, handleSendMessage should handle errors so users know if message delivery failed.

Add onError callback
   const handleSendMessage = () => {
     if (!selectedContributor || !messageText.trim()) return;
     sendMessage.mutate(
       { contributorId: selectedContributor.userId, message: messageText },
       {
         onSuccess: () => {
           toast.success(`Message sent to ${selectedContributor.userName}`);
           setIsMessageOpen(false);
         },
+        onError: () => {
+          toast.error(`Failed to send message to ${selectedContributor.userName}`);
+        },
       },
     );
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
},
);
};
const handleSendMessage = () => {
if (!selectedContributor || !messageText.trim()) return;
sendMessage.mutate(
{ contributorId: selectedContributor.userId, message: messageText },
{
onSuccess: () => {
toast.success(`Message sent to ${selectedContributor.userName}`);
setIsMessageOpen(false);
},
onError: () => {
toast.error(`Failed to send message to ${selectedContributor.userName}`);
},
},
);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/model4-maintainer-dashboard.tsx` around lines 104 -
115, handleSendMessage currently calls sendMessage.mutate without an onError
handler; update handleSendMessage to pass an onError callback to
sendMessage.mutate (alongside the existing onSuccess) that shows an error toast
(e.g., toast.error with a descriptive message and optional error.message) and
keeps the message UI open or resets state appropriately. Locate the
sendMessage.mutate call inside the handleSendMessage function and add the
onError option to mirror other mutation handlers in this file.

Comment on lines +268 to 276
{advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId ? (
<Loader2 className="size-3 mr-1.5 animate-spin" />
) : (
<>
Advance [Coming soon]{" "}
<ArrowRight className="size-3 ml-1.5" />
Advance <ArrowRight className="size-3 ml-1.5" />
</>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Advance button label missing during loading state.

When advanceContributor.isPending is true for this contributor, only the Loader2 spinner is rendered. The "Advance" text and arrow icon are lost, unlike the other buttons that keep their labels during loading.

Keep label visible during loading
                           >
-                            {advanceContributor.isPending &&
-                            advanceContributor.variables?.contributorId ===
-                              contributor.userId ? (
+                            {advanceContributor.isPending &&
+                            advanceContributor.variables?.contributorId ===
+                              contributor.userId && (
                               <Loader2 className="size-3 mr-1.5 animate-spin" />
-                            ) : (
-                              <>
-                                Advance <ArrowRight className="size-3 ml-1.5" />
-                              </>
                             )}
+                            Advance <ArrowRight className="size-3 ml-1.5" />
                           </Button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId ? (
<Loader2 className="size-3 mr-1.5 animate-spin" />
) : (
<>
Advance [Coming soon]{" "}
<ArrowRight className="size-3 ml-1.5" />
Advance <ArrowRight className="size-3 ml-1.5" />
</>
)}
{advanceContributor.isPending &&
advanceContributor.variables?.contributorId ===
contributor.userId && (
<Loader2 className="size-3 mr-1.5 animate-spin" />
)}
Advance <ArrowRight className="size-3 ml-1.5" />
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/model4-maintainer-dashboard.tsx` around lines 268 -
276, The Advance button currently renders only Loader2 when
advanceContributor.isPending for the contributor, removing the "Advance" text
and ArrowRight icon; update the JSX in the Advance button (the block using
advanceContributor, contributor.userId, Loader2 and ArrowRight) to always render
the "Advance" label and ArrowRight icon and conditionally render Loader2
alongside them when advanceContributor.isPending (e.g., show Loader2
before/after the label and keep the label+icon visible), preserving the existing
conditional check on advanceContributor.variables?.contributorId to target the
correct contributor.

Comment on lines +60 to +78
if (milestoneIndex >= 0 && milestoneIndex < milestones.length - 1) {
const nextMilestone = milestones[milestoneIndex + 1];
const newProgress = [...contributorProgress];
newProgress[contributorIndex] = {
...newProgress[contributorIndex],
currentMilestoneId: nextMilestone.id,
};

queryClient.setQueryData<ExtendedBountyQuery>(
bountyKeys.detail(bountyId),
{
...previous,
bounty: {
...previous.bounty,
contributorProgress: newProgress,
},
},
);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

No guard when contributor is already at the last milestone.

If a contributor is on the final milestone (milestoneIndex === milestones.length - 1), the condition at line 60 prevents the cache update, but the mutation still "succeeds" with a toast saying the contributor advanced. This creates a confusing UX where the user sees success but nothing changes.

Consider throwing or returning early in mutationFn
   const advanceContributor = useMutation({
-    mutationFn: async ({ contributorId }: { contributorId: string }) => {
+    mutationFn: async ({ contributorId }: { contributorId: string }) => {
+      // Check if already at last milestone before making the call
+      const current = queryClient.getQueryData<ExtendedBountyQuery>(
+        bountyKeys.detail(bountyId),
+      );
+      if (current?.bounty) {
+        const progress = current.bounty.contributorProgress || [];
+        const milestones = current.bounty.milestones || [];
+        const contributor = progress.find((c) => c.userId === contributorId);
+        if (contributor) {
+          const idx = milestones.findIndex((m) => m.id === contributor.currentMilestoneId);
+          if (idx >= milestones.length - 1) {
+            throw new Error("Contributor is already at the final milestone");
+          }
+        }
+      }
       await delay(1000);
       return { contributorId };
     },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` around lines 60 - 78, The cache update path
assumes there is a next milestone but doesn't handle the case when
milestoneIndex === milestones.length - 1, so the mutation reports success with
no state change; update the mutation to guard against already-at-final-milestone
by checking milestoneIndex against milestones.length - 1 (using the same
variables: milestoneIndex, milestones, contributorIndex, contributorProgress)
and either return early or throw an error from the mutationFn so the success
path (which calls queryClient.setQueryData on bountyKeys.detail(bountyId)) and
the success toast are not executed; ensure the guard is placed before the code
that constructs newProgress and calls queryClient.setQueryData to prevent
misleading UX.

Comment on lines +83 to +88
onError: (_err, _vars, context) => {
if (context?.previous) {
queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
}
},
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing onError callback for user feedback on mutation failures.

Both advanceContributor and removeContributor silently rollback on error without notifying the user. When optimistic updates fail, the UI reverts but the user has no indication that their action didn't succeed on the server.

Proposed fix to add error toasts
+import { toast } from "sonner";
+
 // In advanceContributor mutation:
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
+      toast.error("Failed to advance contributor. Please try again.");
     },

 // In removeContributor mutation:
     onError: (_err, _vars, context) => {
       if (context?.previous) {
         queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous);
       }
+      toast.error("Failed to remove contributor. Please try again.");
     },

Also applies to: 123-128

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` around lines 83 - 88, The optimistic
mutation handlers (advanceContributor and removeContributor) currently only
rollback via queryClient.setQueryData in their onError callbacks, leaving users
uninformed; update both mutation definitions to add an onError that (1) rolls
back as now using context.previous (keep
queryClient.setQueryData(bountyKeys.detail(bountyId), context.previous)), and
(2) triggers a user-facing error toast/message with the error details (use the
mutation onError params to extract the error) so the UI both reverts and shows a
clear failure message to the user; reference the existing onError blocks for
these mutations and reuse bountyId, bountyKeys.detail, and the context.previous
logic.

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 30, 2026

@Jopsan-gm Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@Jopsan-gm
Copy link
Copy Markdown
Contributor Author

Hello @Benjtalkshow PR ready! ;)

@Benjtalkshow
Copy link
Copy Markdown
Contributor

Hello @Benjtalkshow PR ready! ;)

A few things to fix before this can merge.

This PR duplicates PR #263's work for issue #211. The BountyApplication interface and the new fields you added to Bounty in types/bounty.ts belong to #211, not #205. Please remove them.

Three actions don't fully meet the acceptance criteria. Release Payment only invalidates without an escrow update. Remove filters contributorProgress but doesn't decrement totalSlotsOccupied (the code comment admits this). Send Message just await delay(1000) and returns without recording anything against the bounty. Please address each.

Two code quality items: useBountyApplication(bountyId) returns four mutations as one object, but the rest of hooks/use-bounty-application.ts exports one mutation per hook. Split into useReleasePayment, useAdvanceContributor, useRemoveContributor, useSendMessage. Also move type ExtendedBountyQuery and const delay from mid-file (lines 327, 331) to the top.

Last thing: there's significant Prettier reformatting noise across 10+ unrelated files (mode-toggle.tsx, dispute page, saved page, both bounty cards, document-upload, submission-draft test, use-bounty-cta-state.ts, generated.ts, types/participation.ts). Revert those to upstream so the diff only shows the actual #205 work.

@Benjtalkshow
Copy link
Copy Markdown
Contributor

Hi @Jopsan-gm
Most of the previous feedback is addressed. Hook split, top-of-file types, real escrow update on Release Payment, slot decrement on Remove, message recording on Send Message, and the Prettier noise is gone.

One last thing: types/bounty.ts still has the 23-line addition for BountyApplication and the new applications / claimCount / maxParticipants / assignedContributorId fields. That's PR #263's work for issue #211 and will collide with it. Please revert types/bounty.ts to upstream. Your dashboard code doesn't depend on those fields directly, so the revert should be clean.

Once that's reverted, this is ready.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 62-63: Add persist-credentials: false to both checkout steps that
use actions/checkout@v4 to avoid leaking the default GITHUB_TOKEN to later
steps; locate the steps that include "uses: actions/checkout@v4" and add the
key-value pair persist-credentials: false under each step so credentials are not
persisted for subsequent workflow actions.
- Line 63: The workflow currently uses floating major tags and leaves checkout
credentials persisted; update the specified actions to pinned commit SHAs and
explicitly disable persisted credentials for checkout: replace uses:
actions/checkout@v4 with a pinned commit SHA and add with: persist-credentials:
false to the checkout step, and similarly pin actions/setup-node@v4,
pnpm/action-setup@v4, actions/cache@v4, and actions/upload-artifact@v4 to their
corresponding commit SHAs (use the official repository commit SHAs for each
action) so the CI references immutable versions and credentials are not stored
in git config.

In `@hooks/use-bounty-application.ts`:
- Line 542: Remove the debug console.log that prints raw user message in the
useSendMessage code path: delete or replace the line
console.log(`[useSendMessage] Recorded message for bountyId ${bountyId}:
contributorId=${contributorId}, message="${message}"`); so user-authored message
text is not logged; if telemetry is needed, log only non-sensitive identifiers
(e.g., bountyId and contributorId) or rely on the existing recordedMessages
storage instead—refer to useSendMessage, recordedMessages, bountyId,
contributorId, and message to locate the statement.
- Around line 355-357: The code computes milestonesCount using
"previous?.bounty?.milestones?.length ?? 1" which does not guard against an
empty array (length 0) and can cause division by zero for amountToRelease;
update both occurrences (the calculation of milestonesCount and the identical
logic in onMutate) to treat a zero length as fallback to 1—e.g., replace the
nullish fallback with a truthy fallback or use Math.max to ensure
milestonesCount is at least 1 (referencing totalAmount, milestonesCount,
amountToRelease and the onMutate handler to locate both places).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4697e821-3068-4aa7-b453-5bb849a111f2

📥 Commits

Reviewing files that changed from the base of the PR and between 5e53aa8 and 601a0c0.

📒 Files selected for processing (5)
  • .github/workflows/ci.yml
  • components/bounty-detail/bounty-detail-client.tsx
  • components/bounty-detail/model4-maintainer-dashboard.tsx
  • hooks/use-bounty-application.ts
  • lib/services/escrow.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/bounty-detail/bounty-detail-client.tsx

Comment thread .github/workflows/ci.yml
Comment on lines 62 to 63
- name: Checkout repository
uses: actions/checkout@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and print relevant section of the workflow
FILE=".github/workflows/ci.yml"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE" >&2
  exit 1
fi

# Show around the referenced lines
nl -ba "$FILE" | sed -n '45,95p'

Repository: boundlessfi/bounties

Length of output: 108


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
test -f "$FILE"

echo "== Preview lines 55-80 =="
cat -n "$FILE" | sed -n '55,80p'

echo
echo "== Find all actions/checkout steps =="
rg -n "uses:\s*actions/checkout@|actions/checkout@" "$FILE" || true

echo
echo "== Find persist-credentials settings =="
rg -n "persist-credentials" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 1026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"

echo "== Preview lines 1-40 =="
cat -n "$FILE" | sed -n '1,40p'

echo
echo "== Find all persist-credentials settings =="
rg -n "persist-credentials" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 1286


Disable persisted checkout credentials in CI (persist-credentials: false).

Both actions/checkout@v4 steps (lines 20 and 63) lack persist-credentials: false, which leaves the default token configured for later steps.

🛡️ Suggested fix
       - name: Checkout repository
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Checkout repository
uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
with:
persist-credentials: false
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 62-63: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 63-63: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 62 - 63, Add persist-credentials:
false to both checkout steps that use actions/checkout@v4 to avoid leaking the
default GITHUB_TOKEN to later steps; locate the steps that include "uses:
actions/checkout@v4" and add the key-value pair persist-credentials: false under
each step so credentials are not persisted for subsequent workflow actions.

Comment thread .github/workflows/ci.yml

steps:
- name: Checkout repository
uses: actions/checkout@v4
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Relevant uses lines (with numbers) =="
nl -ba "$FILE" | sed -n '40,140p'

echo
echo "== rg for uses: actions/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Relevant uses lines (with numbers) =="
nl -ba "$FILE" | sed -n '40,140p'

echo
echo "== rg for uses: actions/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Relevant uses lines (with numbers) =="
nl -ba "$FILE" | sed -n '40,140p'

echo
echo "== rg for uses: actions/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
nl -ba "$FILE" | sed -n '1,200p'

Repository: boundlessfi/bounties

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE=".github/workflows/ci.yml"
echo "== File: $FILE =="
ls -l "$FILE"
echo

echo "== Line-numbered excerpt (40-140) =="
awk '{printf "%d\t%s\n", NR, $0}' "$FILE" | sed -n '40,140p'

echo
echo "== rg uses: actions/ or pnpm/ =="
rg -n "uses:\s*actions/|uses:\s*pnpm/" "$FILE" || true

echo
echo "== rg for persist-credentials in checkout steps =="
rg -n "persist-credentials" "$FILE" || true

Repository: boundlessfi/bounties

Length of output: 3938


🌐 Web query:

GitHub Actions actions/checkout persist-credentials default value true and description

💡 Result:

For actions/checkout, the input persist-credentials has default value true, and it controls whether the auth token / SSH key is configured in the local git config. Default value: true [1] Description: “Whether to configure the token or SSH key with the local git config” [1] Additional context from the action README: the auth token is persisted in the local git config so scripts can run authenticated git commands, and the token is removed during post-job cleanup; you can opt out with persist-credentials: false. [2]

Citations:


🌐 Web query:

actions/checkout persist-credentials defaults to true

💡 Result:

For actions/checkout, the persist-credentials input defaults to true (i.e., the token is persisted locally) unless you explicitly set persist-credentials: false.[1][2] The action documentation states that “The auth token is persisted in the local git config… Set persist-credentials: false to opt-out.”[1] Security discussions in the actions/checkout issue tracker also describe the parameter’s default as true and that you must explicitly pass persist-credentials: false to prevent persistence.[2]

Citations:


🌐 Web query:

"persist-credentials" actions/checkout defaults to true

💡 Result:

Yes, the persist-credentials input for the actions/checkout GitHub Action defaults to true [1][2][3]. When this setting is enabled (the default), the action configures the local Git environment with the authentication token (such as the GITHUB_TOKEN) used to fetch the repository [1][2][3]. This allows subsequent steps in the same job to execute authenticated Git commands—such as git push or git pull—without requiring further manual configuration [1][3]. The token is removed from the local Git configuration during the post-job cleanup phase [1][2][3]. Security practitioners often recommend setting persist-credentials to false in workflows that do not require Git write access (e.g., build or test jobs) to adhere to the principle of least privilege [4][5][6]. This prevents the authentication token from being available to other steps in the job, which can reduce the impact if a subsequent step is compromised [4][6]. To disable this behavior, use the following syntax in your workflow file [5]: - uses: actions/checkout@v4 with: persist-credentials: false

Citations:


🌐 Web query:

actions/checkout@v4 persist-credentials documentation default

💡 Result:

In the actions/checkout@v4 action, the default value for the persist-credentials input is true [1][2][3]. When this setting is enabled (the default), the action configures the repository's local git configuration to include the authentication token or SSH key used for the checkout [1][4]. This allows subsequent steps in your workflow to run authenticated git commands (such as git push) without needing to manually re-configure credentials [1][5]. The token is automatically removed during the post-job cleanup phase [1][2][4]. If you wish to prevent this behavior—for example, to enhance security by ensuring the credentials are not available to subsequent steps in the workflow—you must explicitly set persist-credentials to false in your workflow YAML [1][5]: steps: - uses: actions/checkout@v4 with: persist-credentials: false

Citations:


Pin GitHub Actions to commit SHAs and set checkout token persistence explicitly.

In .github/workflows/ci.yml, the following steps still use major tags instead of commit SHAs: actions/checkout@v4 (line 63), actions/setup-node@v4 (line 66), pnpm/action-setup@v4 (line 71), actions/cache@v4 (line 76), and actions/upload-artifact@v4 (line 108). This weakens supply-chain guarantees.

Also, actions/checkout@v4 (line 63) does not set with: persist-credentials: false; persist-credentials defaults to true, so credentials are persisted locally in git config unless opted out.

🧰 Tools
🪛 zizmor (1.25.2)

[warning] 62-63: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 63-63: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 63, The workflow currently uses floating
major tags and leaves checkout credentials persisted; update the specified
actions to pinned commit SHAs and explicitly disable persisted credentials for
checkout: replace uses: actions/checkout@v4 with a pinned commit SHA and add
with: persist-credentials: false to the checkout step, and similarly pin
actions/setup-node@v4, pnpm/action-setup@v4, actions/cache@v4, and
actions/upload-artifact@v4 to their corresponding commit SHAs (use the official
repository commit SHAs for each action) so the CI references immutable versions
and credentials are not stored in git config.

Comment on lines +355 to +357
const totalAmount = previous?.bounty?.rewardAmount ?? 100;
const milestonesCount = previous?.bounty?.milestones?.length ?? 1;
const amountToRelease = totalAmount / milestonesCount;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Empty milestones array causes division by zero.

?? 1 only guards null/undefined, not an empty array. If milestones is [], length is 0 and 0 ?? 1 evaluates to 0, so amountToRelease becomes Infinity. The same pattern repeats in onMutate (Line 373). Use || to also cover the empty/zero case.

🐛 Proposed fix (apply to both Line 356 and Line 373)
-      const milestonesCount = previous?.bounty?.milestones?.length ?? 1;
+      const milestonesCount = previous?.bounty?.milestones?.length || 1;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const totalAmount = previous?.bounty?.rewardAmount ?? 100;
const milestonesCount = previous?.bounty?.milestones?.length ?? 1;
const amountToRelease = totalAmount / milestonesCount;
const totalAmount = previous?.bounty?.rewardAmount ?? 100;
const milestonesCount = previous?.bounty?.milestones?.length || 1;
const amountToRelease = totalAmount / milestonesCount;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` around lines 355 - 357, The code computes
milestonesCount using "previous?.bounty?.milestones?.length ?? 1" which does not
guard against an empty array (length 0) and can cause division by zero for
amountToRelease; update both occurrences (the calculation of milestonesCount and
the identical logic in onMutate) to treat a zero length as fallback to 1—e.g.,
replace the nullish fallback with a truthy fallback or use Math.max to ensure
milestonesCount is at least 1 (referencing totalAmount, milestonesCount,
amountToRelease and the onMutate handler to locate both places).

timestamp: new Date().toISOString(),
});

console.log(`[useSendMessage] Recorded message for bountyId ${bountyId}: contributorId=${contributorId}, message="${message}"`);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop the console.log of raw message content.

This logs user-authored message text to the console, which is a debug artifact and a potential privacy concern. The message is already recorded in recordedMessages.

🧹 Proposed fix
-      console.log(`[useSendMessage] Recorded message for bountyId ${bountyId}: contributorId=${contributorId}, message="${message}"`);
       return { contributorId, message };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.log(`[useSendMessage] Recorded message for bountyId ${bountyId}: contributorId=${contributorId}, message="${message}"`);
return { contributorId, message };
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/use-bounty-application.ts` at line 542, Remove the debug console.log
that prints raw user message in the useSendMessage code path: delete or replace
the line console.log(`[useSendMessage] Recorded message for bountyId
${bountyId}: contributorId=${contributorId}, message="${message}"`); so
user-authored message text is not logged; if telemetry is needed, log only
non-sensitive identifiers (e.g., bountyId and contributorId) or rely on the
existing recordedMessages storage instead—refer to useSendMessage,
recordedMessages, bountyId, contributorId, and message to locate the statement.

@Jopsan-gm
Copy link
Copy Markdown
Contributor Author

Hey @Benjtalkshow, thanks for the review!

I just pushed the latest updates. types/bounty.ts has been completely reverted to match upstream, and I also went ahead and resolved the recent merge conflicts with the main branch.

Everything should be ready to merge now. Let me know if there's anything else!

@Benjtalkshow Benjtalkshow merged commit d02b2d5 into boundlessfi:main May 31, 2026
3 of 4 checks passed
Benjtalkshow added a commit that referenced this pull request May 31, 2026
PR #265 dropped --with-deps from the Playwright install step. Without it,
fresh CI runners that don't have Chromium's system libs (fonts, shared
libraries) cached will fail to launch the browser. Putting it back.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement the maintainer dashboard actions for Multi-Winner Milestone bounties

2 participants