web/admin: SPA Messages tab + Purge button + DLQ chips (Phase 5)#798
Conversation
|
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 (3)
📝 WalkthroughWalkthroughThe PR adds message browsing and Dead Letter Queue display to the SQS queue admin page. The backend purge endpoint's 429 rate-limit response changes its JSON field key from ChangesSQS Queue Admin - Message Browsing & DLQ Display
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
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 |
|
@claude review |
|
Claude finished @bootjp's task in 4m 21s —— View job Code Review — PR #798: web/admin: SPA Messages tab + Purge button + DLQ chips
Overall the PR is well-structured. The eager full-body fetch design is sound, the Bugs1. The } catch (err) {
if (err instanceof DOMException && err.name === "AbortError") return; // ← early return
setError(formatApiError(err));
} finally {
setLoading(false); // ← still runs after the return above
}When the component unmounts, the 2. if (err instanceof ApiError && err.code === "PurgeQueueInProgress") {
setPurgeRetryAfter(60); // ← always 60, never the actual remaining seconds
}The server returns the actual remaining cooldown in both the 3. Silent clipboard failure in void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});There is no Accuracy issue4. Truncated to {formatBytes(message.body.length)} of {formatBytes(message.body_original_size)}
Minor issues5. Post-purge After a successful purge, the in-flight post-purge fetch can't be cancelled if the user navigates away immediately. The initial load is covered by the 6. Multi-line comments in CLAUDE.md specifies: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The 7-line comment block added to
|
There was a problem hiding this comment.
Code Review
This pull request implements the Messages tab for SQS queues in the admin UI, enabling users to peek at and purge messages. Key changes include updating the backend error response format for consistency, adding DLQ-specific metadata to the queue summary, and introducing a new MessagesSection component with pagination and message detail views. Feedback suggests using optional chaining for the clipboard API to prevent errors in non-secure contexts and updating byte unit labels to KiB/MiB for better technical accuracy.
| void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => { | ||
| setCopied(true); | ||
| setTimeout(() => setCopied(false), 1500); | ||
| }); |
There was a problem hiding this comment.
The navigator.clipboard API is only available in secure contexts (HTTPS/localhost) and may be undefined in some environments. Accessing writeText directly will throw a TypeError if the API is unavailable. Using optional chaining provides a safe fallback.
| void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => { | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| }); | |
| void navigator.clipboard?.writeText(JSON.stringify(payload, null, 2))?.then(() => { | |
| setCopied(true); | |
| setTimeout(() => setCopied(false), 1500); | |
| }); |
| function formatBytes(n: number): string { | ||
| if (n < 1024) return `${n} B`; | ||
| if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`; | ||
| return `${(n / (1024 * 1024)).toFixed(1)} MB`; | ||
| } |
There was a problem hiding this comment.
The function uses base 1024 for calculations but labels the units as kB and MB. In a technical context, especially when dealing with SQS limits which are defined in kibibytes (KiB), it is more accurate to use KiB and MiB when dividing by 1024.
| function formatBytes(n: number): string { | |
| if (n < 1024) return `${n} B`; | |
| if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} kB`; | |
| return `${(n / (1024 * 1024)).toFixed(1)} MB`; | |
| } | |
| function formatBytes(n: number): string { | |
| if (n < 1024) return n + " B"; | |
| if (n < 1024 * 1024) return (n / 1024).toFixed(1) + " KiB"; | |
| return (n / (1024 * 1024)).toFixed(1) + " MiB"; | |
| } |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f206f1f9d3
ℹ️ 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".
| exported_at: new Date().toISOString(), | ||
| message, | ||
| }; | ||
| void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => { |
There was a problem hiding this comment.
Guard clipboard write behind feature and permission checks
Calling navigator.clipboard.writeText(...) unconditionally will throw (or reject) in non-secure contexts and some browser/privacy configurations where navigator.clipboard is unavailable, which is common for self-hosted admin UIs served over plain HTTP. In that case, clicking Copy as JSON raises an uncaught runtime error and the action silently fails; add a capability check and a rejection handler/fallback path so the modal remains usable.
Useful? React with 👍 / 👎.
Phase 5 of docs/design/2026_05_16_proposed_admin_purge_queue.md:
operator-facing UI for the AdminPeekQueue / AdminPurgeQueue HTTP
endpoints Phase 4 wired. The queue detail page now:
- Surfaces is_dlq with a pill in the header and a "DLQ for"
section listing source queues (each linked to its detail page).
- Renders a Messages section with a paginated table of currently-
visible messages: message-id prefix, sent timestamp, FIFO
MessageGroupId (column shown only on FIFO queues), receive
count, body preview, original size. Row-click opens a detail
modal showing the full body, every attribute, and a "Copy as
JSON" button that writes a schema-versioned payload to the
clipboard.
- Adds a Purge button labelled "Purge messages" or "Purge DLQ"
depending on is_dlq. Confirmation modal requires typing the
exact queue name; on 429 PurgeQueueInProgress the modal stays
open and shows the rate-limit message.
The eager full-body fetch (body_max_bytes = 262144 = 256 KiB, the
maximum SQS stored size) means the detail modal renders from the
row already in memory - no re-peek round-trip on modal open. This
eliminates the entire "row disappeared between list and modal"
class of failures (concurrent purge, ReceiveMessage by another
client, visibility timer started); see design doc §3.5. Page size
stays at the documented Phase 5 default of 20 rows, keeping the
worst-case response at 5 MiB.
## API client (web/admin/src/api/client.ts)
- SqsQueueSummary gains is_dlq + dlq_sources.
- New types: SqsPeekedAttribute, SqsPeekedMessage, SqsPeekResult,
SqsPeekOptions.
- New methods: api.peekQueue(name, opts, signal) and
api.purgeQueue(name). peekQueue is signal-aware so the SPA can
cancel an in-flight peek on navigation.
## Handler 429 envelope (internal/admin/sqs_handler.go)
writePurgeInProgress now uses the canonical "error" key (matching
writeJSONError) so the SPA's apiFetch wrapper extracts the AWS-
style sentinel into ApiError.code consistently with every other
4xx the admin surface returns. retry_after_seconds remains as an
extra field alongside the canonical Retry-After header. The
matching test assertion was updated.
## Caller audit (semantic-change rule)
The 429 JSON-body key change (code to error) is a wire-shape
adjustment with no other consumer yet: PR #797 introduced the
endpoint, no client has been built against the prior key. The
test was the only reader; updated.
## Verification
- cd web/admin && npm run lint (tsc -b --noEmit) passes
- cd web/admin && npm run build (vite build) passes
- go test -race ./internal/admin/... passes
- golangci-lint run ./internal/admin/... 0 issues
- The Vite build's hashed-name artifact in
internal/admin/dist/index.html is gitignored as a placeholder
by the existing .gitignore (lines 47-49); only the placeholder
HTML is committed to the repo.
Six items called out across Codex P2, Gemini medium x2, and Claude bot on PR #798: 1. Codex P2 / Gemini medium / Bot bug #3 - clipboard write_text fires in non-secure contexts. Add capability check (navigator.clipboard?.writeText) and a try/catch around the await; on failure fall back to window.prompt with the JSON pre-selected so the operator can Ctrl/Cmd+C it manually. Surface the failure in a text-danger line so the action no longer silently fails. 2. Gemini medium - formatBytes used base 1024 but labelled kB/MB. Use KiB/MiB. (kB/MB are decimal SI units; the code multiplies by 1024.) 3. Bot accuracy #4 - the truncation display used message.body.length which is JavaScript's UTF-16 code-unit count, not bytes. The server applies BodyMaxBytes against raw UTF-8 so non-ASCII bodies (CJK, emoji) under-reported. Add utf8ByteLength(s) using TextEncoder and use it in the display. 4. Bot bug #2 - purgeRetryAfter was hardcoded to 60 regardless of the actual remaining cooldown. formatApiError already includes the server's "only one PurgeQueue per 60 seconds" message, so removed the state entirely. 5. Bot bug #1 - setLoading(false) in `finally` ran even when `catch` returned early on AbortError. Move setLoading out of finally into the explicit success and non-abort error branches so unmounted components don't get the state update. 6. Bot minor #6 - distilled the multi-line comment blocks on writePurgeInProgress, SqsPeekedAttribute, SqsPeekedMessage, SqsPeekResult, peekQueue, and purgeQueue to single-line summaries per CLAUDE.md conventions. Caller audit (semantic-change rule): no signature changes; the clipboard / formatBytes / utf8ByteLength helpers are private to SqsDetail.tsx; purgeRetryAfter removal touches only the same file. Verification: - cd web/admin && npm run lint passes - go test -race ./internal/admin/... passes - golangci-lint run ./internal/admin/... 0 issues Rebased onto main (Phase 4 PR #797 merged).
f206f1f to
97ad686
Compare
|
@claude review Addressed r1 in 97ad686 + rebased onto main (Phase 4 #797 merged):
Verification: npm run lint (tsc) PASS, go test -race ./internal/admin/... PASS, golangci-lint 0 issues. |
|
Claude finished @bootjp's task in 4m 1s —— View job Code Review — PR #798 (r2)
|
| r1 item | Status |
|---|---|
Bug #1 — setLoading in finally fires on AbortError |
✅ Moved out of finally; only called in success and non-abort error paths. Comment at line 225 confirms intent. |
Bug #2 — hardcoded purgeRetryAfter=60 |
✅ State removed entirely; formatApiError(err) surfaces the server's message verbatim. |
| Bug #3 — silent clipboard failure | ✅ Capability check + try/catch + setCopyError + window.prompt fallback. Error renders below the button. |
Accuracy #4 — body.length is UTF-16 units |
✅ utf8ByteLength(s) helper with TextEncoder added; fallback to s.length for legacy contexts. |
| Gemini medium — KiB/MiB labels | ✅ formatBytes now emits KiB/MiB (was kB/MB). |
Minor #5 — post-purge fetchPage(undefined) has no AbortController |
Not fixed. The low-risk assessment stands; flagging for tracking only. |
Three new findings below.
Bug (new)
window.prompt fallback will be visually confusing and may truncate large bodies — SqsDetail.tsx:460–463
The clipboard fallback path:
- Sets
copyError(informing the user to use the prompt) - Immediately calls
window.prompt(…, json)(blocking — the React state update doesn't render until after the prompt closes)
The user sees the prompt before the error message, so the setCopyError instruction "Use the prompt to copy manually" appears only after they dismiss the prompt. Worse, the pre-filled value is the full JSON — up to ~350 KiB with a 256 KiB body plus metadata. Chrome/Firefox technically support large window.prompt inputs, but in practice many browsers silently truncate the default value in their internal dialog code, and users navigating a 350 KiB <input> to Ctrl+A+C is fragile.
Simplest safe fix: show the error message, skip window.prompt, and have the user copy from the pre-existing <pre> body already rendered in the modal. For situations where even the body is truncated (body_truncated: true), note that in the error.
// Instead of window.prompt fallback, just show the error;
// the body is already visible in the modal's <pre> block.
setCopyError("Clipboard API unavailable. Select and copy the body text above manually.");Accuracy (new)
Binary attribute display shows base64-string length, not decoded byte count — SqsDetail.tsx:512
{v.string_value ?? (v.binary_value ? `<binary, base64 length=${v.binary_value.length}>` : "")}v.binary_value.length is the number of base64 characters, which is ~4/3× the decoded byte count. An operator reading base64 length=1364 thinks "1364 bytes" but the actual value is ~1023 bytes. Consider either labelling it clearly (base64-encoded, ${v.binary_value.length} chars) or computing the decoded byte count (Math.floor(v.binary_value.length * 3 / 4) bytes, adjusting for padding). Fix this →
Minor (CLAUDE.md)
Multi-line comment blocks still present after claimed fix
The trigger comment says "Bot minor #6 (multi-line comments): distilled to single-line summaries per CLAUDE.md." However, the following blocks in the final commit still span multiple lines against the CLAUDE.md rule ("one short line max"):
sqs_handler_test.go:350–352— 3-line comment on thebody["error"]assertionsqs_handler.go:43–46, 48–53— 4- and 5-line field comments onIsDLQ/DLQSourcesSqsDetail.tsx:11–14, 17–23— 4- and 7-line constant commentsSqsDetail.tsx:195–199, 225–227, 256–262, 441–444, 458–460— 3–5 line inline blocksclient.ts:213–216, 218–220, 227–228, 354–355, 365–366— 2–4 line field/method comments
The writePurgeInProgress Go comment was condensed from 7 lines to 3, which is progress but still not "one short line."
This won't block the PR (it's a style rule, not a correctness issue), but the go-side struct-field comments in sqs_handler.go are especially easy to condense since the field names are self-describing.
Everything else looks good
is_dlq/dlq_sourcesfully wired inmain_admin.go:217–221, 340–341andadapter/sqs_admin.go; the TypeScript required field is safe sinceIsDLQ boolhas noomitemptytag so Go always emits it.cursorstate semantics (tracks last-requested cursor, not server-returnednext_cursor) are correct; "First page" disabled state and "Refresh" semantics are consistent.fetchPageuseCallbackdependency array[queue]is correct; only re-creates when the queue name changes (i.e., navigation to a different queue detail page).- Purge confirm button
disabled={purging || purgeName !== queue}guards correctly. previewBodyusing.lengthfor the 96-char display truncation is appropriate — that's a character count for visual layout, not a byte count.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 97ad686ab3
ℹ️ 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".
| setResult(res); | ||
| setCursor(pageCursor); | ||
| setLoading(false); |
There was a problem hiding this comment.
Ignore stale peek responses before updating state
fetchPage writes setResult/setCursor for every completed request, but only the initial load is abort-wired; button-triggered requests run without cancellation or request-version checks. If an operator starts a page fetch and then quickly navigates to another queue (same component instance via route param change) or triggers another page fetch, an older response can resolve last and overwrite the newer state, causing the Messages table to show stale/wrong queue data until another refresh.
Useful? React with 👍 / 👎.
… length) Three r2 items on PR #798: 1. Codex P2 (peek stale-response race): button-triggered peeks had no abort wiring, so a fast page-change could land the older response after the newer one and overwrite the Messages table with stale data. Add pendingAbortRef (AbortController per call): each fetchPage aborts the previous one and discards its result. 2. Bot bug (clipboard window.prompt fallback): the prompt was blocking and pre-empted setCopyError rendering, plus could truncate ~350KiB JSON in some browsers. Drop the prompt; point the operator at the body <pre> already rendered in the modal. 3. Bot accuracy (binary attribute length): displayed base64 char count instead of decoded bytes (~4/3 inflation). Add base64DecodedByteLength(b64) using padded-length arithmetic (no actual decode) and show '<binary, N bytes>'. Plus: shorten the IsDLQ / DLQSources docstrings on QueueSummary per CLAUDE.md (the bot flagged them in the comment-style list). Caller audit: no signature changes. pendingAbortRef and the new base64DecodedByteLength helper are private to SqsDetail.tsx.
|
@claude review Addressed r2 in 2397ca9:
|
|
Claude finished @bootjp's task in 3m 46s —— View job Code Review — PR #798 (r3)
All four substantive r2 items verified:
The abort logic is sound. After
Minor (CLAUDE.md style — no behaviour impact)The r3 commit condensed the Go struct field comments to single lines as stated, but two new multi-line comment blocks were added in the same commit:
And None of these block merge; the logic in all three cases is clear without the extra lines. Worth a follow-up pass if the CLAUDE.md limit is a firm rule. Everything else looks good
Verdict: All r2 correctness and accuracy issues are resolved. PR is ready to merge on this dimension. The remaining multi-line comment occurrences (including two introduced in this commit) are style-only; they won't block anything but are worth a quick squash-cleanup pass if you'd prefer the diff to be consistent with the stated goal. |
Claude r3 noted that the r2 commit left three multi-line comment blocks behind (and added two new ones) despite claiming to address the CLAUDE.md style rule. Squash-cleanup pass: - pendingAbortRef: 4 lines to 1 - base64DecodedByteLength: 4 lines to 1 - is_dlq / dlq_sources in client.ts SqsQueueSummary: 4 + 3 to 1 + 1 No behaviour change. Verdict from the bot was 'PR is ready to merge'; this is the style follow-up.
|
Claude finished @bootjp's task in 2m 4s —— View job Code Review — PR #798 (r4)
All three items specifically flagged in the r3 review are verified fixed:
No new multi-line comment blocks were introduced in 3a023a1. The diff is exactly the four items the commit message claims: The remaining multi-line comment blocks in the file ( PR is ready to merge. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3a023a1b60
ℹ️ 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".
| setError(formatApiError(err)); | ||
| setLoading(false); |
There was a problem hiding this comment.
Reset message table when peek request fails
When api.peekQueue rejects, this catch block sets an error but leaves the previous result state intact, and the render path still shows MessagesTable whenever result.messages.length > 0. If an operator switches from queue A to queue B and B's peek call fails (or any later refresh fails), the UI can display stale messages from A under queue B, which is misleading for troubleshooting and purge decisions. Clear result (and usually cursor) on failed loads or on queue change so failed fetches don't keep cross-queue data visible.
Useful? React with 👍 / 👎.
Codex r3 P2 on PR #798: when the peek request rejects, the catch block set error but left result intact. Combined with the queue param change re-running fetchPage, a failed fetch on the new queue could leave the prior queue's messages visible — misleading during troubleshooting / purge decisions. Two fixes: 1. useEffect now clears result + cursor when fetchPage changes (i.e., queue prop changed), so the brief loading window does not show cross-queue data. 2. catch path clears result + cursor before setting the error, so a failed refresh after a queue change does not leave the prior queue's table visible.
|
@claude review Addressed Codex r3 P2 in 0370c0c:
|
|
Claude finished @bootjp's task in 1m 27s —— View job Code Review — PR #798 (r4 follow-up)
Both Codex r3 P2 items verified ✅:
The implementation is correct on both counts. The abort-guard at line 231 ( One minor observation (non-blocking)Two new 2-line comments were introduced in 0370c0c: CLAUDE.md says "one short line max." These are 2-liners explaining non-obvious "why" — exactly the class CLAUDE.md intends to keep — so they're defensible. If you want strict compliance the each collapses cleanly to one line. No behaviour impact either way. Verdict: PR is ready to merge. All correctness and accuracy issues from r1/r2/r3 are resolved. The two Codex r3 P2 items from this round are correctly addressed. |
Phase 6 of the rollout plan: all milestones in docs/design/2026_05_16_*_admin_purge_queue.md have shipped on main: Phase 2 (#771) - AdminPurgeQueue + IsDLQ/DLQSources Phase 3 (#794) - AdminPeekQueue backend Phase 4 (#797) - HTTP handler + bridge Phase 5 (#798) - SPA Messages tab + Purge button + DLQ chips Rename the file via git mv per CLAUDE.md's lifecycle rule (*_proposed_*.md to *_implemented_*.md), bump Status, and add an Implementation history table so a future reader can trace each phase back to its PR. Out-of-scope follow-ups (throttle integration, audit + Prometheus counters, principalForReadSensitive live re-check, page-size selector) are listed in the doc but are tracked separately and do not gate this rename.

Summary
Phase 5 of
docs/design/2026_05_16_proposed_admin_purge_queue.md— operator-facing SPA UI for theAdminPeekQueue/AdminPurgeQueueendpoints PR #797 wired. The queue detail page now ships:RedrivePolicypoints at this queue, a pill in the header marks it as a DLQ and a section below lists the source queues (each linked to its detail page so the operator can navigate the topology).MessageGroupId(column shown only on FIFO queues), receive count, body preview, original size. Row-click opens a detail modal showing the full body, every attribute (typedString/Binary/Numberetc.), and a "Copy as JSON" button writing a schema-versioned payload to the clipboard.Purge messagesorPurge DLQdepending onis_dlq. Confirmation modal requires typing the exact queue name; on 429PurgeQueueInProgressthe modal stays open and surfaces the rate-limit message.Eager full-body fetch
The list call sets
body_max_bytes = 262144(= 256 KiB, SQS's stored-message hard cap) so the detail modal renders directly from the row already in memory — no re-peek round-trip on modal open. This eliminates the entire "row disappeared between list and modal" class of failures: concurrent purge, ReceiveMessage from another client, visibility timer started, leader step-down (design doc §3.5).Page size stays at the documented Phase 5 default of 20 rows. Worst case: 20 × 256 KiB = 5 MiB response, well under typical network and JSON-parse budgets.
API client changes (
web/admin/src/api/client.ts)SqsQueueSummarygainsis_dlq+ optionaldlq_sources.SqsPeekedAttribute,SqsPeekedMessage,SqsPeekResult,SqsPeekOptions.api.peekQueue(name, opts, signal)(signal-aware so navigation aborts the in-flight peek) andapi.purgeQueue(name).Handler 429 envelope (
internal/admin/sqs_handler.go)The Phase 4
writePurgeInProgresswas emitting the JSON body with acodekey, which diverged fromwriteJSONError's canonicalerrorkey — soapiFetch.tscould not extract the AWS-style sentinel intoApiError.codeconsistently with every other 4xx. Switched toerror(withretry_after_secondsstill in the body alongside the canonicalRetry-Afterheader). The matching unit test assertion was updated.Caller audit (semantic-change rule): the wire-shape adjustment has no other consumer yet — Phase 4 (#797) introduced the endpoint and the test was the sole reader. Updated.
Risk
Low. New SPA UI only; the handler change is a 1-key rename matching the existing admin error envelope.
Self-review (5 passes)
AdminPurgeQueuewrapper. No new write paths.DOMException name=AbortErroris filtered before setError to avoid stale-component warnings.npm run lint(tsc -b --noEmit) passes.npm run build(vite) passes. Go-sideTestSqsHandler_PurgeQueue_RateLimited429updated for theerror-key envelope. No frontend test infra in the project (intentional; thelintscript is the contract).Test plan
cd web/admin && npm run lint(tsc -b --noEmit) passescd web/admin && npm run build(vite build) passesgo test -race -count=1 ./internal/admin/...passes (12 existing + the 429 envelope update)golangci-lint run ./internal/admin/...0 issuesOut of scope (follow-ups)
bucketActionAdminPeek+resolveActionConfigexplicit case + typedadminPeekThrottledError/PeekThrottledError). Leader-only +Limit ≤ 100already bound the steady-state cost; the dedicated bucket adds a per-queue cap.admin.sqs.purge_queueaudit line +elastickv_sqs_admin_purge_queue_total{queue, outcome}/elastickv_sqs_admin_peek_queue_total{queue, outcome}.principalForReadSensitiveliveRoleStorere-check (design doc Goal 8) — blocked on the wider live-role plumbing._proposed_→_implemented_once Phase 6 lands (just the rename — Phases 2/3/4/5 are functionally complete).Summary by CodeRabbit
New Features
Updates