feat: implement graceful shutdown (#585) and backer dashboard (#598) - #752
feat: implement graceful shutdown (#585) and backer dashboard (#598)#752teefeh-07 wants to merge 1 commit into
Conversation
|
@teefeh-07 is attempting to deploy a commit to the ritik4ever's projects Team on Vercel. A member of the Team first needs to authorize it. |
|
@teefeh-07 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! 🚀 |
📝 WalkthroughWalkthroughThe backend adds contributor pledge aggregation, an API endpoint, and SQLite-aware graceful shutdown. The frontend adds wallet-gated routing, pledge metrics, campaign summaries, and quick refunds through a new backer dashboard. ChangesBacker dashboard
Graceful shutdown
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant BackerDashboard
participant BackendAPI
participant campaignStore
BackerDashboard->>BackendAPI: Request wallet pledge summaries
BackendAPI->>campaignStore: getUserPledges(address)
campaignStore-->>BackendAPI: Campaign pledge summaries
BackendAPI-->>BackerDashboard: Return summary data
sequenceDiagram
participant ShutdownHandler
participant HTTPServer
participant SQLite
ShutdownHandler->>HTTPServer: Stop accepting connections and drain requests
HTTPServer-->>ShutdownHandler: server.close completes
ShutdownHandler->>SQLite: WAL checkpoint and close connection
SQLite-->>ShutdownHandler: Close result
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches 💡 2⚔️ Resolve merge conflicts 💡
🛠️ Fix failing CI checks 💡
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
backend/src/index.tsParsing error: Cannot read file '/tsconfig.eslint.json'. backend/src/services/campaignStore.tsParsing error: Cannot read file '/tsconfig.eslint.json'. backend/src/services/db.tsParsing error: Cannot read file '/tsconfig.eslint.json'.
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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
backend/src/index.ts (1)
869-878: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick winForced-shutdown path skips the WAL checkpoint/DB close entirely.
When the grace-period timer fires (Line 871-878), the process calls
process.exit(1)immediately without ever invokingcloseDb(). This is precisely the path exercised when in-flight requests hang past 30s — i.e., the scenario most likely to trigger a forced exit — yet it bypasses the WAL checkpoint that the rest of this PR's objective calls out ("Checkpoints WAL and closes the SQLite connection viacloseDb()"). SQLite's WAL mode tolerates ungraceful termination, but skipping the checkpoint here means the WAL file is never truncated on this path, which can leave WAL growth uncontrolled if forced shutdowns are frequent (e.g., during rolling restarts with slow drains).Consider attempting a best-effort
closeDb()call inside the timeout branch too (it should be fast since it's just a pragma + close):🛡️ Suggested fix
const gracePeriodTimer = setTimeout(() => { logError( new Error('Graceful shutdown timeout exceeded'), { event: 'graceful_shutdown_timeout', gracePeriodSeconds }, config.logLevel, ); + import('./services/db').then(({ closeDb }) => closeDb()).catch(() => {}); process.exit(1); }, gracePeriodSeconds * 1000);🤖 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 `@backend/src/index.ts` around lines 869 - 878, Update the graceful-shutdown timeout callback to make a best-effort asynchronous call to closeDb() before forcing process.exit(1), ensuring the WAL checkpoint and SQLite close are attempted when the grace period expires. Preserve the existing timeout logging and forced-exit behavior, and handle any closeDb failure without preventing the exit.
🧹 Nitpick comments (3)
frontend/src/pages/BackerDashboard.tsx (2)
170-170: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winReuse the backend-computed
canRefundflag instead of re-deriving it fromstatus.
summary.campaign.progress.canRefundalready encodesclaimedAt === undefined && deadlineReached && pledgedAmount < targetAmount, which is exactly the condition being reconstructed here viastatus === 'failed'. Deriving eligibility independently duplicates business logic that could drift ifcalculateProgress's status/canRefund relationship ever changes.♻️ Suggested fix
- const canRefund = summary.campaign.progress.status === 'failed' && summary.totalPledged > 0; + const canRefund = summary.campaign.progress.canRefund && summary.totalPledged > 0;🤖 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/pages/BackerDashboard.tsx` at line 170, Update the canRefund assignment in BackerDashboard to use the backend-provided summary.campaign.progress.canRefund value directly, removing the local status and totalPledged derivation while preserving the existing refund flow.
9-9: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win
ErrorBoundaryis imported but never wraps any content.
App.tsxwraps comparable panels (e.g.CampaignDetailPanel,CampaignsTable) inErrorBoundaryto contain render errors. This dashboard importsErrorBoundarybut the pledges table/section isn't wrapped in it, so a render error here would crash the whole page instead of degrading gracefully.Also applies to: 149-204
🤖 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/pages/BackerDashboard.tsx` at line 9, Wrap the pledges table/section rendered in BackerDashboard with the imported ErrorBoundary, matching the existing panel-wrapping pattern used in App.tsx. Ensure the relevant content in the render block around the pledges section is enclosed so render errors are contained, and remove the import only if no content remains to wrap.backend/src/services/campaignStore.ts (1)
1220-1261: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winN+1 query pattern in
getUserPledges.For each distinct campaign the function issues a dedicated pledges query (Line 1236-1240) plus
calculateProgress(Line 1234), which itself queriesgetActivePledgeCountper campaign. For a contributor with N backed campaigns this results in roughly1 + 2Nqueries per dashboard load instead of a small constant number.♻️ Suggested consolidation
- const campaignRows = db.prepare(` - SELECT DISTINCT c.* - FROM campaigns c - JOIN pledges p ON p.campaign_id = c.id - WHERE p.contributor = ? AND c.deleted_at IS NULL - `).all(contributor) as CampaignRow[]; - - const summaries: UserPledgeSummary[] = []; - - for (const row of campaignRows) { - const campaign = rowToCampaign(row); - const progress = calculateProgress(campaign); - - const pledgeRows = db.prepare(` - SELECT * FROM pledges - WHERE campaign_id = ? AND contributor = ? - ORDER BY created_at DESC - `).all(campaign.id, contributor) as PledgeRow[]; - - const pledges: PledgeRecord[] = pledgeRows.map(rowToPledge); + const allPledgeRows = db.prepare(` + SELECT * FROM pledges WHERE contributor = ? ORDER BY created_at DESC + `).all(contributor) as PledgeRow[]; + + const pledgesByCampaign = new Map<string, PledgeRow[]>(); + for (const row of allPledgeRows) { + const list = pledgesByCampaign.get(row.campaign_id) ?? []; + list.push(row); + pledgesByCampaign.set(row.campaign_id, list); + } + + const campaignRows = db.prepare(` + SELECT * FROM campaigns WHERE id IN (${[...pledgesByCampaign.keys()].map(() => '?').join(',')}) AND deleted_at IS NULL + `).all(...pledgesByCampaign.keys()) as CampaignRow[]; + + const summaries: UserPledgeSummary[] = []; + + for (const row of campaignRows) { + const campaign = rowToCampaign(row); + const progress = calculateProgress(campaign); + const pledges: PledgeRecord[] = (pledgesByCampaign.get(campaign.id) ?? []).map(rowToPledge);🤖 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 `@backend/src/services/campaignStore.ts` around lines 1220 - 1261, Consolidate the per-campaign work in getUserPledges to avoid the N+1 query pattern: fetch the contributor’s campaign and pledge data in a bounded number of queries, and derive each campaign’s progress from the already-loaded pledge data instead of calling calculateProgress for every campaign. Preserve the existing totals, refund classification, pledge ordering, and deleted-campaign filtering.
🤖 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 `@backend/src/index.ts`:
- Around line 634-642: Update the `/api/users/:address/pledges` handler to
validate `address` with the existing `isValidStellarPublicKey` helper in
addition to the current type/presence check. Reject invalid keys with the
established 400 `AppError` response before calling `getUserPledges`.
In `@backend/src/services/campaignStore.ts`:
- Around line 1244-1253: The pledge totals in
backend/src/services/campaignStore.ts:1244-1253 must make totalPledged gross by
adding every pledge amount unconditionally, while totalRefunded only accumulates
pledges with refundedAt; preserve the resulting totalPledged - totalRefunded
calculation. No code change is needed in
frontend/src/pages/BackerDashboard.tsx:55-67; verify its net calculation and
per-campaign Total Pledged/Total Refunded columns remain correct with gross
totals.
In `@backend/src/services/db.ts`:
- Around line 56-60: Update closeDb() so wal_checkpoint(TRUNCATE) failures are
captured and surfaced after the database close operation completes. Ensure the
database is always closed, then rethrow or otherwise propagate the checkpoint
error so shutdown does not report success when checkpointing fails.
In `@frontend/src/pages/BackerDashboard.tsx`:
- Around line 99-104: Update the BackerDashboard wallet connection flow around
WalletWidget and reuse App.tsx’s config-driven network passphrase, such as
appConfig?.networkPassphrase ?? DEFAULT_NETWORK_PASSPHRASE, instead of the
hardcoded testnet value. Route the connection through the existing
handleConnectWallet behavior or equivalent logic so the promise is awaited and
failures update loading/error state and display the established toast feedback.
---
Outside diff comments:
In `@backend/src/index.ts`:
- Around line 869-878: Update the graceful-shutdown timeout callback to make a
best-effort asynchronous call to closeDb() before forcing process.exit(1),
ensuring the WAL checkpoint and SQLite close are attempted when the grace period
expires. Preserve the existing timeout logging and forced-exit behavior, and
handle any closeDb failure without preventing the exit.
---
Nitpick comments:
In `@backend/src/services/campaignStore.ts`:
- Around line 1220-1261: Consolidate the per-campaign work in getUserPledges to
avoid the N+1 query pattern: fetch the contributor’s campaign and pledge data in
a bounded number of queries, and derive each campaign’s progress from the
already-loaded pledge data instead of calling calculateProgress for every
campaign. Preserve the existing totals, refund classification, pledge ordering,
and deleted-campaign filtering.
In `@frontend/src/pages/BackerDashboard.tsx`:
- Line 170: Update the canRefund assignment in BackerDashboard to use the
backend-provided summary.campaign.progress.canRefund value directly, removing
the local status and totalPledged derivation while preserving the existing
refund flow.
- Line 9: Wrap the pledges table/section rendered in BackerDashboard with the
imported ErrorBoundary, matching the existing panel-wrapping pattern used in
App.tsx. Ensure the relevant content in the render block around the pledges
section is enclosed so render errors are contained, and remove the import only
if no content remains to wrap.
🪄 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 Plus
Run ID: 74f37e08-9444-40a1-8a20-876ada08cf86
📒 Files selected for processing (8)
backend/src/index.tsbackend/src/services/campaignStore.tsbackend/src/services/db.tsfrontend/src/App.tsxfrontend/src/main.tsxfrontend/src/pages/BackerDashboard.tsxfrontend/src/services/api.tsfrontend/src/types/campaign.ts
| app.get('/api/users/:address/pledges', (req: Request, res: Response) => { | ||
| const address = req.params.address; | ||
| if (!address || typeof address !== 'string') { | ||
| throw new AppError('Invalid address parameter.', 400, 'BAD_REQUEST'); | ||
| } | ||
|
|
||
| const summaries = getUserPledges(address); | ||
| res.json({ data: summaries }); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check whether other routes/handlers validate Stellar address format
rg -nP -C3 '(isValidStellarAddress|StrKey\.isValidEd25519PublicKey|address.*regex|G\[A-Z2-7\]\{55\})' backend/srcRepository: ritik4ever/stellar-goal-vault
Length of output: 2394
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== stellarAddress.ts =="
cat -n backend/src/validation/stellarAddress.ts
echo
echo "== schemas.ts =="
cat -n backend/src/validation/schemas.ts
echo
echo "== openapi.ts =="
cat -n backend/src/openapi.ts
echo
echo "== usage of VALIDATORS/via import paths =="
rg -n "from ['\"](\.\./)?validation/(stellarAddress|schemas)|from ['\"]\.\./validation|STELLAR_ACCOUNT_REGEX|isValidStellarPublicKey|stellarAddressSchema" backend/srcRepository: ritik4ever/stellar-goal-vault
Length of output: 50385
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== validateStellarAddress export status =="
if [ -f backend/src/services/checkpoints/checkpoints.ts ]; then
rg -n -C3 "validateStellarAddress|isValidStellar" backend/src/services/checkpoints/checkpoints.ts
else
echo "checkpoints.ts not found"
fi
echo
echo "== index route context =="
sed -n '600,660p;850,905p' backend/src/index.ts | cat -n
echo
echo "== stellarAddress schema exports/usages =="
rg -n "export (const|function|let)|isValidStellarPublicKey|stellarAccount|stellarAccountIdSchema" backend/src/validation backend/src -g '!index.ts' -g '!openapi.ts' -g '!schemas.ts' -g '!stellarAddress.ts'Repository: ritik4ever/stellar-goal-vault
Length of output: 14173
Validate /api/users/:address/pledges with the Stellar public-key validator.
This endpoint only checks that address is a non-empty string before calling getUserPledges. Use the existing isValidStellarPublicKey validation logic here as well so malformed G... inputs fail fast with a consistent 400 response instead of reaching store/query logic.
🤖 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 `@backend/src/index.ts` around lines 634 - 642, Update the
`/api/users/:address/pledges` handler to validate `address` with the existing
`isValidStellarPublicKey` helper in addition to the current type/presence check.
Reject invalid keys with the established 400 `AppError` response before calling
`getUserPledges`.
| let totalPledged = 0; | ||
| let totalRefunded = 0; | ||
|
|
||
| for (const pledge of pledges) { | ||
| if (pledge.refundedAt) { | ||
| totalRefunded += pledge.amount; | ||
| } else { | ||
| totalPledged += pledge.amount; | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win
Net Invested can go negative — totalPledged excludes refunded amounts, so the frontend double-subtracts refunds. getUserPledges accumulates totalPledged only for pledges without refundedAt, meaning refunded contributions are dropped from totalPledged entirely rather than counted then subtracted back out. The dashboard then computes net = totalPledged - totalRefunded, which subtracts refunds a second time. Concretely: a $50 pledge that gets fully refunded yields totalPledged=0, totalRefunded=50, so net = -50 — a backer who was fully refunded appears to have negative net investment instead of $0.
backend/src/services/campaignStore.ts#L1244-L1253: maketotalPledgedthe gross amount (sum every pledge unconditionally), and only add tototalRefundedwhenrefundedAtis set, sototalPledged - totalRefundedyields the correct currently-active amount:
for (const pledge of pledges) {
- if (pledge.refundedAt) {
- totalRefunded += pledge.amount;
- } else {
- totalPledged += pledge.amount;
- }
+ totalPledged += pledge.amount;
+ if (pledge.refundedAt) {
+ totalRefunded += pledge.amount;
+ }
}frontend/src/pages/BackerDashboard.tsx#L55-L67: no code change needed oncetotalPledgedis gross;net = round(pledged - refunded)will then be correct. Verify the per-campaign table columns (Total Pledged/Total Refunded, Lines 181-182) still read sensibly with grosstotalPledged.
📝 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.
| let totalPledged = 0; | |
| let totalRefunded = 0; | |
| for (const pledge of pledges) { | |
| if (pledge.refundedAt) { | |
| totalRefunded += pledge.amount; | |
| } else { | |
| totalPledged += pledge.amount; | |
| } | |
| } | |
| let totalPledged = 0; | |
| let totalRefunded = 0; | |
| for (const pledge of pledges) { | |
| totalPledged += pledge.amount; | |
| if (pledge.refundedAt) { | |
| totalRefunded += pledge.amount; | |
| } | |
| } |
📍 Affects 2 files
backend/src/services/campaignStore.ts#L1244-L1253(this comment)frontend/src/pages/BackerDashboard.tsx#L55-L67
🤖 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 `@backend/src/services/campaignStore.ts` around lines 1244 - 1253, The pledge
totals in backend/src/services/campaignStore.ts:1244-1253 must make totalPledged
gross by adding every pledge amount unconditionally, while totalRefunded only
accumulates pledges with refundedAt; preserve the resulting totalPledged -
totalRefunded calculation. No code change is needed in
frontend/src/pages/BackerDashboard.tsx:55-67; verify its net calculation and
per-campaign Total Pledged/Total Refunded columns remain correct with gross
totals.
| try { | ||
| db.pragma('wal_checkpoint(TRUNCATE)'); | ||
| } catch { | ||
| // Ignore if checkpoint fails during shutdown | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n "closeDb|wal_checkpoint|gracefulShutdown" backend --glob '*test*' --glob '*spec*'Repository: ritik4ever/stellar-goal-vault
Length of output: 167
🏁 Script executed:
#!/bin/bash
set -e
echo "Files:"
git ls-files backend/src/services backend/src | sed -n '1,120p'
echo
echo "db.ts:"
cat -n backend/src/services/db.ts
echo
echo "index.ts shutdown references:"
rg -n "closeDb|process.exit|shutdown|logger|close" backend/src/index.ts backend/src || trueRepository: ritik4ever/stellar-goal-vault
Length of output: 12356
🏁 Script executed:
#!/bin/bash
set -e
echo "index.ts shutdown block:"
cat -n backend/src/index.ts | sed -n '855,896p'
echo
echo "db.wal.test.ts:"
cat -n backend/src/services/db.wal.test.ts
echo
echo "Tests mentioning db close/wal/no tests with those refs:"
rg -n "closeDb|wal_checkpoint|db_close_error|process.exit" backend --glob '*test*'Repository: ritik4ever/stellar-goal-vault
Length of output: 3626
Propagate WAL checkpoint failures instead of swallowing them.
closeDb() currently returns normally when wal_checkpoint(TRUNCATE) throws, so graceful shutdown still calls process.exit(0) despite the requested checkpoint not completing. Close the database in all cases, then rethrow or otherwise surface checkpoint failures so shutdown failure is observable rather than suppressed.
🤖 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 `@backend/src/services/db.ts` around lines 56 - 60, Update closeDb() so
wal_checkpoint(TRUNCATE) failures are captured and surfaced after the database
close operation completes. Ensure the database is always closed, then rethrow or
otherwise propagate the checkpoint error so shutdown does not report success
when checkpointing fails.
| <WalletWidget | ||
| status={freighter.status} | ||
| publicKey={freighter.publicKey} | ||
| error={freighter.error} | ||
| onConnect={() => freighter.connect("Test SDF Network ; September 2015")} | ||
| /> |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Hardcoded testnet passphrase diverges from the app-wide config-driven connect flow.
onConnect={() => freighter.connect("Test SDF Network ; September 2015")} hardcodes the testnet passphrase, while App.tsx's handleConnectWallet uses appConfig?.networkPassphrase ?? DEFAULT_NETWORK_PASSPHRASE fetched from the backend config. If the deployment is configured for a different network (e.g. mainnet), connecting from this dashboard will request the wrong network, causing wallet-connect mismatches or failures. The connect call also isn't awaited/error-handled the way handleConnectWallet is (no loading/error state, no toast on failure).
🛡️ Suggested fix
+import { getAppConfig } from "../services/api";
+import { DEFAULT_NETWORK_PASSPHRASE } from "../constants";
+...
+ const [appConfig, setAppConfig] = useState<AppConfig | null>(null);
+ useEffect(() => {
+ getAppConfig().then(setAppConfig).catch(() => {});
+ }, []);
+ async function handleConnectWallet() {
+ const networkPassphrase = appConfig?.networkPassphrase ?? DEFAULT_NETWORK_PASSPHRASE;
+ try {
+ await freighter.connect(networkPassphrase);
+ } catch (error) {
+ addToast(error instanceof Error ? error.message : "Failed to connect wallet", "error");
+ }
+ }
...
- onConnect={() => freighter.connect("Test SDF Network ; September 2015")}
+ onConnect={() => { void handleConnectWallet(); }}🤖 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/pages/BackerDashboard.tsx` around lines 99 - 104, Update the
BackerDashboard wallet connection flow around WalletWidget and reuse App.tsx’s
config-driven network passphrase, such as appConfig?.networkPassphrase ??
DEFAULT_NETWORK_PASSPHRASE, instead of the hardcoded testnet value. Route the
connection through the existing handleConnectWallet behavior or equivalent logic
so the promise is awaited and failures update loading/error state and display
the established toast feedback.
|
Hi @teefeh-07, This PR could not be merged because it has merge conflicts with the target branch. Please resolve the merge conflicts, push the updated changes, and the PR can be reviewed and merged. Thank you! |
1 similar comment
|
Hi @teefeh-07, This PR could not be merged because it has merge conflicts with the target branch. Please resolve the merge conflicts, push the updated changes, and the PR can be reviewed and merged. Thank you! |
Description
This Pull Request introduces a comprehensive Backer Dashboard and implements graceful shutdown procedures for the backend, resolving issues #598 and #585.
Closes
Tasks and Fixes Made
🛡️ Graceful Shutdown (Backend)
Ensured that the server shuts down smoothly without dropping active requests or corrupting the database.
backend/src/index.tsto listen forSIGTERMandSIGINTsignals, immediately stopping the acceptance of new connections.closeDb()function inbackend/src/services/db.tsto explicitly checkpoint the WAL (Write-Ahead Logging) and cleanly close the SQLite connection before exiting.drainDurationMs).📊 Backer Dashboard (Frontend & Backend)
Created a dedicated dashboard allowing backers to track their pledges, view investments, and claim refunds.
GET /api/users/:address/pledgesendpoint inbackend/src/index.tsto fetch a user's entire pledge history.getUserPledgesinbackend/src/services/campaignStore.tsto perform the necessary SQL joins betweencampaignsandpledgestables to aggregate a user's total active investments and claimable refunds.BackerDashboard.tsxinfrontend/src/pages/to display:/my-pledgesroute inmain.tsxand added a convenient "My Pledges" link to the globalApp.tsxnavigation header (visible when a wallet is connected).How to Test
Test the Dashboard:
Test Graceful Shutdown:
npm startornpm run dev).SIGTERM(e.g.,kill <pid>) orSIGINT(Ctrl+C).server_shutting_downis emitted, wait for the connection to drain, and ensureserver_closedis logged with the drain duration before the process cleanly exits.Closes [FEATURE] Add backer dashboard with pledging activity #598
Closes [FEATURE] Add graceful shutdown handler to backend #585
Summary by CodeRabbit