feat: implement SenderDashboard with stream analytics and activity feed#575
Conversation
- Add SenderDashboard component with stats cards for total streams, amount, active, completed - Implement bar chart visualization for streams by status (Scheduled, Active, Paused, Completed, Canceled) - Add recent activity feed showing last 10 events aggregated from sender's streams - Implement quick action buttons: Create Stream and Bulk Cancel - Add getSenderEvents() API helper function for aggregating events across streams - Include comprehensive Vitest test suite with 14 tests covering all features - Add MSW mock handlers for API endpoints with realistic test data fixtures Closes ritik4ever#392
|
@LawalRahman is attempting to deploy a commit to the ritik4ever's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@LawalRahman 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! 🚀 |
📝 WalkthroughWalkthrough
ChangesSenderDashboard Analytics, Activity Feed & Bulk Cancel
Sequence Diagram(s)sequenceDiagram
participant User
participant SenderDashboard
participant getSenderEvents
participant listStreams
participant getStreamHistory
participant cancelStream
rect rgba(70, 130, 180, 0.5)
Note over SenderDashboard: Mount / 5s poll
SenderDashboard->>listStreams: listStreams({ sender })
listStreams-->>SenderDashboard: streams[] + stats/chartData via useMemo
SenderDashboard->>getSenderEvents: getSenderEvents(senderAddress, 10)
getSenderEvents->>getStreamHistory: getStreamHistory(id) × N parallel
getStreamHistory-->>getSenderEvents: StreamEvent[]
getSenderEvents-->>SenderDashboard: sorted, sliced events[]
end
rect rgba(60, 179, 113, 0.5)
Note over User,SenderDashboard: Bulk Cancel flow
User->>SenderDashboard: check row checkboxes / select-all
SenderDashboard->>SenderDashboard: update selectedStreams Set
User->>SenderDashboard: click "Bulk Cancel"
SenderDashboard->>cancelStream: cancelStream(id) × M parallel
cancelStream-->>SenderDashboard: results
SenderDashboard->>listStreams: refresh streams
SenderDashboard->>getSenderEvents: refresh events
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
frontend/src/components/SenderDashboard.tsx (1)
170-182: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPrevent overlapping poll cycles.
setIntervalstarts a new async refresh every 5 seconds even if the previouslistStreams/getSenderEventscycle is still running. Slow requests can stack up and let older responses overwrite newer state.Proposed guard
+ let polling = false; const interval = setInterval(async () => { + if (polling) return; + polling = true; try { const result = await listStreams({ sender: senderAddress }); if (active) { setStreams(result.data); // Also refresh events const recentEvents = await getSenderEvents(senderAddress); if (active) { setEvents(recentEvents); } } } catch { // Silent fail on polling + } finally { + polling = false; } }, 5000);🤖 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 `@frontend/src/components/SenderDashboard.tsx` around lines 170 - 182, The polling logic in SenderDashboard is using setInterval with async work, which can overlap refresh cycles and allow stale responses to update state. Update the polling effect around the listStreams and getSenderEvents calls to use a re-entrancy guard or single-flight flag so a new cycle is skipped while the previous one is still running. Keep the existing active check, but ensure only one refresh runs at a time and that the interval cleanup still clears properly.
🧹 Nitpick comments (1)
frontend/src/components/SenderDashboard.test.tsx (1)
53-59: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueMinor DRY:
mockActiveStreamis invoked twice per call.Each helper calls
mockActiveStream(id, sender)for the spread and again for...progress, building two throwaway objects. Capture once for clarity.♻️ Optional
-const mockPausedStream = (id: string, sender: string): Stream => ({ - ...mockActiveStream(id, sender), - progress: { - ...mockActiveStream(id, sender).progress, - status: "paused", - }, -}); +const mockPausedStream = (id: string, sender: string): Stream => { + const base = mockActiveStream(id, sender); + return { ...base, progress: { ...base.progress, status: "paused" } }; +};Also applies to: 73-79
🤖 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 `@frontend/src/components/SenderDashboard.test.tsx` around lines 53 - 59, The mockPausedStream helper is calling mockActiveStream(id, sender) twice, creating unnecessary duplicate objects. Update mockPausedStream (and the similar helper referenced in the review) to call mockActiveStream once, store the result in a local variable, and reuse it for both the spread and progress override so the test data is clearer and avoids repeated work.
🤖 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 `@ACCEPTANCE_CRITERIA_VERIFICATION.md`:
- Around line 24-33: The markdown fences in ACCEPTANCE_CRITERIA_VERIFICATION
should use explicit language identifiers to satisfy MD040. Update each fenced
block in this document, including the ones around the table output and the other
referenced sections, to use a consistent language tag such as text. Focus on the
fenced code blocks shown in the diff and any matching blocks elsewhere in the
file so they all follow the same markdown lint-friendly format.
- Around line 78-101: The recent activity feed acceptance note is inconsistent
with the implemented data source, since getSenderEvents and SenderDashboard
aggregate stream history instead of using the stated /api/events?sender=
contract. Update the acceptance criteria text to either match the actual
implementation path or add an explicit signed-off deviation, and make sure the
status reflects the real contract used by frontend/src/services/api.ts and
frontend/src/components/SenderDashboard.tsx.
In `@frontend/src/components/SenderDashboard.test.tsx`:
- Around line 181-185: The SenderDashboard test is asserting the wrong formatted
total for stats.totalAmount. Update the amount expectation in
SenderDashboard.test.tsx to match the fixture sum rendered by the component’s
toLocaleString("en-US", { maximumFractionDigits: 2 }) formatting, and change the
assertion near the Total Amount Streamed card to expect 3,500 instead of 3000.
- Around line 229-259: The chart test in SenderDashboard.test.tsx is relying on
jsdom/Recharts layout rendering and text labels, which is brittle without the
same mocking used elsewhere. Update the "renders bar chart showing streams by
status" test to avoid asserting axis labels from the rendered chart, and instead
either mock the chart/recharts components locally or verify the underlying
status data passed into SenderDashboard/Streams by Status behavior using
existing helpers like setupSenderHandler and mock*Stream fixtures.
In `@frontend/src/components/SenderDashboard.tsx`:
- Around line 222-225: The `completedStreams` metric in `SenderDashboard` is
incorrectly counting canceled streams as completed, which inflates the completed
stat. Update the filtering logic used for `completedStreams` so it only includes
streams whose `progress.status` is exactly `"completed"` and excludes
`"canceled"`; check the related stats/card rendering and any other references in
`SenderDashboard` to keep the completed and canceled counts split correctly.
- Around line 275-284: The cancel handlers in SenderDashboard are treating
refresh failures as cancel failures because the mutation and post-success state
reloads are inside the same try; move the listStreams/getSenderEvents refresh
logic out of the mutation error path so a successful cancel still updates state
even if refresh fails. For the bulk flow in the same component, update the
Promise.all-based cancellation in the bulk handler to tolerate partial success,
then refresh streams/events and clear any selected IDs based on the completed
cancels rather than aborting on the first rejection.
- Around line 233-239: The chart color lookup in SenderDashboard’s chart data
rendering is using filtered indexes and capitalized labels, which causes
mismatched or gray bars. Update the chart data objects to carry the actual
lowercase status key alongside the display name in the chartData builder, then
have the <Cell> color lookup use that status key when calling statusColor
instead of relying on array position or the label text. Apply the same fix
anywhere else the same chart color mapping pattern is used, including the
repeated chart rendering block referenced in the comment.
- Around line 493-526: Quick Actions is gated on stats.activeStreams, but Bulk
Cancel should be driven by the same cancellable rows the table can actually
select. Update SenderDashboard to use a single cancellable stream list for both
the quick-actions section and selection state, so scheduled rows can still
surface Bulk Cancel when selected. Review the Quick Actions block and the row
selection logic around selectedStreamForBulkCancel, handleBulkCancel, and the
stream table rendering to ensure paused/scheduled streams are handled
consistently.
- Around line 94-97: The claimed activity label in SenderDashboard’s label
formatting can render undefined values because it always interpolates amount and
assetCode even when they are optional. Update the logic in the label switch for
the claimed case to conditionally include these fields only when present,
matching the pattern already used in the created case, so the activity text
never shows undefined.
In `@frontend/src/services/api.ts`:
- Around line 345-370: The getSenderEvents helper is using listStreams plus
per-stream getStreamHistory fan-out instead of the sender events endpoint
required by the review. Update getSenderEvents to fetch directly from
/api/events with the sender query parameter, keep the existing limit handling
for the last 10 items, and remove the N+1 stream/history aggregation logic so
the function returns the sender’s activity from the single endpoint.
In `@SENDER_DASHBOARD_IMPLEMENTATION.md`:
- Line 95: The test-count documentation is inconsistent across the added docs,
so align the verified number everywhere it appears. Update the count in
SENDER_DASHBOARD_IMPLEMENTATION.md and the other referenced sections to match
the actual PR artifact count, using the same wording consistently so the
summary, implementation notes, and test coverage references all agree.
In `@SENDER_DASHBOARD_STRUCTURE.md`:
- Around line 5-83: The fenced diagram/tree/flow blocks in this markdown file
are missing language identifiers, which triggers MD040. Update each affected
fenced block in SENDER_DASHBOARD_STRUCTURE.md to use a text/diagram-style
language tag consistently, and verify the blocks around the SenderDashboard
layout, the tree view, and the flow steps all have explicit fence languages
while keeping the content unchanged.
---
Outside diff comments:
In `@frontend/src/components/SenderDashboard.tsx`:
- Around line 170-182: The polling logic in SenderDashboard is using setInterval
with async work, which can overlap refresh cycles and allow stale responses to
update state. Update the polling effect around the listStreams and
getSenderEvents calls to use a re-entrancy guard or single-flight flag so a new
cycle is skipped while the previous one is still running. Keep the existing
active check, but ensure only one refresh runs at a time and that the interval
cleanup still clears properly.
---
Nitpick comments:
In `@frontend/src/components/SenderDashboard.test.tsx`:
- Around line 53-59: The mockPausedStream helper is calling mockActiveStream(id,
sender) twice, creating unnecessary duplicate objects. Update mockPausedStream
(and the similar helper referenced in the review) to call mockActiveStream once,
store the result in a local variable, and reuse it for both the spread and
progress override so the test data is clearer and avoids repeated work.
🪄 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: defaults
Review profile: CHILL
Plan: Pro
Run ID: 4fc5723b-643a-4b36-bd9e-8b997b0c3256
📒 Files selected for processing (6)
ACCEPTANCE_CRITERIA_VERIFICATION.mdSENDER_DASHBOARD_IMPLEMENTATION.mdSENDER_DASHBOARD_STRUCTURE.mdfrontend/src/components/SenderDashboard.test.tsxfrontend/src/components/SenderDashboard.tsxfrontend/src/services/api.ts
| ``` | ||
| ┌──────────────────────────┬──────────────────────────┐ | ||
| │ Total Streams Created │ Total Amount Streamed │ | ||
| │ 4 │ 3,000 │ | ||
| └──────────────────────────┴──────────────────────────┘ | ||
| ┌──────────────────────────┬──────────────────────────┐ | ||
| │ Active Streams │ Completed/Canceled │ | ||
| │ 2 │ 2 │ | ||
| └──────────────────────────┴──────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add language identifiers to fenced code blocks (markdownlint MD040).
Use explicit fence languages for these blocks to keep lint clean and improve rendering consistency.
Suggested patch
-```
+```text
┌──────────────────────────┬──────────────────────────┐
...
-```
+```
-```
+```text
Streams by Status
...
-```
+```
-```
+```text
Recent Activity
...
-```
+```
-```
+```text
Header:
...
-```
+```Also applies to: 65-74, 119-130, 180-190
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 24-24: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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 `@ACCEPTANCE_CRITERIA_VERIFICATION.md` around lines 24 - 33, The markdown
fences in ACCEPTANCE_CRITERIA_VERIFICATION should use explicit language
identifiers to satisfy MD040. Update each fenced block in this document,
including the ones around the table output and the other referenced sections, to
use a consistent language tag such as text. Focus on the fenced code blocks
shown in the diff and any matching blocks elsewhere in the file so they all
follow the same markdown lint-friendly format.
Source: Linters/SAST tools
| ### 3. ✅ Recent activity feed: last 10 events from `/api/events?sender=` | ||
|
|
||
| **Implemented in:** | ||
| - API Helper: `frontend/src/services/api.ts` (lines 336-370) | ||
| - Component: `frontend/src/components/SenderDashboard.tsx` (lines 354-394) | ||
|
|
||
| **API Function: `getSenderEvents(senderAddress, limit=10)`** | ||
| ```typescript | ||
| export async function getSenderEvents( | ||
| senderAddress: string, | ||
| limit: number = 10 | ||
| ): Promise<StreamEvent[]> { | ||
| // 1. Fetch all streams for sender | ||
| // 2. Get event history for each stream | ||
| // 3. Aggregate all events | ||
| // 4. Sort by timestamp (newest first) | ||
| // 5. Return limited set | ||
| } | ||
| ``` | ||
|
|
||
| **Features:** | ||
| - Fetches from actual `/api/streams/:streamId/history` endpoints | ||
| - Aggregates events from all sender's streams | ||
| - Displays last 10 most recent events (sorted by timestamp descending) |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Acceptance criterion for activity endpoint is not actually met as written.
This section states the feed is from /api/events?sender=, but the implemented path (via getSenderEvents) aggregates /api/streams/:streamId/history calls instead. Please either implement the required endpoint contract or explicitly document a signed-off deviation in acceptance status.
🤖 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 `@ACCEPTANCE_CRITERIA_VERIFICATION.md` around lines 78 - 101, The recent
activity feed acceptance note is inconsistent with the implemented data source,
since getSenderEvents and SenderDashboard aggregate stream history instead of
using the stated /api/events?sender= contract. Update the acceptance criteria
text to either match the actual implementation path or add an explicit
signed-off deviation, and make sure the status reflects the real contract used
by frontend/src/services/api.ts and frontend/src/components/SenderDashboard.tsx.
| expect(screen.getByText("Total Amount Streamed")).toBeInTheDocument(); | ||
| const amountCard = screen | ||
| .getByText("Total Amount Streamed") | ||
| .closest("article"); | ||
| expect(amountCard?.querySelector("strong")?.textContent).toContain("3000"); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Confirm the completed-stream fixture amount and that the rendered total is comma-formatted.
fd -t f 'SenderDashboard.test.tsx' --exec sed -n '60,80p' {}
# Inspect the amount-rendering line in the component
fd -t f 'SenderDashboard.tsx' frontend/src/components --exec rg -n 'toLocaleString' {}Repository: ritik4ever/stellar-stream
Length of output: 848
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the fixture definitions and the stats assertion block.
sed -n '1,220p' frontend/src/components/SenderDashboard.test.tsx
# Show the stats rendering/aggregation area in the component.
sed -n '380,470p' frontend/src/components/SenderDashboard.tsxRepository: ritik4ever/stellar-stream
Length of output: 9510
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show the fixture definitions and the stats assertion block.
sed -n '1,220p' frontend/src/components/SenderDashboard.test.tsx
# Show the stats rendering/aggregation area in the component.
sed -n '380,470p' frontend/src/components/SenderDashboard.tsxRepository: ritik4ever/stellar-stream
Length of output: 9510
Update the amount assertion to match the fixture total
stats.totalAmount is rendered with toLocaleString("en-US", { maximumFractionDigits: 2 }), and these fixtures add up to 3,500 (1000 + 1000 + 500 + 1000). toContain("3000") will not match; assert against "3,500" instead.
🤖 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 `@frontend/src/components/SenderDashboard.test.tsx` around lines 181 - 185, The
SenderDashboard test is asserting the wrong formatted total for
stats.totalAmount. Update the amount expectation in SenderDashboard.test.tsx to
match the fixture sum rendered by the component’s toLocaleString("en-US", {
maximumFractionDigits: 2 }) formatting, and change the assertion near the Total
Amount Streamed card to expect 3,500 instead of 3000.
| it("renders bar chart showing streams by status", async () => { | ||
| const SENDER_CHART = "GSENDER_CHART"; | ||
| const streams = [ | ||
| mockActiveStream("s1", SENDER_CHART), | ||
| mockActiveStream("s2", SENDER_CHART), | ||
| mockScheduledStream("s3", SENDER_CHART), | ||
| mockPausedStream("s4", SENDER_CHART), | ||
| mockCompletedStream("s5", SENDER_CHART), | ||
| mockCanceledStream("s6", SENDER_CHART), | ||
| ]; | ||
| setupSenderHandler(streams, SENDER_CHART); | ||
| streams.forEach((s) => setupStreamHistoryHandler(s.id, [])); | ||
|
|
||
| render( | ||
| <SenderDashboard | ||
| senderAddress={SENDER_CHART} | ||
| onEditStartTime={onEditStartTime} | ||
| /> | ||
| ); | ||
|
|
||
| await waitFor(() => | ||
| expect(screen.getByText("Streams by Status")).toBeInTheDocument() | ||
| ); | ||
|
|
||
| // Verify chart is rendered (look for axis labels) | ||
| expect(screen.getByText("Scheduled")).toBeInTheDocument(); | ||
| expect(screen.getByText("Active")).toBeInTheDocument(); | ||
| expect(screen.getByText("Paused")).toBeInTheDocument(); | ||
| expect(screen.getByText("Completed")).toBeInTheDocument(); | ||
| expect(screen.getByText("Canceled")).toBeInTheDocument(); | ||
| }); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Look for ResizeObserver / dimension shims in test setup.
fd -t f -e ts -e tsx 'setup' frontend --exec rg -n -i 'resizeobserver|getBoundingClientRect|ResponsiveContainer' {}
rg -n -i 'resizeobserver' frontend/src 2>/dev/nullRepository: ritik4ever/stellar-stream
Length of output: 163
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Relevant test/setup files ==\n'
git ls-files frontend | rg '(^|/)(setup|test|tests|vitest|jest|jest\.setup|setupTests|config|SenderDashboard)\.(ts|tsx|js|jsx|mjs|cjs)$|(^|/)test'
printf '\n== Search for jsdom/layout shims ==\n'
rg -n -i 'ResizeObserver|getBoundingClientRect|matchMedia|offsetWidth|offsetHeight|clientWidth|clientHeight|ResponsiveContainer' frontend --glob '!**/node_modules/**'
printf '\n== Search for chart-related test helpers ==\n'
rg -n -i 'ResponsiveContainer|recharts|chart' frontend/src frontend/test frontend --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 50382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Relevant test/setup files ==\n'
git ls-files frontend | rg '(^|/)(setup|test|tests|vitest|jest|jest\.setup|setupTests|config|SenderDashboard)\.(ts|tsx|js|jsx|mjs|cjs)$|(^|/)test'
printf '\n== Search for jsdom/layout shims ==\n'
rg -n -i 'ResizeObserver|getBoundingClientRect|matchMedia|offsetWidth|offsetHeight|clientWidth|clientHeight|ResponsiveContainer' frontend --glob '!**/node_modules/**'
printf '\n== Search for chart-related test helpers ==\n'
rg -n -i 'ResponsiveContainer|recharts|chart' frontend/src frontend/test frontend --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 50382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '\n== Relevant test/setup files ==\n'
git ls-files frontend | rg '(^|/)(setup|test|tests|vitest|jest|jest\.setup|setupTests|config|SenderDashboard)\.(ts|tsx|js|jsx|mjs|cjs)$|(^|/)test'
printf '\n== Search for jsdom/layout shims ==\n'
rg -n -i 'ResizeObserver|getBoundingClientRect|matchMedia|offsetWidth|offsetHeight|clientWidth|clientHeight|ResponsiveContainer' frontend --glob '!**/node_modules/**'
printf '\n== Search for chart-related test helpers ==\n'
rg -n -i 'ResponsiveContainer|recharts|chart' frontend/src frontend/test frontend --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 50382
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== frontend/src/setupTests.ts =='
wc -l frontend/src/setupTests.ts
cat -n frontend/src/setupTests.ts | sed -n '1,220p'
echo '== frontend/src/setupTests.js =='
wc -l frontend/src/setupTests.js
cat -n frontend/src/setupTests.js | sed -n '1,220p'
echo '== frontend/src/components/SenderDashboard.tsx (chart area) =='
cat -n frontend/src/components/SenderDashboard.tsx | sed -n '430,520p'
echo '== frontend/src/components/StreamMetricsChart.test.tsx =='
cat -n frontend/src/components/StreamMetricsChart.test.tsx | sed -n '1,120p'Repository: ritik4ever/stellar-stream
Length of output: 7437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== frontend/src/setupTests.ts =='
wc -l frontend/src/setupTests.ts
cat -n frontend/src/setupTests.ts | sed -n '1,220p'
echo '== frontend/src/setupTests.js =='
wc -l frontend/src/setupTests.js
cat -n frontend/src/setupTests.js | sed -n '1,220p'
echo '== frontend/src/components/SenderDashboard.tsx (chart area) =='
cat -n frontend/src/components/SenderDashboard.tsx | sed -n '430,520p'
echo '== frontend/src/components/StreamMetricsChart.test.tsx =='
cat -n frontend/src/components/StreamMetricsChart.test.tsx | sed -n '1,120p'Repository: ritik4ever/stellar-stream
Length of output: 7437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== frontend/src/setupTests.ts =='
wc -l frontend/src/setupTests.ts
cat -n frontend/src/setupTests.ts | sed -n '1,220p'
echo '== frontend/src/setupTests.js =='
wc -l frontend/src/setupTests.js
cat -n frontend/src/setupTests.js | sed -n '1,220p'
echo '== frontend/src/components/SenderDashboard.tsx (chart area) =='
cat -n frontend/src/components/SenderDashboard.tsx | sed -n '430,520p'
echo '== frontend/src/components/StreamMetricsChart.test.tsx =='
cat -n frontend/src/components/StreamMetricsChart.test.tsx | sed -n '1,120p'Repository: ritik4ever/stellar-stream
Length of output: 7437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== frontend/src/setupTests.ts =='
wc -l frontend/src/setupTests.ts
cat -n frontend/src/setupTests.ts | sed -n '1,220p'
echo '== frontend/src/setupTests.js =='
wc -l frontend/src/setupTests.js
cat -n frontend/src/setupTests.js | sed -n '1,220p'
echo '== frontend/src/components/SenderDashboard.tsx (chart area) =='
cat -n frontend/src/components/SenderDashboard.tsx | sed -n '430,520p'
echo '== frontend/src/components/StreamMetricsChart.test.tsx =='
cat -n frontend/src/components/StreamMetricsChart.test.tsx | sed -n '1,120p'Repository: ritik4ever/stellar-stream
Length of output: 7437
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== SenderDashboard.test.tsx around chart test =='
cat -n frontend/src/components/SenderDashboard.test.tsx | sed -n '1,320p'
echo '== Search for recharts mocks in SenderDashboard tests/setup =='
rg -n 'vi\.mock\("recharts"|mock\("recharts"|ResponsiveContainer|XAxis|YAxis|BarChart' frontend/src/components/SenderDashboard.test.tsx frontend/src/setupTests.ts frontend/src/setupTests.js frontend/src --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 13925
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== SenderDashboard.test.tsx around chart test =='
cat -n frontend/src/components/SenderDashboard.test.tsx | sed -n '1,320p'
echo '== Search for recharts mocks in SenderDashboard tests/setup =='
rg -n 'vi\.mock\("recharts"|mock\("recharts"|ResponsiveContainer|XAxis|YAxis|BarChart' frontend/src/components/SenderDashboard.test.tsx frontend/src/setupTests.ts frontend/src/setupTests.js frontend/src --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 13925
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== SenderDashboard.test.tsx around chart test =='
cat -n frontend/src/components/SenderDashboard.test.tsx | sed -n '1,320p'
echo '== Search for recharts mocks in SenderDashboard tests/setup =='
rg -n 'vi\.mock\("recharts"|mock\("recharts"|ResponsiveContainer|XAxis|YAxis|BarChart' frontend/src/components/SenderDashboard.test.tsx frontend/src/setupTests.ts frontend/src/setupTests.js frontend/src --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 13925
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '== SenderDashboard.test.tsx around chart test =='
cat -n frontend/src/components/SenderDashboard.test.tsx | sed -n '1,320p'
echo '== Search for recharts mocks in SenderDashboard tests/setup =='
rg -n 'vi\.mock\("recharts"|mock\("recharts"|ResponsiveContainer|XAxis|YAxis|BarChart' frontend/src/components/SenderDashboard.test.tsx frontend/src/setupTests.ts frontend/src/setupTests.js frontend/src --glob '!**/node_modules/**'Repository: ritik4ever/stellar-stream
Length of output: 13925
frontend/src/components/SenderDashboard.test.tsx:229-258 — Mock Recharts or avoid chart-label assertions here. frontend/src/setupTests.ts has no ResizeObserver/layout shim, and this test doesn’t mock recharts like StreamMetricsChart.test.tsx does. That makes the getByText("Scheduled" | "Active" | …) checks depend on jsdom chart rendering; assert the status values from the data instead, or stub the chart components locally.
🤖 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 `@frontend/src/components/SenderDashboard.test.tsx` around lines 229 - 259, The
chart test in SenderDashboard.test.tsx is relying on jsdom/Recharts layout
rendering and text labels, which is brittle without the same mocking used
elsewhere. Update the "renders bar chart showing streams by status" test to
avoid asserting axis labels from the rendered chart, and instead either mock the
chart/recharts components locally or verify the underlying status data passed
into SenderDashboard/Streams by Status behavior using existing helpers like
setupSenderHandler and mock*Stream fixtures.
| case "created": | ||
| return `Stream created${amount ? ` (${amount} ${assetCode})` : ""}`; | ||
| case "claimed": | ||
| return `Claimed ${amount} ${assetCode}`; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Avoid rendering undefined in activity labels.
amount and assetCode are optional, but the claimed label always interpolates both; missing data renders Claimed undefined undefined.
Proposed fix
case "created":
- return `Stream created${amount ? ` (${amount} ${assetCode})` : ""}`;
+ return amount != null && assetCode
+ ? `Stream created (${amount} ${assetCode})`
+ : "Stream created";
case "claimed":
- return `Claimed ${amount} ${assetCode}`;
+ return amount != null && assetCode
+ ? `Claimed ${amount} ${assetCode}`
+ : "Claimed";📝 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.
| case "created": | |
| return `Stream created${amount ? ` (${amount} ${assetCode})` : ""}`; | |
| case "claimed": | |
| return `Claimed ${amount} ${assetCode}`; | |
| case "created": | |
| return amount != null && assetCode | |
| ? `Stream created (${amount} ${assetCode})` | |
| : "Stream created"; | |
| case "claimed": | |
| return amount != null && assetCode | |
| ? `Claimed ${amount} ${assetCode}` | |
| : "Claimed"; |
🤖 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 `@frontend/src/components/SenderDashboard.tsx` around lines 94 - 97, The
claimed activity label in SenderDashboard’s label formatting can render
undefined values because it always interpolates amount and assetCode even when
they are optional. Update the logic in the label switch for the claimed case to
conditionally include these fields only when present, matching the pattern
already used in the created case, so the activity text never shows undefined.
| try { | ||
| await cancelStream(id); | ||
| const result = await listStreams({ sender: senderAddress! }); | ||
| setStreams(result.data); | ||
|
|
||
| // Refresh events | ||
| const recentEvents = await getSenderEvents(senderAddress!); | ||
| setEvents(recentEvents); | ||
| } catch (err) { | ||
| alert(err instanceof Error ? err.message : "Failed to cancel stream"); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Refresh state after successful cancellations, including partial bulk success.
Both handlers put the mutation and refresh in the same try. If refresh fails after a cancel succeeds, the user sees a cancel failure. In bulk mode, Promise.all rejects on the first failure even after earlier streams may already be canceled, leaving stale rows and selected IDs.
Proposed bulk-cancel pattern
- await Promise.all(selectedIds.map((id) => cancelStream(id)));
+ const results = await Promise.allSettled(
+ selectedIds.map((id) => cancelStream(id))
+ );
const result = await listStreams({ sender: senderAddress! });
setStreams(result.data);
setSelectedStreamForBulkCancel(new Set());
// Refresh events
const recentEvents = await getSenderEvents(senderAddress!);
setEvents(recentEvents);
+ const failedCount = results.filter((r) => r.status === "rejected").length;
+ if (failedCount > 0) {
+ alert(`Failed to cancel ${failedCount} stream(s).`);
+ }Also applies to: 303-313
🤖 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 `@frontend/src/components/SenderDashboard.tsx` around lines 275 - 284, The
cancel handlers in SenderDashboard are treating refresh failures as cancel
failures because the mutation and post-success state reloads are inside the same
try; move the listStreams/getSenderEvents refresh logic out of the mutation
error path so a successful cancel still updates state even if refresh fails. For
the bulk flow in the same component, update the Promise.all-based cancellation
in the bulk handler to tolerate partial success, then refresh streams/events and
clear any selected IDs based on the completed cancels rather than aborting on
the first rejection.
| {/* Quick Action Buttons */} | ||
| {stats.activeStreams.length > 0 && ( | ||
| <section | ||
| className="recipient-dashboard-section" | ||
| style={{ marginTop: "2rem" }} | ||
| > | ||
| <div | ||
| style={{ | ||
| display: "flex", | ||
| gap: "1rem", | ||
| alignItems: "center", | ||
| }} | ||
| > | ||
| <h3 className="recipient-dashboard-section-title"> | ||
| Quick Actions | ||
| </h3> | ||
| {selectedStreamForBulkCancel.size > 0 && ( | ||
| <button | ||
| type="button" | ||
| className="btn-primary" | ||
| style={{ | ||
| backgroundColor: "#ef4444", | ||
| padding: "0.5rem 1rem", | ||
| fontSize: "0.875rem", | ||
| marginLeft: "auto", | ||
| }} | ||
| onClick={handleBulkCancel} | ||
| > | ||
| Bulk Cancel ({selectedStreamForBulkCancel.size}) | ||
| </button> | ||
| )} | ||
| </div> | ||
| </section> | ||
| )} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Use one cancellable stream list for quick actions and selection.
Quick Actions only renders when there is at least one active stream, but the table supports scheduled rows too; with only scheduled streams, users can select rows but never see Bulk Cancel. Paused streams are also counted in stats but are not rendered in either table.
Proposed direction
+ const cancellableStreams = [
+ ...stats.scheduledStreams,
+ ...stats.activeStreams,
+ ...stats.pausedStreams,
+ ];- {stats.activeStreams.length > 0 && (
+ {cancellableStreams.length > 0 && (- (stats.activeStreams.length +
- stats.scheduledStreams.length)
+ cancellableStreams.length
...
- [...stats.activeStreams, ...stats.scheduledStreams].map(
- (s) => s.id
- )
+ cancellableStreams.map((s) => s.id)
...
- {[...stats.scheduledStreams, ...stats.activeStreams].map(
+ {cancellableStreams.map(Also applies to: 578-624
🤖 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 `@frontend/src/components/SenderDashboard.tsx` around lines 493 - 526, Quick
Actions is gated on stats.activeStreams, but Bulk Cancel should be driven by the
same cancellable rows the table can actually select. Update SenderDashboard to
use a single cancellable stream list for both the quick-actions section and
selection state, so scheduled rows can still surface Bulk Cancel when selected.
Review the Quick Actions block and the row selection logic around
selectedStreamForBulkCancel, handleBulkCancel, and the stream table rendering to
ensure paused/scheduled streams are handled consistently.
| export async function getSenderEvents(senderAddress: string, limit: number = 10): Promise<StreamEvent[]> { | ||
| try { | ||
| // First get all streams for the sender | ||
| const streamsResult = await listStreams({ sender: senderAddress }); | ||
| const streams = streamsResult.data; | ||
|
|
||
| if (streams.length === 0) { | ||
| return []; | ||
| } | ||
|
|
||
| // Fetch events from each stream | ||
| const allEvents: StreamEvent[] = []; | ||
| const eventPromises = streams.map((stream) => | ||
| getStreamHistory(stream.id) | ||
| .then((events) => allEvents.push(...events)) | ||
| .catch(() => { | ||
| // Silent fail on individual stream event fetch | ||
| }) | ||
| ); | ||
|
|
||
| await Promise.all(eventPromises); | ||
|
|
||
| // Sort by timestamp descending (most recent first) and limit | ||
| return allEvents | ||
| .sort((a, b) => b.timestamp - a.timestamp) | ||
| .slice(0, limit); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Fetch sender activity from the sender events endpoint.
The linked requirement calls for /api/events?sender=<address> as the source for the last 10 activity items. This implementation fans out through listStreams and per-stream history, so it only covers the current streams page and adds N+1 requests on every dashboard refresh.
Proposed direction
export async function getSenderEvents(senderAddress: string, limit: number = 10): Promise<StreamEvent[]> {
try {
- // First get all streams for the sender
- const streamsResult = await listStreams({ sender: senderAddress });
- const streams = streamsResult.data;
-
- if (streams.length === 0) {
- return [];
- }
-
- // Fetch events from each stream
- const allEvents: StreamEvent[] = [];
- const eventPromises = streams.map((stream) =>
- getStreamHistory(stream.id)
- .then((events) => allEvents.push(...events))
- .catch(() => {
- // Silent fail on individual stream event fetch
- })
- );
-
- await Promise.all(eventPromises);
-
- // Sort by timestamp descending (most recent first) and limit
- return allEvents
+ const params = new URLSearchParams({
+ sender: senderAddress,
+ limit: String(limit),
+ });
+ const response = await fetch(`${API_BASE}/events?${params.toString()}`);
+ const body = await parseResponse<{ data: StreamEvent[] }>(response);
+ return body.data
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, limit);
} catch {
return [];
}
}📝 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.
| export async function getSenderEvents(senderAddress: string, limit: number = 10): Promise<StreamEvent[]> { | |
| try { | |
| // First get all streams for the sender | |
| const streamsResult = await listStreams({ sender: senderAddress }); | |
| const streams = streamsResult.data; | |
| if (streams.length === 0) { | |
| return []; | |
| } | |
| // Fetch events from each stream | |
| const allEvents: StreamEvent[] = []; | |
| const eventPromises = streams.map((stream) => | |
| getStreamHistory(stream.id) | |
| .then((events) => allEvents.push(...events)) | |
| .catch(() => { | |
| // Silent fail on individual stream event fetch | |
| }) | |
| ); | |
| await Promise.all(eventPromises); | |
| // Sort by timestamp descending (most recent first) and limit | |
| return allEvents | |
| .sort((a, b) => b.timestamp - a.timestamp) | |
| .slice(0, limit); | |
| export async function getSenderEvents(senderAddress: string, limit: number = 10): Promise<StreamEvent[]> { | |
| try { | |
| const params = new URLSearchParams({ | |
| sender: senderAddress, | |
| limit: String(limit), | |
| }); | |
| const response = await fetch(`${API_BASE}/events?${params.toString()}`); | |
| const body = await parseResponse<{ data: StreamEvent[] }>(response); | |
| return body.data | |
| .sort((a, b) => b.timestamp - a.timestamp) | |
| .slice(0, limit); | |
| } catch { | |
| return []; | |
| } | |
| } |
🤖 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 `@frontend/src/services/api.ts` around lines 345 - 370, The getSenderEvents
helper is using listStreams plus per-stream getStreamHistory fan-out instead of
the sender events endpoint required by the review. Update getSenderEvents to
fetch directly from /api/events with the sender query parameter, keep the
existing limit handling for the last 10 items, and remove the N+1 stream/history
aggregation logic so the function returns the sender’s activity from the single
endpoint.
| ### 3. Comprehensive Test Suite (`SenderDashboard.test.tsx`) | ||
| **Location:** `frontend/src/components/SenderDashboard.test.tsx` | ||
|
|
||
| #### Test Coverage: 15+ Tests |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Test-count documentation is inconsistent with PR artifacts.
This file states “15+ tests”, while the PR summary and other added docs state 14. Please align to one verified count to avoid acceptance/reporting drift.
Also applies to: 168-168, 207-207
🤖 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 `@SENDER_DASHBOARD_IMPLEMENTATION.md` at line 95, The test-count documentation
is inconsistent across the added docs, so align the verified number everywhere
it appears. Update the count in SENDER_DASHBOARD_IMPLEMENTATION.md and the other
referenced sections to match the actual PR artifact count, using the same
wording consistently so the summary, implementation notes, and test coverage
references all agree.
| ``` | ||
| ┌─────────────────────────────────────────────────────────────┐ | ||
| │ SenderDashboard │ | ||
| │ │ | ||
| │ Header: "Sender Dashboard" [Create Stream Button] ──┐ │ | ||
| │ Subtitle: "View your outgoing streams..." │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ STATS CARDS SECTION │ │ | ||
| │ │ │ │ | ||
| │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌────────┐ │ │ | ||
| │ │ │ Streams │ │ Total │ │ Active │ │Complet │ │ │ | ||
| │ │ │ Created │ │ Amount │ │ Streams │ │ /Cancel│ │ │ | ||
| │ │ │ 5 │ │ 3000 │ │ 3 │ │ 2 │ │ │ | ||
| │ │ └──────────┘ └──────────┘ └──────────┘ └────────┘ │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ ASSET BREAKDOWN CARDS │ │ | ||
| │ │ │ │ | ||
| │ │ ┌──────────┐ ┌──────────┐ │ │ | ||
| │ │ │Total USDC│ │ Total XLM│ │ │ | ||
| │ │ │ 2500 │ │ 500 │ │ │ | ||
| │ │ └──────────┘ └──────────┘ │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ STREAMS BY STATUS - BAR CHART │ │ | ||
| │ │ │ │ | ||
| │ │ ▓ │ │ | ||
| │ │ ▓ ▓ ▓ │ │ | ||
| │ │ ┌────┼──────┼───┼──┬───┐ │ │ | ||
| │ │ │Sch│Active│Pau│Com│Can│ │ │ | ||
| │ │ └────┴──────┴───┴───┴───┘ │ │ | ||
| │ │ 1 2 1 1 1 │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ QUICK ACTIONS │ │ | ||
| │ │ │ │ | ||
| │ │ [Bulk Cancel (2)] ◄─ When streams selected │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ RECENT ACTIVITY │ │ | ||
| │ │ │ │ | ||
| │ │ • Stream paused Nov 24, 10:55 │ │ | ||
| │ │ Stream: str...m1 │ │ | ||
| │ │ │ │ | ||
| │ │ • Stream resumed Nov 23, 09:30 │ │ | ||
| │ │ Stream: str...m1 │ │ | ||
| │ │ │ │ | ||
| │ │ • Claimed 500 USDC Nov 22, 14:20 │ │ | ||
| │ │ Stream: str...m1 │ │ | ||
| │ │ │ │ | ||
| │ │ • Stream created (1000 USDC) Nov 21, 11:00 │ │ | ||
| │ │ Stream: str...m1 │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ ACTIVE & SCHEDULED STREAMS TABLE │ │ | ||
| │ │ │ │ | ||
| │ │ ☐ To Asset Total Status Progress Actions │ │ | ||
| │ │ ☑ Gre... USDC 1000 Active 50% ✏️ Cancel │ │ | ||
| │ │ ☐ Gre... USDC 500 Schedul 0% Edit Cancel │ │ | ||
| │ │ │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| │ ┌─────────────────────────────────────────────────────┐ │ | ||
| │ │ HISTORY - COMPLETED STREAMS TABLE │ │ | ||
| │ │ │ │ | ||
| │ │ To Asset Total Status │ │ | ||
| │ │ Gre... USDC 1000 Completed │ │ | ||
| │ │ Gre... USDC 500 Canceled │ │ | ||
| │ │ │ │ | ||
| │ └─────────────────────────────────────────────────────┘ │ | ||
| │ │ | ||
| └─────────────────────────────────────────────────────────────┘ | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add language identifiers to fenced code blocks (markdownlint MD040).
Please specify fence languages for the diagram/tree/flow blocks.
Suggested patch
-```
+```text
┌─────────────────────────────────────────────────────────────┐
...
-```
+```
-```
+```text
SenderDashboard
...
-```
+```
-```
+```text
1. Component Mount
...
-```
+```Also applies to: 87-127, 131-153
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 5-5: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 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 `@SENDER_DASHBOARD_STRUCTURE.md` around lines 5 - 83, The fenced
diagram/tree/flow blocks in this markdown file are missing language identifiers,
which triggers MD040. Update each affected fenced block in
SENDER_DASHBOARD_STRUCTURE.md to use a text/diagram-style language tag
consistently, and verify the blocks around the SenderDashboard layout, the tree
view, and the flow steps all have explicit fence languages while keeping the
content unchanged.
Source: Linters/SAST tools
Closes #392
What changed
Testing done
Related issues
Closes #392
Checklist
Summary by CodeRabbit
New Features
Bug Fixes