docs/design: propose admin web UI for DynamoDB/S3 item+object CRUD#801
Conversation
Extends the admin Web Console with item-level CRUD for DynamoDB and object-level CRUD for S3, mirroring the SQS Messages tab pattern PR #798 established. Scope per user discussion: - DynamoDB: basic CRUD only (Scan/Get/Put/Delete); no Query, GSI, conditional updates, or partial updates in this round. - S3: prefix + delimiter listings (pseudo-directory tree), Get (download), Put (upload, 100 MiB cap, raw octet-stream), Delete. - Read-side calls gated by AllowsRead; write-side by AllowsWrite, with the action-verb parameter introduced in the SQS Phase 4 fix carrying through. - Audit + Prometheus counters scoped to writes only (read paths poll-heavy). Six rollout phases; Phase 2 (backend RPCs) can split per adapter if review surface gets large.
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThis pull request introduces a comprehensive design document proposing DynamoDB item and S3 object CRUD operations for the elastickv admin Web Console, specifying backend RPC contracts, HTTP API shapes, security headers, SPA UI components, audit/metrics, failure modes, testing strategy, and rollout phases. ChangesAdmin Data Browser Design Document
Estimated code review effort🎯 1 (Trivial) | ⏱️ ~5 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
|
@claude review |
|
Claude finished @bootjp's task in 3m 50s —— View job Code Review
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52000c62ad
ℹ️ 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".
| | Endpoint | Method | Request body | Response body | | ||
| |----------|--------|--------------|---------------| | ||
| | `/dynamo/tables/{n}/items` | GET | — | `{items: AdminItem[], next_cursor?: string}` | | ||
| | `/dynamo/tables/{n}/items` | POST | `{key, attributes}` (single item) | 204 | |
There was a problem hiding this comment.
Reconcile Dynamo create endpoint method and shape
This table defines POST /dynamo/tables/{n}/items with {key, attributes}, but earlier sections define item writes as PUT /.../items/{key} and route /items as "scan / put". Leaving these conflicting contracts in one design doc makes the implementation/test plan ambiguous and can split backend and SPA work onto incompatible endpoints; please choose one write pattern and update all sections consistently.
Useful? React with 👍 / 👎.
|
|
||
| /admin/api/v1/s3/buckets → list buckets (existing) | ||
| /admin/api/v1/s3/buckets/{name} → describe / delete (existing) | ||
| /admin/api/v1/s3/buckets/{name}/objects → list / put (NEW) |
There was a problem hiding this comment.
Remove contradictory PUT from S3 collection route
The route summary says /s3/buckets/{name}/objects supports "list / put", but the endpoint list and body-shape table only define upload as PUT /s3/buckets/{name}/objects/{key}. This inconsistency can cause implementers to add an extra collection-level PUT handler that the SPA and tests don't use, so the route contract should be made consistent before phase work starts.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Code Review
This design document proposes extending the admin Web Console to support CRUD operations for DynamoDB items and S3 objects. Feedback highlights several critical areas: the operational risk of large Raft log entries with the 100 MiB S3 upload cap, the need for security headers to mitigate XSS risks when serving raw S3 objects, and inconsistencies in the proposed DynamoDB write API shapes. Additionally, suggestions were made to include content types in S3 metadata, clarify recursive structures for complex DynamoDB attributes, and address DynamoDB's 1 MiB response limit during scans.
|
|
||
| ## 4. Failure Modes | ||
|
|
||
| 1. **Concurrent writes.** Both backends already serialise writes through Raft; the admin RPCs use the same commit path. No new TOCTOU class. |
There was a problem hiding this comment.
The proposal for a 100 MiB upload cap for S3 objects (line 41) combined with the statement that writes are serialized through Raft (line 244) presents a significant operational risk. Standard Raft implementations are not designed to handle log entries of this size, as they can block the heartbeat mechanism, cause leader election instability, and lead to excessive memory pressure on all nodes. As this is a significant operational risk, the design document must detail potential mitigation strategies (e.g., implementing a chunked upload mechanism or a proxy mode) to ensure system stability.
References
- When a design document identifies a significant operational risk, such as the inability to perform rolling upgrades, it must also detail potential mitigation strategies, like implementing a temporary 'bridge' or 'proxy' mode.
| type AdminObject struct { | ||
| Key string | ||
| Size int64 | ||
| ETag string | ||
| LastModified time.Time | ||
| StorageClass string | ||
| } |
There was a problem hiding this comment.
The AdminObject struct should include the ContentType of the object. While line 184 mentions the Content-Type header in the HTTP response, the backend RPC needs to provide this metadata so the handler can correctly set the header.
| type AdminObject struct { | |
| Key string | |
| Size int64 | |
| ETag string | |
| LastModified time.Time | |
| StorageClass string | |
| } | |
| type AdminObject struct { | |
| Key string | |
| Size int64 | |
| ContentType string | |
| ETag string | |
| LastModified time.Time | |
| StorageClass string | |
| } |
| | `/dynamo/tables/{n}/items/{key}` | PUT | `{attributes}` | 204 | | ||
| | `/dynamo/tables/{n}/items/{key}` | DELETE | — | 204 | | ||
| | `/s3/buckets/{n}/objects` | GET | — | `{objects: AdminObject[], common_prefixes: string[], next_continuation_token?: string}` | | ||
| | `/s3/buckets/{n}/objects/{key}` | GET | — | raw octet-stream + `Content-Type` + `Last-Modified` + `ETag` headers | |
There was a problem hiding this comment.
Serving raw S3 objects directly via the admin API introduces a Cross-Site Scripting (XSS) risk if an operator downloads or views a malicious HTML/SVG file. The design should specify security headers for these responses, such as Content-Security-Policy: default-src 'none'; sandbox and X-Content-Type-Options: nosniff, to ensure the browser treats the content as a download or an isolated resource.
| AdminDeleteItem(ctx, principal, tableName, key) error | ||
| ``` | ||
|
|
||
| **Scan implementation.** Reuse the existing internal `Scan` machinery; the new admin call is a thin wrapper that: |
There was a problem hiding this comment.
The AdminScanTable implementation should clarify how it handles DynamoDB's 1 MiB response limit. A single Scan call to the underlying engine might return a LastEvaluatedKey before reaching the requested Limit (line 84). The admin RPC should ideally loop internally to fulfill the requested Limit or the design should explicitly state that the SPA must handle partial pages.
|
|
||
| type AdminAttributeValue struct { | ||
| Type string // "S", "N", "B", "BOOL", "NULL", "L", "M", "SS", "NS", "BS" | ||
| Value any // shape depends on Type; mirrors the DynamoDB wire shape |
There was a problem hiding this comment.
The AdminAttributeValue struct uses Value any. For complex types like "M" (Map) and "L" (List), the design should explicitly confirm that this structure is recursive (e.g., Value being map[string]AdminAttributeValue or []AdminAttributeValue). This is important for ensuring the SPA can correctly deserialize nested items.
| Value any // shape depends on Type; mirrors the DynamoDB wire shape | |
| Value any // shape depends on Type; recursive for "L" and "M" |
| | `/dynamo/tables/{n}/items` | POST | `{key, attributes}` (single item) | 204 | | ||
| | `/dynamo/tables/{n}/items/{key}` | GET | — | `AdminItem` | | ||
| | `/dynamo/tables/{n}/items/{key}` | PUT | `{attributes}` | 204 | |
There was a problem hiding this comment.
The request body shapes for DynamoDB writes appear inconsistent with the AdminPutItem RPC (line 94). The RPC takes an AdminItem, which is a flat map of attributes. However, the HTTP table suggests POST takes {key, attributes} and PUT takes {attributes}. Since DynamoDB items are inherently flat maps where key attributes are not structurally distinguished from other attributes, it would be simpler to have the HTTP body be the JSON representation of AdminItem (the full attribute map) for both methods.
… security headers Eight findings on PR #801 r1: - Codex P2 (POST /items vs PUT /items/{key}): dropped POST, keep only PUT-by-key; route summary updated to 'scan' / 'list' only on collection roots. Body-shape table now matches the RPC's AdminItem signature directly. - Codex P2 (collection-level PUT on S3 objects): removed; path routing now says 'list (NEW)' only for /objects collection root. - Gemini HIGH (100 MiB upload + Raft heartbeat instability): clarified — the admin path reuses the SigV4 chunked dispatch (s3ChunkSize=1 MiB × s3ChunkBatchOps=3 ~= 3 MiB Raft entries), well under MaxSizePerMsg=4 MiB. The 100 MiB cap is the admin UI's exposed limit, not Raft's capacity (SigV4 path supports 5 GiB on the same chunked write). - Gemini security-medium (XSS via raw object GET): added explicit defence-in-depth headers — CSP sandbox + nosniff + Disposition attachment + no-store. New §3.3.2 paragraph. - Gemini medium (AdminObject ContentType): added the field so the GET handler can set the response header. - Gemini medium (Scan 1 MiB response cap): explicit — admin does NOT loop internally on partial pages; SPA's Next-page button is the operator-driven refill. - Gemini medium (AdminAttributeValue recursive): annotated Value comment as recursive for L/M types. - Gemini medium (HTTP body shape vs AdminItem RPC): aligned — body is the flat AdminItem shape; key segment + body key are cross-checked before PutItem.
|
@claude review Addressed r1 in 01ee057:
|
|
Claude finished @bootjp's task in 2m 23s —— View job Claude finished @bootjp's task —— Code Review (r2)
r1 Codex and Gemini items are all correctly handled in 01ee057. Summary of each fix and one item that remains open from r1: Still Open: Critical #1 — Follower-node handling of streaming S3 operationsThis was the blocking finding in r1. It is still not addressed in the current doc. §3.1.2 says "The HTTP handler relays the stream byte-for-byte." §3.3.3 explains how writes are chunked through Raft. §4 item 2 notes "if a follower loses leadership while streaming a 100 MiB Get." None of these answer the original question:
The existing Three options exist; pick one and document it:
Recommendation (same as r1): Option 1 (307 redirect) for S3 Get/Put is the most idiomatic HTTP approach and requires zero new wire protocol work. Option 2 is the simplest to implement correctly. Either choice should be explicitly stated in §3.1.2 or a new §3.2.x before Phase 2 begins. r1 Items Confirmed Fixed
Remaining Minor Items (carry into implementation)These were flagged as minor polish in r1 and are still present; none blocks Phase 2. 6. Error format for S3 GET not in §3.3.2. Open Question 2 documents that non-2xx responses use 7. AdminObject.StorageClass expected value. The field is present and useful, but it is not stated whether the elastickv S3 adapter will always populate it as 8. §3.4.3 upload-progress wording. The sentence "Upload progress is reported via the Summary: The doc is substantially improved and all Codex / Gemini r1 items are correctly resolved. The one remaining blocker for Phase 2 is the streaming-forwarding question (how followers handle S3 Get/Put HTTP requests). Once that is documented — even one sentence in §3.1.2 — the design is ready to implementation. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 01ee05701b
ℹ️ 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".
|
|
||
| 1. **Concurrent writes.** Both backends already serialise writes through Raft; the admin RPCs use the same commit path (chunked at `s3ChunkSize = 1 MiB` × `s3ChunkBatchOps = 3` for S3 large objects — see §3.3.3). No new TOCTOU class. | ||
| 2. **Large object download mid-stream.** If a follower loses leadership while streaming a 100 MiB Get, the connection is closed by the leader-step-down path. The operator's browser shows a partial download error; safest re-try is from scratch (the partial bytes have an unknown integrity boundary). | ||
| 3. **Truncated upload.** `http.MaxBytesReader` cuts the stream at the cap; the leader receives a partial body and returns 413 without committing. No state change. |
There was a problem hiding this comment.
Correct truncated-upload state guarantee
This section states that an over-limit upload returns 413 “without committing” and causes “No state change,” but the existing S3 write path commits chunk batches before body-read errors are surfaced and then performs best-effort cleanup. If the admin upload reuses that path, a 100+ MiB request can still leave committed orphan chunk blobs when cleanup dispatch fails, so treating this as a guaranteed no-write outcome is inaccurate and can hide storage-leak behavior in tests/ops planning.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
docs/design/2026_05_22_proposed_admin_data_browser.md (1)
163-163: 💤 Low valueAdd language specifiers to fenced code blocks.
Markdown linter flags three fenced code blocks without language specifiers (lines 163, 241, 252). These blocks show text content (route patterns, audit formats), so either add
```textor omit the language entirely if your renderer supports it.📝 Example fix for line 163
-``` +```text /admin/api/v1/dynamo/tables → list tables (existing) ...</details> Also applies to: 241-241, 252-252 <details> <summary>🤖 Prompt for AI Agents</summary>Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.In
@docs/design/2026_05_22_proposed_admin_data_browser.mdat line 163, Add
language specifiers to the three fenced code blocks that currently lack them
(the blocks containing the route pattern starting with
"/admin/api/v1/dynamo/tables" and the two blocks showing audit/format examples)
by changing their opening fences to include a language hint such astext (ornone/plain if preferred by the renderer); ensure each fenced block still has a matching closingso the markdown linter no longer flags them.</details> </blockquote></details> </blockquote></details> <details> <summary>🤖 Prompt for all review comments with AI agents</summary>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@docs/design/2026_05_22_proposed_admin_data_browser.md:
- Around line 69-72: AdminAttributeValue is recursive (Type/Value for "L" and
"M") and needs a depth limit to avoid stack overflows on JSON unmarshal;
implement a custom deserialization that enforces a max recursion depth (suggest
- by adding a depth parameter (or context) to a helper like
unmarshalAdminAttributeValue(data []byte, depth int) and call it from
AdminAttributeValue.UnmarshalJSON, returning an error when depth exceeds the
limit; ensure the logic checks Type and only recurses into Value for "L" (parse
[]AdminAttributeValue) and "M" (parse map[string]AdminAttributeValue) so
malformed payloads cannot bypass the depth guard.
- Line 275: Update the phrasing in the "Integration (
internal/admin)" bullet
to reference the correct HTTP status for upload cap enforcement: replace the
phrase "429-shaped errors" with "413-shaped errors" or "upload cap enforcement
(413)"; look for the bullet containing "6-step path validation, table-level keys
with/and binary attributes" and adjust that clause accordingly.
Nitpick comments:
In@docs/design/2026_05_22_proposed_admin_data_browser.md:
- Line 163: Add language specifiers to the three fenced code blocks that
currently lack them (the blocks containing the route pattern starting with
"/admin/api/v1/dynamo/tables" and the two blocks showing audit/format examples)
by changing their opening fences to include a language hint such astext (ornone/plain if preferred by the renderer); ensure each fenced block still has a matching closingso the markdown linter no longer flags them.</details> <details> <summary>🪄 Autofix (Beta)</summary> Fix all unresolved CodeRabbit comments on this PR: - [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended) - [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes </details> --- <details> <summary>ℹ️ Review info</summary> <details> <summary>⚙️ Run configuration</summary> **Configuration used**: defaults **Review profile**: CHILL **Plan**: Pro **Run ID**: `35a6c1bb-64ee-4edf-bd9e-8e958e849e57` </details> <details> <summary>📥 Commits</summary> Reviewing files that changed from the base of the PR and between 6757f6a26e5957528a11a08176cf5d3cd046131e and 01ee05701b5833f8c68b21821ef89dc38335d65f. </details> <details> <summary>📒 Files selected for processing (1)</summary> * `docs/design/2026_05_22_proposed_admin_data_browser.md` </details> </details> <!-- This is an auto-generated comment by CodeRabbit for review status -->
| type AdminAttributeValue struct { | ||
| Type string // "S", "N", "B", "BOOL", "NULL", "L", "M", "SS", "NS", "BS" | ||
| Value any // recursive for "L" ([]AdminAttributeValue) and "M" (map[string]AdminAttributeValue); raw value for scalars | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Document depth limit requirement for recursive AdminAttributeValue.
The AdminAttributeValue type is recursive (Lists and Maps nest arbitrarily). JSON unmarshaling without depth limits can cause stack overflow from malicious payloads. Note that the implementation must enforce a reasonable depth limit (e.g., 32 levels) during deserialization.
🤖 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 `@docs/design/2026_05_22_proposed_admin_data_browser.md` around lines 69 - 72,
AdminAttributeValue is recursive (Type/Value for "L" and "M") and needs a depth
limit to avoid stack overflows on JSON unmarshal; implement a custom
deserialization that enforces a max recursion depth (suggest 32) by adding a
depth parameter (or context) to a helper like unmarshalAdminAttributeValue(data
[]byte, depth int) and call it from AdminAttributeValue.UnmarshalJSON, returning
an error when depth exceeds the limit; ensure the logic checks Type and only
recurses into Value for "L" (parse []AdminAttributeValue) and "M" (parse
map[string]AdminAttributeValue) so malformed payloads cannot bypass the depth
guard.
Codex r2 P2 on PR #801: the original §4 Failure Modes #3 stated that 413 returns 'without committing' and 'No state change'. Codex correctly pointed out that the SigV4 chunked write path commits chunk batches BEFORE the body-read error is surfaced, then performs best-effort cleanup via cleanupManifestBlobs. A coordinator dispatch failure during cleanup can leave orphan chunk blobs in storage. Reword the failure mode to spell out the actual contract: client-visible state (the object manifest) is unchanged but partial chunk blobs may need reaping by the per-generation bucket reaper.
|
@claude review Addressed Codex r2 P2 in 14af408:
CI failure on the prior commit was a Redis flake unrelated to the doc change; re-run in progress. |
|
Claude finished @bootjp's task in 1m 52s —— View job Code Review (r3)
Codex r2 P2 fix verified. §4 Failure Modes #3 is now accurate. The rewrite correctly distinguishes between client-visible state (manifest not committed → no object appears) and internal state (chunk blobs from earlier batch flushes may persist until the bucket reaper sweeps). The reference to Still Open: Critical — Follower-node handling of streaming S3 operationsThis was the blocking finding in r1 and r2 and remains unaddressed in 14af408. The current doc has two relevant passages:
Neither answers the original question: what does the HTTP handler do when a follower receives the request in the first place? §4 #2 is the mid-stream leadership-loss case (the request started on the leader, then it stepped down); it is silent on the request-arrives-on-follower case. The existing Pick one option and document it in §3.1.2 or a new §3.2.x:
The r1 and r2 recommendations both suggested Option 1 (307 for these paths) or Option 2 (503 + header) as the simplest choices. Either one closes the gap. A single sentence in §3.1.2 — "For S3 Get/Put, the handler checks Medium — §5 test plan: "429-shaped errors" should be 413Line 275 reads:
HTTP 429 is "Too Many Requests" (rate limiting). The upload cap returns 413 Payload Too Large (as correctly specified in §3.3.3). This is a copy-paste error from a rate-limiting pattern elsewhere. CodeRabbit flagged this in r2; it is still present. Medium —
|
| r2 Finding | Status |
|---|---|
| Codex P2: POST /items vs PUT /items/{key} | ✅ Fixed in 01ee057 |
| Codex P2: S3 collection "list / put" annotation | ✅ Fixed in 01ee057 |
| Gemini HIGH: 100 MiB + Raft entry size | ✅ Fixed in 01ee057 |
| Gemini security: XSS on S3 GET | ✅ Fixed in 01ee057 |
| Gemini: AdminObject.ContentType | ✅ Fixed in 01ee057 |
| Gemini: Scan 1 MiB response cap | ✅ Fixed in 01ee057 |
| Gemini: AdminAttributeValue recursive annotation | ✅ Fixed in 01ee057 |
| Gemini: HTTP body vs RPC key-in-path cross-check | ✅ Fixed in 01ee057 |
| Codex r2 P2: truncated-upload state guarantee | ✅ Fixed in 14af408 |
Carry-Forward Minor Items (polish, non-blocking)
These were flagged minor in r1/r2 and are still open; none blocks Phase 2:
- §3.3.2 S3 GET error format — body shapes table says "raw octet-stream" but does not document the
text/plainnon-2xx shape. Open Question 2 answers it; migrate the answer to a note in the table row before Phase 3 SPA work begins. - §3.1.2
AdminObject.StorageClass— still not stated whether the elastickv S3 adapter populates this as"STANDARD"or leaves it empty. SPA should not render an ambiguous null tiering column. Can resolve during Phase 5. - §3.4.3 upload-progress wording — the sentence about
fetch+XMLHttpRequest-fallback is technically incorrect (fetchhas no upload progress API in any shipping browser). The MVP note ("does not show a progress bar") is sufficient; the forward-looking sentence should be dropped.
Summary: The Codex r2 P2 fix (truncated upload) is correct and well-sourced against the actual cleanupManifestBlobs call sites. The doc is ready to merge once the follower-streaming question (critical, third time flagged) is answered with one sentence in §3.1.2, and the 429 → 413 typo in §5 is corrected. The AdminAttributeValue depth-limit note is worth adding now so Phase 2 implementers don't miss it.

Summary
Design proposal for extending the admin Web Console with item-level CRUD on DynamoDB and object-level CRUD on S3, mirroring the SQS Messages-tab pattern that landed in PRs #797 / #798. Per CLAUDE.md's design-doc-first workflow, this PR is the doc only — implementation lands in follow-up PRs after the design is reviewed.
Scope (per user discussion)
Scan,GetItem,PutItem,DeleteItem. No Query / GSI / conditional updates / partial updates.prefix+delimiter(renders as pseudo-directory tree in the SPA),GetObject(download),PutObject(upload, 100 MiB cap, raw octet-stream),DeleteObject.Authorization
principalForReadSensitive(read role, payload is sensitive)principalForWrite(write role, with the per-action verb parameter introduced in the SQS Phase 4 fix)Roll-out (six phases)
Admin{Scan/Get/Put/Delete}{Item,Object}RPCs + tests (can split 2a/2b per adapter)_proposed_→_implemented_Risk
None. Doc-only PR.
Out of scope (explicit non-goals)
Test plan
AdminListBuckets,AdminListTables,AdminListQueues)/items/{key}and/objects/{key}sub-resourcesNote on stacking
PR #800 (Phase 6 rename of the SQS admin-purge-queue doc) is unrelated and can land in either order.
Summary by CodeRabbit