feat: add in-app feedback board#3834
Conversation
TinyPlace-style feedback board for openhuman: a global list of all users' feedback with Reddit-style up/down voting, feature/bug submission (LLM-moderated server-side), comment threads, board filters, and an admin-only status control. Backed by backend-alphahuman's /feedback API via apiClient (own JWT users, not the tiny.place wallet board). Adds the /feedback route + sidebar nav entry, i18n across all 14 locales, and Vitest coverage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughAdds a complete Feedback board feature: TypeScript types and a ChangesFeedback Board Feature
Sequence Diagram(s)sequenceDiagram
participant User
participant FeedbackPage
participant FeedbackItemRow
participant FeedbackVoteControl
participant feedbackApi
rect rgba(100, 149, 237, 0.5)
note over FeedbackPage,feedbackApi: Initial Load
FeedbackPage->>feedbackApi: listFeedback(sort, typeFilter, statusFilter, page)
feedbackApi-->>FeedbackPage: FeedbackListResult { items, total }
FeedbackPage->>FeedbackItemRow: render item rows
end
rect rgba(144, 238, 144, 0.5)
note over User,feedbackApi: Optimistic Vote
User->>FeedbackVoteControl: click upvote
FeedbackVoteControl->>FeedbackPage: onVoted(optimisticItem)
FeedbackPage->>FeedbackPage: patch item in-place (no refetch)
FeedbackVoteControl->>feedbackApi: voteFeedback(id, 1)
feedbackApi-->>FeedbackVoteControl: serverItem
FeedbackVoteControl->>FeedbackPage: onVoted(serverItem)
end
rect rgba(255, 165, 0, 0.5)
note over User,feedbackApi: Submit New Feedback
User->>FeedbackPage: submit form (FeedbackSubmitForm)
FeedbackPage->>feedbackApi: submitFeedback(input)
feedbackApi-->>FeedbackPage: CreateFeedbackResult
alt accepted and matches active filters
FeedbackPage->>feedbackApi: listFeedback(page 1) reload
feedbackApi-->>FeedbackPage: updated FeedbackListResult
end
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b7aeb11131
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
handleAccepted prepended a newly-accepted item and bumped the total unconditionally, so a submission that doesn't match the active type/status filter (e.g. a Feature while filtered to Bugs) wrongly appeared and skewed the count. Now gated by a pure acceptedItemMatchesFilters() helper (unit tested). Addresses the review comment on Feedback.tsx. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…aths Adds Vitest specs for FeedbackSubmitForm (submit/clear/success, moderation reject, type toggle, error) and FeedbackAdminMenu (status change + error), and extends FeedbackItemRow (comment-thread expand, GitHub issue link). Raises diff coverage on the feedback changes back above the 80% gate. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CodeGhost21
left a comment
There was a problem hiding this comment.
Review — feedback board
Clean, well-architected PR that follows project conventions closely and is unusually well-tested. All 19 CI checks pass. Findings below are mostly minor; nothing blocking.
Strengths
- Architecture compliance is excellent. All HTTP lives in
services/api/feedbackApi.ts; components are presentation/orchestration only. TheApiResponse<T>→response.dataunwrap and directapiClientusage match the establishedinviteApi/rewardsApipattern verbatim. Using RESTapiClientrather thancore_rpc_relayis correct here — this hits the backend directly like its siblings, not the Rust core. - i18n is complete and real — keys in
en.tsplus genuine translations across all 14 locales; parity job passes, no English placeholders. - Thorough tests — every component covers happy path, error, and edge cases (optimistic vote + rollback + retract, moderation reject, admin gating, name fallback). The pure
acceptedItemMatchesFiltershelper is exported and unit-tested. - Good defensive patterns: request-id guards on async loads,
encodeURIComponenton ids, optimistic-with-rollback voting reconciled against server tallies,rel="noreferrer noopener"on external links, namespaceddebuglogging on every API call/failure.
Issues & risks
1. Deploy ordering — nav tab ships to all users unconditionally (functional risk).
The /feedback route and sidebar tab are added with no feature flag and no capability gate. If this frontend deploys before the companion backend (/feedback endpoint) is live, every user gets a visible tab that lands on a load error. The PR body says the backend PR "should merge/deploy first," but nothing enforces it. Worth confirming the deploy sequence, or gating the tab behind a flag until the backend is rolled out. This is the one item I'd actively confirm before merge.
2. Comment count isn't pluralized and assembles word-order in JSX.
FeedbackItemRow.tsx: {item.commentCount} {t('feedback.comments')} renders "1 comments", and count-then-word order is hardcoded — awkward for several of the locales just added (and RTL like ar). Since everything else is fully i18n'd, an ICU-style {count, plural, ...} key would be more consistent.
3. Admin can set closed, but the status filter can't select it.
FeedbackAdminMenu offers open/planned/completed/closed, while the board's status filter (Feedback.tsx) only offers open/planned/completed. A closed item is then only visible under "All statuses". Confirm this is deliberate.
4. New accepted items are always prepended regardless of sort.
handleAccepted does [accepted, ...prev] even when sort is top/hot, so a brand-new zero-score item jumps above higher-ranked ones until the next reload. Cosmetic; self-corrects on refresh.
5. Minor. github.issueUrl is rendered straight into href with no scheme check — low risk since it's server-generated, but worth a guard if ever user-influenceable. FeedbackComments.getFeedback also loads all comments unpaginated — fine for now, could grow.
Verdict
Approve with minor follow-ups. No blocking correctness or security issues; conventions, tests, and i18n are all in order. The thing to confirm before merge is #1 (backend deploy ordering); #2–#5 are nits that can be deferred.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f6ee47757a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| }, [load]); | ||
|
|
||
| const handleItemChange = (updated: FeedbackItem) => { | ||
| setItems(prev => prev.map(item => (item.id === updated.id ? updated : item))); |
There was a problem hiding this comment.
Drop updated rows that leave the active filter
When an admin is viewing a filtered board (for example status=open) and changes a row to completed or closed, FeedbackAdminMenu sends the updated item here, but the list only replaces the row. The item then remains visible in a query it no longer matches and the filtered total stays too high until a full reload; remove/decrement when acceptedItemMatchesFilters no longer passes or refetch the current query after status updates.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Addressed in 4598d00. handleItemChange now checks acceptedItemMatchesFilters on the updated row: in-filter edits (votes, comments, in-filter status changes) are still patched in place, but once a status change pushes the row out of the active filter (e.g. open → completed while filtered to Open) it calls reload() so the row drops and the total/paging realign with the query.
…ions Optimistic local edits on the feedback board drifted from the filtered/ paginated query the server actually returns: - Accepting a submission prepended it and bumped the total unconditionally, so when page 1 was full the next "Load more" fetched backend page 2 while page 1 now held the new item — duplicating/skipping rows. - A status change only replaced the row in place, so an item moved out of the active filter (e.g. open -> completed while filtered to Open) stayed visible and kept the total too high until a full reload. Re-anchor to the server instead of patching local state: a shared reload() refetches page 1 after a mutation that changes membership. handleAccepted reloads only when the new item matches the active filters; handleItemChange patches in place for in-filter edits (votes, comments) and reloads when a status change pushes the row out of the filter. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 6
🧹 Nitpick comments (1)
app/src/pages/Feedback.tsx (1)
114-114: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winUse a shared interface type for the callback payload instead of an inline object shape.
Line 114 defines an inline object type, which can drift from the API contract. Reuse
CreateFeedbackResultfromapp/src/types/feedback(or a derivedPick) for consistency.Proposed change
-import type { FeedbackItem, FeedbackSort, FeedbackStatus, FeedbackType } from '../types/feedback'; +import type { + CreateFeedbackResult, + FeedbackItem, + FeedbackSort, + FeedbackStatus, + FeedbackType, +} from '../types/feedback'; @@ - const handleAccepted = (result: { feedback: FeedbackItem | null }) => { + const handleAccepted = (result: CreateFeedbackResult) => { const accepted = result.feedback;As per coding guidelines:
**/*.{ts,tsx}requires usinginterfacefor object shapes in TypeScript.🤖 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 `@app/src/pages/Feedback.tsx` at line 114, Replace the inline object type annotation in the handleAccepted function parameter with a shared interface type from the feedback types module. Instead of defining the type inline as `{ feedback: FeedbackItem | null }`, import and use the `CreateFeedbackResult` interface (or create a derived type using `Pick` if only specific properties are needed) from `app/src/types/feedback` to ensure consistency with the API contract and follow the TypeScript coding guidelines that require named interfaces for object shapes.Source: Coding guidelines
🤖 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 `@app/src/components/feedback/FeedbackAdminMenu.tsx`:
- Around line 50-55: The label element for the feedback status is not
programmatically associated with the select control below it, making it
inaccessible to assistive technology. Add an id attribute to the select element
(the one with value={item.status} and the handleChange callback), and add an
htmlFor attribute to the label element with the same id value to create the
proper association between the label and the form control.
In `@app/src/components/feedback/FeedbackItemRow.tsx`:
- Around line 118-121: The onCommentAdded callback in the FeedbackComments
component passes a full reconstructed item object to onChange, which can
overwrite newer field changes if they occur before the comment post completes.
Instead of passing the entire item with an updated commentCount, modify the
callback to pass only the id and commentCountDelta (as 1) to onChange, and
update the parent component to merge this delta against the latest item state
rather than replacing the full object. This ensures concurrent updates to other
fields like vote or status are not lost.
In `@app/src/components/feedback/FeedbackSubmitForm.tsx`:
- Around line 127-145: The title input and body textarea fields in the
FeedbackSubmitForm component currently lack explicit labels and rely only on
placeholders for accessibility. Add an id attribute to both the input element
(for the title) and the textarea element (for the body), then wrap each with a
label element that uses htmlFor to reference the corresponding id, or
alternatively add aria-label attributes to each field. Use the existing
translation keys (feedback.submit.titlePlaceholder and
feedback.submit.bodyPlaceholder) as a reference for creating descriptive labels
that make the fields accessible to screen readers and maintain clarity when
users start typing.
In `@app/src/lib/i18n/ko.ts`:
- Around line 36-37: The 'feedback.submit.rejected' key in the Korean
translations file currently has a message that sounds like a generic technical
error, making it indistinguishable from 'feedback.submit.error'. Update the
Korean translation for 'feedback.submit.rejected' to explicitly indicate that
the feedback was rejected due to moderation or policy reasons rather than a
technical failure, ensuring the message clearly differentiates a rejection
decision from a system error.
- Around line 10-13: The feedback.loadMore and feedback.expand keys in the
Korean translation file (ko.ts) both use the same text "더 보기", which makes
pagination and item expansion indistinguishable to users. Change the
feedback.expand key to use a distinct Korean translation that clearly indicates
expanding or showing details of an item, while keeping feedback.loadMore as is
for pagination purposes.
In `@app/src/lib/i18n/pl.ts`:
- Line 15: In the Polish translation file, change the value for the
feedback.comments key from the genitive plural form komentarzy to the nominative
plural form Komentarze with a capital K. The current genitive form reads like a
count suffix rather than a standalone section label, so use the nominative
plural form which is more appropriate for display as a section heading on its
own.
---
Nitpick comments:
In `@app/src/pages/Feedback.tsx`:
- Line 114: Replace the inline object type annotation in the handleAccepted
function parameter with a shared interface type from the feedback types module.
Instead of defining the type inline as `{ feedback: FeedbackItem | null }`,
import and use the `CreateFeedbackResult` interface (or create a derived type
using `Pick` if only specific properties are needed) from
`app/src/types/feedback` to ensure consistency with the API contract and follow
the TypeScript coding guidelines that require named interfaces for object
shapes.
🪄 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: 3e3d54b5-9974-47a4-9045-59f18f5e2153
📒 Files selected for processing (36)
app/src/AppRoutes.tsxapp/src/components/feedback/FeedbackAdminMenu.test.tsxapp/src/components/feedback/FeedbackAdminMenu.tsxapp/src/components/feedback/FeedbackComments.test.tsxapp/src/components/feedback/FeedbackComments.tsxapp/src/components/feedback/FeedbackFilterSelect.test.tsxapp/src/components/feedback/FeedbackFilterSelect.tsxapp/src/components/feedback/FeedbackItemRow.test.tsxapp/src/components/feedback/FeedbackItemRow.tsxapp/src/components/feedback/FeedbackStatusBadge.tsxapp/src/components/feedback/FeedbackSubmitForm.test.tsxapp/src/components/feedback/FeedbackSubmitForm.tsxapp/src/components/feedback/FeedbackVoteControl.test.tsxapp/src/components/feedback/FeedbackVoteControl.tsxapp/src/components/layout/shell/navIcons.tsxapp/src/config/__tests__/navConfig.test.tsapp/src/config/navConfig.tsapp/src/lib/i18n/ar.tsapp/src/lib/i18n/bn.tsapp/src/lib/i18n/de.tsapp/src/lib/i18n/en.tsapp/src/lib/i18n/es.tsapp/src/lib/i18n/fr.tsapp/src/lib/i18n/hi.tsapp/src/lib/i18n/id.tsapp/src/lib/i18n/it.tsapp/src/lib/i18n/ko.tsapp/src/lib/i18n/pl.tsapp/src/lib/i18n/pt.tsapp/src/lib/i18n/ru.tsapp/src/lib/i18n/zh-CN.tsapp/src/pages/Feedback.test.tsxapp/src/pages/Feedback.tsxapp/src/services/api/feedbackApi.test.tsapp/src/services/api/feedbackApi.tsapp/src/types/feedback.ts
- Associate the admin status <label> with its <select> via htmlFor/id so the control has an accessible name. - Give the submit form's title/body fields programmatic sr-only labels instead of relying on placeholders alone. - Bubble a comment-count delta by id (onCommentAdded) and merge it against the latest row in the parent, instead of emitting a reconstructed item built from closed-over props that could clobber a concurrent vote/status change. - ko: distinguish loadMore/expand/collapse (all collided on "더 보기") and make the moderation-rejection message read as a rejection, not a generic error. - pl: use the nominative "Komentarze" for the standalone comments label. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sanil-23
left a comment
There was a problem hiding this comment.
LGTM ✅ — approving.
Strong, self-contained feature. The riskiest part — optimistic vote ↔ server reconciliation, filter-aware reload, and the comment-delta-by-id merge that avoids clobbering concurrent edits — is carefully handled and genuinely tested. Architecture (HTTP isolated in feedbackApi.ts, presentation-only components), i18n parity across all 14 locales, a11y basics, and path-encoding/injection safety all check out.
Leaving a few non-blocking nits inline — none gate the merge:
- Error banner + empty state can render together on load failure.
expandedconflates body-expand with mounting the comments fetch.- Custom filter dropdown advertises listbox ARIA without arrow-key nav.
One app-wide observation (not for this PR): apiClient throws plain {success,error} objects, not Error, so the instanceof Error branches always fall back to the generic message and the backend's specific error string never surfaces. This matches the existing Invites/Rewards pattern, so it's fine to leave as-is here.
| </svg> | ||
| </div> | ||
| <p className="text-sm text-neutral-500 dark:text-neutral-400"> | ||
| {t('feedback.empty')} |
There was a problem hiding this comment.
Non-blocking: on a failed load, finally sets isLoading=false and items stays [], so this empty state renders alongside the error banner above — the user sees both the red error and "No feedback yet. Be the first to share an idea."
Suggest gating the empty branch on !loadError:
) : loadError ? null : (
/* empty state */
)There was a problem hiding this comment.
Fixed in 9eb4cc8. Gated the empty-state branch on the error: ... : items.length > 0 ? (list) : loadError ? null : (empty state), so on a failed load only the error banner shows. Added a test asserting the "No feedback yet" copy is not in the document when the load rejects.
| </div> | ||
|
|
||
| {expanded && ( | ||
| <FeedbackComments feedbackId={item.id} onCommentAdded={() => onCommentAdded?.(item.id)} /> |
There was a problem hiding this comment.
Non-blocking: the "N comments" button (L96) and the "Show more/Show less" button (L110) both toggle the same expanded flag, which un-clamps the body and mounts <FeedbackComments> (firing a getFeedback request). So expanding just to read the full description triggers an unexpected comments fetch, and clicking "comments" also expands the body. Consider separate expanded/showComments flags if these are meant to be independent.
There was a problem hiding this comment.
Good observation — and you're right that expanding to read a long body also mounts FeedbackComments and fires a getFeedback request. The single expanded flag is currently intentional (expanding an item reveals its full description and its discussion together), so I've left the behaviour as-is here rather than change the interaction model in this PR. If we want the "N comments" control and "Show more" to be independent (and avoid the comments fetch when only reading the body), splitting into expanded/showComments is the right move — happy to do that as a follow-up if you'd prefer it.
|
|
||
| {open && ( | ||
| <ul | ||
| role="listbox" |
There was a problem hiding this comment.
Non-blocking a11y: this advertises aria-haspopup="listbox" + role="listbox"/role="option", which implies Up/Down/Home/End option navigation, but only Escape + outside-click are handled. Options are still Tab-reachable (they're <button>s) so it works, but the ARIA contract isn't fully met — either add roving arrow-key handling or drop to aria-haspopup="menu" semantics.
There was a problem hiding this comment.
Fixed in 9eb4cc8. Reworked FeedbackFilterSelect to honour the listbox contract: the open popover is focusable and takes focus, a single keydown handler implements Arrow Up/Down + Home/End to move a highlighted option (tracked via aria-activedescendant), Enter/Space selects, and the trigger opens with Arrow keys (Escape + outside-click already dismissed). Keeping focus on the listbox instead of roving it across the option buttons means the keys work regardless of whether it was opened by mouse or keyboard. Covered by new tests.
Addresses review feedback on the board: - Feedback.tsx: gate the "No feedback yet" empty state on `!loadError` so it no longer renders alongside the error banner when a load fails. - FeedbackFilterSelect: honour the listbox ARIA contract it advertises. The open popover now takes focus and tracks a highlighted option via aria-activedescendant, with Up/Down/Home/End to move, Enter/Space to select, and Escape + outside-click to dismiss; the trigger opens with Arrow keys. Keeping focus on the listbox (rather than roving it across option buttons) means every keystroke lands on one handler regardless of how it was opened. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9eb4cc8
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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 `@app/src/components/feedback/FeedbackFilterSelect.test.tsx`:
- Around line 77-96: The test name for the keyboard navigation test in
FeedbackFilterSelect.test.tsx claims to test ArrowUp and Home keys, but the
actual test implementation only exercises ArrowDown and End keys. Either add
fireEvent.keyDown calls for ArrowUp and Home keys with corresponding expect
statements to validate their behavior, or rename the test to accurately reflect
that it only tests ArrowDown, End, wrap-around behavior and Enter selection.
🪄 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: 8d12eae7-cea8-42d0-aa29-d40418f24597
📒 Files selected for processing (4)
app/src/components/feedback/FeedbackFilterSelect.test.tsxapp/src/components/feedback/FeedbackFilterSelect.tsxapp/src/pages/Feedback.test.tsxapp/src/pages/Feedback.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
- app/src/pages/Feedback.tsx
- app/src/pages/Feedback.test.tsx
- app/src/components/feedback/FeedbackFilterSelect.tsx
The keyboard-nav case was named for Up/Down/Home/End but only drove ArrowDown and End. Add the missing Home and ArrowUp assertions so coverage matches the name (per review feedback). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
/feedbackroute + sidebar nav entry (with icon); available to all authenticated users.backend-alphahuman's existing/feedbackAPI via the standardapiClient(openhuman's own JWT users — not the tiny.place wallet board, and not the Agent Worldtinyplace_*RPC bridge).createdByName/ commentuserName), gracefully falling back to a short@handlewhen absent.Problem
openhuman had no in-app way for users to submit feature requests / bug reports or to see and vote on others' feedback. We want the tiny.place feedback experience, but for openhuman's own user/identity model.
Solution
app/srcper the architecture rules: all HTTP lives inservices/api/feedbackApi.ts; components hold no business logic.useState/useEffect+ request-id-guard pattern (no react-query), matching Invites/Rewards.myVote.user.role === 'admin'client-side; the server'srequireAdminis the real boundary. Moving an item to "planned" triggers GitHub-issue creation server-side and surfaces a "View issue" link.FeedbackFilterSelect); sort is a segmented Hot/Top/New control.Submission Checklist
feedbackApi,FeedbackVoteControl(optimistic + rollback + retract),FeedbackComments,FeedbackFilterSelect,FeedbackItemRow(incl. admin gating + name fallback),Feedbackpage, andnavConfig.apiClient; no new deps.Impact
feat/feedback-author-name, addscreatedByName/userName). Until that ships, names fall back to@handle— no breakage.Related
tinyhumansai/backend#feat/feedback-author-name(should merge/deploy first so names render). Deferred: backend vote IP rate-limit;about_appcatalog entry.AI Authored PR Metadata (required for Codex/Linear PRs)
Linear Issue
Commit & Branch
feat/feedback-boardb7aeb11Validation Run
pnpm --filter openhuman-app format:checkpnpm typecheckvitest runoversrc/components/feedback,src/pages/Feedback.test.tsx,src/services/api/feedbackApi.test.ts,src/config/__tests__/navConfig.test.ts— all green.rust:check,rust:format:check) — no Rust changes in this PR.Validation Blocked
command:N/Aerror:N/Aimpact:N/ABehavior Changes
/feedbackboard.Parity Contract
Duplicate / Superseded PR Handling
Summary by CodeRabbit
Release Notes
/feedbackwith sort, type/status filtering, pagination, and “Load more”.