feat: implement soft-delete recovery#203
Conversation
|
Warning Review limit reached
More reviews will be available in 13 minutes. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
📝 WalkthroughWalkthroughAdds recover endpoints, tests, common webhook/cleanup utilities, and web-dashboard UI to restore soft-deleted documents by unsetting isDeleted/deletedAt; maps 400/404/409/500 errors via AppError and synchronizes cleanup jobs and webhook dispatch. ChangesDocument Recovery Feature
Sequence Diagram(s)sequenceDiagram
participant User
participant WebDashboard
participant PublicAPI
participant DashboardAPI
participant MongoDB
participant WebhookService
User->>WebDashboard: Click Recover on deleted record
WebDashboard->>PublicAPI: PATCH /:collectionName/:id/recover
PublicAPI->>MongoDB: findOneAndUpdate {_id,isDeleted:true,deletedAt>=threshold,rlsFilter?} -> {$set isDeleted:false,deletedAt:null}
MongoDB-->>PublicAPI: recovered document / null
PublicAPI->>WebhookService: dispatchWebhooks(action: "recover", documentId, options.bypassLimit?)
WebDashboard->>WebDashboard: update local state, clear isDeleted/deletedAt
WebDashboard-->>User: show success toast
alt dashboard scoped recovery
WebDashboard->>DashboardAPI: PATCH /:projectId/.../data/:id/recover
DashboardAPI->>MongoDB: findOne (project owner) + findOneAndUpdate restore filter
MongoDB-->>DashboardAPI: recovered document / null
DashboardAPI->>WebhookService: dispatchWebhooks(action: "recover", documentId, options.bypassLimit)
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 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. ✨ 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: 4
🧹 Nitpick comments (2)
apps/dashboard-api/src/__tests__/project.controller.softDelete.test.js (1)
108-172: ⚡ Quick winAdd a
recoverRowduplicate-key conflict test (409 path).The new recover tests cover success and not-found, but not the unique-conflict branch (
err.code === 11000). That path is part of this feature’s critical behavior and should be locked with a test.🤖 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 `@apps/dashboard-api/src/__tests__/project.controller.softDelete.test.js` around lines 108 - 172, Add a new test for recoverRow that simulates a duplicate-key conflict by having mockFindOneAndUpdate return a Promise rejection with an error object whose code === 11000; call recoverRow(req, res) with the same project/request setup used in the existing tests (use makeReq/makeRes, mockFindOne returning the project), and assert that res.status was called with 409 and res.json was called with the appropriate error message for duplicate key conflicts. Target the recoverRow function and the mockFindOneAndUpdate mock to trigger the err.code === 11000 branch.apps/public-api/src/__tests__/softDelete.test.js (1)
100-138: ⚡ Quick winExpand
recoverSingleDoctests for conflict and invalid-id branches.Please add coverage for:
- duplicate-key conflict (
err.code === 11000→ 409), and- invalid ID input handling (expected 400 once validation is added).
This will close the most likely regression gaps for the new endpoint.
🤖 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 `@apps/public-api/src/__tests__/softDelete.test.js` around lines 100 - 138, Add two tests for recoverSingleDoc: one that simulates a duplicate-key conflict by having mockFindOneAndUpdate return a rejected promise with an error object containing code: 11000 and assert the handler responds with status 409 and an appropriate error JSON; and another that simulates invalid ID input by setting req.params.id to an invalid value and having mockFindOneAndUpdate reject with a validation/CastError (e.g., { name: 'CastError' } or a validation error) and assert the handler responds with status 400 and the expected error JSON; use the existing helper functions makeReq, makeRes and the same mockFindOneAndUpdate to throw/reject to trigger the conflict and invalid-id branches in recoverSingleDoc.
🤖 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 `@apps/dashboard-api/src/controllers/project.controller.js`:
- Around line 993-1042: The recoverRow handler must validate the incoming id and
normalize all responses to the API envelope and use AppError for errors;
specifically, in recoverRow validate req.params.id (e.g., with
mongoose.Types.ObjectId.isValid or a helper) and return res.status(400).json({
success:false, data:{}, message: "Invalid id" }) instead of letting a cast error
occur, replace all direct error JSON returns (Project not found, Collection not
found, Document not found or not in trash) with the standard envelope
(res.status(...).json({ success:false, data:{}, message: "..."})), and in the
catch block wrap errors with AppError (map err.code === 11000 to a 409 AppError
with a safe conflict message) and never return raw err.message to the
client—return a generic message via the envelope and appropriate status code;
locate fixes around recoverRow, the Model.findOneAndUpdate call, and the catch
block to implement these changes.
In `@apps/public-api/src/controllers/data.controller.js`:
- Around line 627-681: recoverSingleDoc currently uses the raw id in
Model.findOneAndUpdate (can throw on invalid ObjectId) and returns
raw/non-enveloped errors; validate the id up-front (e.g. use
mongoose.Types.ObjectId.isValid or your isValidId helper) before calling
getConnection/Model and return a 400 AppError if invalid; replace direct
res.status(...).json(...) error responses (the 404s and the 500 and the
duplicate-key 409 branch) with AppError instances (or call next(new
AppError(..., statusCode))) so all error responses follow the { success, data,
message } envelope and never expose raw err.message from Mongo; keep the
duplicate-key handling but map it to an AppError with status 409 and a safe user
message and preserve the successful response envelope for the recovered
document.
In `@apps/web-dashboard/src/components/RecordList.jsx`:
- Around line 92-107: The icon-only buttons in RecordList.jsx lack stable
accessible names; update both button elements (the recover button inside the
record.isDeleted branch that calls onRecover(record._id) and the fallback
ArrowRight button) to include an explicit aria-label (keep the existing title
attribute), e.g., use a descriptive aria-label like "Recover record" for the
RotateCcw button (optionally append record id or date if needed for uniqueness)
and "Open record" for the ArrowRight button so assistive tech can identify the
actions; ensure these aria-label strings are meaningful and match the intent
provided by getDeletionTooltip/onRecover and the ArrowRight action.
In `@apps/web-dashboard/src/pages/Database.jsx`:
- Around line 170-171: The toast currently concatenates "Failed to restore
document: " with errMsg which can be undefined; update the restore error
handling (the errMsg calculation and the toast.error call in the restore
routine) to ensure a definite fallback string (e.g., "Unknown error" or an empty
string and omit the colon) before concatenation so the toast never shows
"undefined" — set errMsg = err.response?.data?.message ||
err.response?.data?.error || err.message || "Unknown error" (or conditionally
build the message) and then call toast.error with the safe message.
---
Nitpick comments:
In `@apps/dashboard-api/src/__tests__/project.controller.softDelete.test.js`:
- Around line 108-172: Add a new test for recoverRow that simulates a
duplicate-key conflict by having mockFindOneAndUpdate return a Promise rejection
with an error object whose code === 11000; call recoverRow(req, res) with the
same project/request setup used in the existing tests (use makeReq/makeRes,
mockFindOne returning the project), and assert that res.status was called with
409 and res.json was called with the appropriate error message for duplicate key
conflicts. Target the recoverRow function and the mockFindOneAndUpdate mock to
trigger the err.code === 11000 branch.
In `@apps/public-api/src/__tests__/softDelete.test.js`:
- Around line 100-138: Add two tests for recoverSingleDoc: one that simulates a
duplicate-key conflict by having mockFindOneAndUpdate return a rejected promise
with an error object containing code: 11000 and assert the handler responds with
status 409 and an appropriate error JSON; and another that simulates invalid ID
input by setting req.params.id to an invalid value and having
mockFindOneAndUpdate reject with a validation/CastError (e.g., { name:
'CastError' } or a validation error) and assert the handler responds with status
400 and the expected error JSON; use the existing helper functions makeReq,
makeRes and the same mockFindOneAndUpdate to throw/reject to trigger the
conflict and invalid-id branches in recoverSingleDoc.
🪄 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: b24f947c-a807-4a00-8edc-57bdb92951ce
📒 Files selected for processing (11)
apps/dashboard-api/src/__tests__/project.controller.softDelete.test.jsapps/dashboard-api/src/__tests__/routes.projects.storage.test.jsapps/dashboard-api/src/controllers/project.controller.jsapps/dashboard-api/src/routes/projects.jsapps/public-api/src/__tests__/softDelete.test.jsapps/public-api/src/controllers/data.controller.jsapps/public-api/src/routes/data.jsapps/web-dashboard/src/components/CollectionTable.jsxapps/web-dashboard/src/components/RecordList.jsxapps/web-dashboard/src/components/RowDetailDrawer.jsxapps/web-dashboard/src/pages/Database.jsx
There was a problem hiding this comment.
♻️ Duplicate comments (1)
apps/dashboard-api/src/controllers/project.controller.js (1)
1007-1013:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
recoverRowstill mixes direct error responses withAppErrorflowLine 1007, Line 1019, Line 1030, and Line 1057 return errors via
res.status(...).json(...)instead ofnext(new AppError(...)). Please route these branches throughAppErrortoo, to keep controller error handling consistent.Suggested patch
- if (!mongoose.isValidObjectId(id)) { - return res.status(400).json({ - success: false, - data: {}, - message: "Invalid id" - }); - } + if (!mongoose.isValidObjectId(id)) { + return next(new AppError(400, "Invalid id")); + } @@ - if (!project) { - return res.status(404).json({ - success: false, - data: {}, - message: "Project not found." - }); - } + if (!project) { + return next(new AppError(404, "Project not found.")); + } @@ - if (!collectionConfig) { - return res.status(404).json({ - success: false, - data: {}, - message: "Collection not found." - }); - } + if (!collectionConfig) { + return next(new AppError(404, "Collection not found.")); + } @@ - if (!result) { - return res.status(404).json({ - success: false, - data: {}, - message: "Document not found or not in trash." - }); - } + if (!result) { + return next(new AppError(404, "Document not found or not in trash.")); + }As per coding guidelines, "
**/src/controllers/**/*.{js,ts}: All API endpoints return:{ success: bool, data: {}, message: "" }. Use AppError class for errors — never raw throw, never expose MongoDB errors to client."Also applies to: 1019-1025, 1030-1036, 1057-1063
🤖 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 `@apps/dashboard-api/src/controllers/project.controller.js` around lines 1007 - 1013, The recoverRow controller currently returns errors directly with res.status(...).json(...); update each error branch in recoverRow (the invalid ObjectId check using mongoose.isValidObjectId(id), the "project not found" path, the "already deleted"/path checks, and the permission/unauthorized path) to call next(new AppError(message, statusCode)) instead of sending JSON directly, preserving the `{ success, data, message }` shape in the AppError message and status code; ensure AppError is imported/available in this module and replace the four direct-responses at the checks you see surrounding id validation, the not-found lookup, the deleted-state check, and the ownership/permission check with next(new AppError(...)).
🤖 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.
Duplicate comments:
In `@apps/dashboard-api/src/controllers/project.controller.js`:
- Around line 1007-1013: The recoverRow controller currently returns errors
directly with res.status(...).json(...); update each error branch in recoverRow
(the invalid ObjectId check using mongoose.isValidObjectId(id), the "project not
found" path, the "already deleted"/path checks, and the permission/unauthorized
path) to call next(new AppError(message, statusCode)) instead of sending JSON
directly, preserving the `{ success, data, message }` shape in the AppError
message and status code; ensure AppError is imported/available in this module
and replace the four direct-responses at the checks you see surrounding id
validation, the not-found lookup, the deleted-state check, and the
ownership/permission check with next(new AppError(...)).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2ac31648-a280-4760-836b-9d8c12a42e05
📒 Files selected for processing (6)
apps/dashboard-api/src/__tests__/project.controller.softDelete.test.jsapps/dashboard-api/src/controllers/project.controller.jsapps/public-api/src/__tests__/softDelete.test.jsapps/public-api/src/controllers/data.controller.jsapps/web-dashboard/src/components/RecordList.jsxapps/web-dashboard/src/pages/Database.jsx
|
Hey @yash-pouranik , could you please review the changes and on the frontend, I’ve updated the dashboard with recovery actions let me know if you need any adjustments.! |
|
Please show sequence diagram |
|
Tip For best results, initiate chat on the files or code changes. The PR description Image 1 shows the "posts" collection table with several records marked "DELETED" (rows 1, 2, 4, 5, 6) and a tooltip on row 4 reading "Deleted on: 19/05/2026, 12:01:53 am (24 days until permanent deletion)". Each deleted row has a restore (↺) icon in the ACTIONS column. The PR description Image 2 shows the same table after restoration, with a green success banner "Document restored successfully". Records 2, 4, and 7 (among others) no longer show the DELETED badge, while records 1, 5, and 6 still remain deleted.
sequenceDiagram
autonumber
actor User
participant UI as Web Dashboard<br/>(CollectionTable / RecordList)
participant DB_Page as Database.jsx<br/>(handleRecoverRecord)
box "`#1E64B4`" "Dashboard API"
participant Auth as authMiddleware
participant DCtrl as recoverRow<br/>Controller
end
box "`#1EA050`" "Public API"
participant PAuth as apiKey + RLS +<br/>writeAuth Middleware
participant PCtrl as recoverSingleDoc<br/>Controller
end
participant Mongoose as Mongoose Model
participant MongoDB as MongoDB
participant Webhook as Webhook Dispatcher
Note over User,MongoDB: ── Dashboard API path ──
User->>UI: Clicks Restore button on deleted row
UI->>DB_Page: onRecover(record._id)
DB_Page->>Auth: PATCH /api/:projectId/collections/:collectionName/data/:id/recover
Auth->>Auth: Verify JWT & session
Auth-->>DCtrl: Attach req.user
DCtrl->>DCtrl: Validate ObjectId format<br/>(400 if invalid)
DCtrl->>MongoDB: Project.findOne({ _id: projectId, owner: userId })
MongoDB-->>DCtrl: project doc (or null → 404)
DCtrl->>DCtrl: Locate collectionName in project schema<br/>(404 if missing)
DCtrl->>Mongoose: getConnection() + getCompiledModel()
DCtrl->>MongoDB: findOneAndUpdate<br/>({ _id: id, isDeleted: true },<br/>{ $set: { isDeleted: false, deletedAt: null } },<br/>{ new: true })
MongoDB-->>DCtrl: restored doc (null → 404)<br/>or duplicate key error → 409
DCtrl-->>DB_Page: 200 { success: true, data: restoredDoc }
DB_Page->>DB_Page: Optimistic UI update:<br/>clear isDeleted & deletedAt in local state
DB_Page-->>UI: Re-render row (badge removed, icons changed)
DB_Page-->>User: Toast "Document restored successfully"
Note over User,MongoDB: ── Public API path ──
User->>PAuth: PATCH /public/:collectionName/:id/recover<br/>(API key in header)
PAuth->>PAuth: Verify API key
PAuth->>PAuth: Check collection not blocked
PAuth->>PAuth: Enforce usage limit
PAuth->>PAuth: Resolve public auth context (RLS filter)
PAuth->>PAuth: Check write authorization
PAuth-->>PCtrl: Attach req.rlsFilter
PCtrl->>PCtrl: Validate ObjectId format<br/>(400 if invalid)
PCtrl->>PCtrl: Verify collection exists<br/>(404 if missing)
PCtrl->>Mongoose: getCompiledModel()
PCtrl->>MongoDB: findOneAndUpdate<br/>({ _id: id, isDeleted: true, ...rlsFilter },<br/>{ $set: { isDeleted: false, deletedAt: null } },<br/>{ new: true })
MongoDB-->>PCtrl: restored doc (null → 404)<br/>or duplicate key error (code 11000) → 409
PCtrl->>Webhook: dispatch "update" event with restored doc
PCtrl-->>User: 200 { success: true, data: restoredDoc,<br/>message: "Document recovered from trash" }
Key points from the diagram
|
|
reallly nice design |
|
Tip For best results, initiate chat on the files or code changes.
The design is clean! Here are a few things worth considering before merging: 1. Webhook inconsistency between the two APIs
Suggestion: Add a webhook dispatch call in 2. BullMQ cleanup job cancellation (from issue
|
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
apps/dashboard-api/src/controllers/project.controller.js (1)
944-969:⚠️ Potential issue | 🟠 Major | ⚡ Quick winValidate
idbefore querying indeleteRowto avoid false 500s.Line 944 reads
idbutdeleteRownever validates it. A malformed ID can throw a cast error and incorrectly return Line 993’s 500 instead of a 400 client error.Suggested patch
module.exports.deleteRow = async (req, res, next) => { try { const { projectId, collectionName, id } = req.params; + if (!mongoose.isValidObjectId(id)) { + return next(new AppError(400, "Invalid document ID format.")); + } const project = await Project.findOne({ _id: projectId, owner: req.user._id,As per coding guidelines, "
**/src/controllers/**/*.{js,ts}: All API endpoints return:{ success: bool, data: {}, message: "" }. Use AppError class for errors — never raw throw, never expose MongoDB errors to client."Also applies to: 991-994
🤖 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 `@apps/dashboard-api/src/controllers/project.controller.js` around lines 944 - 969, The deleteRow handler should validate the route param id before querying to avoid MongoDB cast errors: add an ObjectId validity check (e.g., using mongoose.Types.ObjectId.isValid) at the start of deleteRow (before calling getConnection/getCompiledModel and before Model.findOneAndUpdate) and, if invalid, call next(new AppError(400, "Invalid id parameter")) so the endpoint returns a 400 instead of a 500; ensure this validation is applied to the id used in the findOneAndUpdate call and follows the API response conventions for { success, data, message } and AppError usage.apps/web-dashboard/src/components/CollectionTable.jsx (1)
56-59:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winClamp deletion countdown to avoid negative “days remaining”.
daysRemainingcan go below 0, producing confusing tooltips. Clamp it at 0 before rendering.Suggested fix
const getDeletionTooltip = (deletedAt, now) => { if (!deletedAt || !now) return ""; - const daysRemaining = 30 - Math.floor((now - new Date(deletedAt).getTime()) / (1000 * 60 * 60 * 24)); + const elapsedDays = Math.floor((now - new Date(deletedAt).getTime()) / (1000 * 60 * 60 * 24)); + const daysRemaining = Math.max(0, 30 - elapsedDays); return `Deleted on: ${formatDate(deletedAt)} (${daysRemaining} days until permanent deletion)`; };🤖 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 `@apps/web-dashboard/src/components/CollectionTable.jsx` around lines 56 - 59, The tooltip can show negative days because daysRemaining is not clamped; in getDeletionTooltip(deletedAt, now) compute daysRemaining as currently done and then clamp it to a minimum of 0 (e.g., daysRemaining = Math.max(0, daysRemaining)) before using it in the returned string that calls formatDate(deletedAt), so the tooltip never displays negative “days remaining.”
🤖 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.
Outside diff comments:
In `@apps/dashboard-api/src/controllers/project.controller.js`:
- Around line 944-969: The deleteRow handler should validate the route param id
before querying to avoid MongoDB cast errors: add an ObjectId validity check
(e.g., using mongoose.Types.ObjectId.isValid) at the start of deleteRow (before
calling getConnection/getCompiledModel and before Model.findOneAndUpdate) and,
if invalid, call next(new AppError(400, "Invalid id parameter")) so the endpoint
returns a 400 instead of a 500; ensure this validation is applied to the id used
in the findOneAndUpdate call and follows the API response conventions for {
success, data, message } and AppError usage.
In `@apps/web-dashboard/src/components/CollectionTable.jsx`:
- Around line 56-59: The tooltip can show negative days because daysRemaining is
not clamped; in getDeletionTooltip(deletedAt, now) compute daysRemaining as
currently done and then clamp it to a minimum of 0 (e.g., daysRemaining =
Math.max(0, daysRemaining)) before using it in the returned string that calls
formatDate(deletedAt), so the tooltip never displays negative “days remaining.”
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 6673a33e-925d-4e90-b6a0-f50faef4f237
📒 Files selected for processing (19)
apps/dashboard-api/src/__tests__/project.controller.softDelete.test.jsapps/dashboard-api/src/controllers/project.controller.jsapps/public-api/src/__tests__/softDelete.test.jsapps/public-api/src/__tests__/storage.controller.test.jsapps/public-api/src/controllers/data.controller.jsapps/public-api/src/controllers/storage.controller.jsapps/public-api/src/middlewares/api_usage.jsapps/public-api/src/middlewares/usageGate.jsapps/public-api/src/utils/webhookDispatcher.jsapps/web-dashboard/src/components/CollectionTable.jsxapps/web-dashboard/src/components/RecordList.jsxapps/web-dashboard/src/index.cssapps/web-dashboard/src/pages/Database.jsxpackages/common/src/index.jspackages/common/src/models/Webhook.jspackages/common/src/queues/trashCleanupQueue.jspackages/common/src/utils/input.validation.jspackages/common/src/utils/usageCounter.jspackages/common/src/utils/webhookDispatcher.js
💤 Files with no reviewable changes (1)
- apps/public-api/src/utils/webhookDispatcher.js
✅ Files skipped from review due to trivial changes (2)
- apps/public-api/src/tests/storage.controller.test.js
- packages/common/src/utils/input.validation.js
|
Hey @yash-pouranik, could you please review the changes, here is a breakdown of the core improvements in this commit
|
|
@MdTowfikomer
ELse I checked and well implemented. |
|
@yash-pouranik ,recover is added in
|
|
OKay got it |
|
Sure, take you time..👍 |
🚀 Pull Request Description
Fixes #193
This PR implements the recovery (undelete) functionality for soft-deleted documents, completing the lifecycle initiated in PR #185. Users can now restore documents from the "trash" state back to "active" directly from the dashboard or via the public API.
Key Changes:
🛠️ Type of Change
🧪 Testing & Validation
Backend Verification:
via Thunder Client.
status code.
worker.
Frontend Verification:
the UI state without a full page reload.
view).
📸 Screenshots
✅ Checklist
Built with ❤️ for urBackend.
Summary by CodeRabbit
New Features
UI
Tests
Documentation