Skip to content

web/admin: SPA Messages tab + Purge button + DLQ chips (Phase 5)#798

Merged
bootjp merged 5 commits into
mainfrom
feat/sqs-admin-peek-purge-spa
May 21, 2026
Merged

web/admin: SPA Messages tab + Purge button + DLQ chips (Phase 5)#798
bootjp merged 5 commits into
mainfrom
feat/sqs-admin-peek-purge-spa

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented May 21, 2026

Summary

Phase 5 of docs/design/2026_05_16_proposed_admin_purge_queue.md — operator-facing SPA UI for the AdminPeekQueue / AdminPurgeQueue endpoints PR #797 wired. The queue detail page now ships:

  • DLQ chip + "DLQ for" sources list — when another queue's RedrivePolicy points 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).
  • Messages section — 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 (typed String / Binary / Number etc.), and a "Copy as JSON" button writing a schema-versioned payload to the clipboard.
  • 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 surfaces the rate-limit message.

⚠️ Stacked on PR #797. This branch builds on feat/sqs-admin-peek-purge-http because the SPA needs the Phase 4 HTTP endpoints. Once #797 merges to main, please rebase (or merge #797 first then this auto-fast-forwards). I can rebase on request.

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)

  • SqsQueueSummary gains is_dlq + optional dlq_sources.
  • New types: SqsPeekedAttribute, SqsPeekedMessage, SqsPeekResult, SqsPeekOptions.
  • New methods: api.peekQueue(name, opts, signal) (signal-aware so navigation aborts the in-flight peek) and api.purgeQueue(name).

Handler 429 envelope (internal/admin/sqs_handler.go)

The Phase 4 writePurgeInProgress was emitting the JSON body with a code key, which diverged from writeJSONError's canonical error key — so apiFetch.ts could not extract the AWS-style sentinel into ApiError.code consistently with every other 4xx. Switched to error (with retry_after_seconds still in the body alongside the canonical Retry-After header). 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)

  1. Data loss — read endpoint (peek) plus the existing AdminPurgeQueue wrapper. No new write paths.
  2. Concurrency — eager full-body fetch eliminates the modal re-peek race. AbortController cancels in-flight peek on navigation / unmount; DOMException name=AbortError is filtered before setError to avoid stale-component warnings.
  3. Performance — page size capped at 20 (default; server clamps to ≤ 100). Body cap 256 KiB. No hot-path allocations.
  4. Consistency — Purge button reuses the queue's name as the confirmation token, matching the existing Delete confirmation. The 429 response shape now matches every other admin 4xx so the SPA error handling is uniform.
  5. Test coveragenpm run lint (tsc -b --noEmit) passes. npm run build (vite) passes. Go-side TestSqsHandler_PurgeQueue_RateLimited429 updated for the error-key envelope. No frontend test infra in the project (intentional; the lint script is the contract).

Test plan

  • cd web/admin && npm run lint (tsc -b --noEmit) passes
  • cd web/admin && npm run build (vite build) passes
  • go test -race -count=1 ./internal/admin/... passes (12 existing + the 429 envelope update)
  • golangci-lint run ./internal/admin/... 0 issues
  • CI on this PR
  • Manual smoke: navigate to a queue detail page, peek messages, open the modal, copy-as-JSON, attempt purge twice (second attempt should hit 429)

Out of scope (follow-ups)

  • Throttle integration: dedicated per-queue admin-peek bucket (bucketActionAdminPeek + resolveActionConfig explicit case + typed adminPeekThrottledError / PeekThrottledError). Leader-only + Limit ≤ 100 already bound the steady-state cost; the dedicated bucket adds a per-queue cap.
  • Audit logging & Prometheus counters (design doc §3.6): admin.sqs.purge_queue audit line + elastickv_sqs_admin_purge_queue_total{queue, outcome} / elastickv_sqs_admin_peek_queue_total{queue, outcome}.
  • Page-size selector (20 / 50 / 100) + cumulative-response-size warning. The MVP defaults to 20 to keep the worst-case at 5 MiB; larger sizes can land as a follow-up if operators request them.
  • principalForReadSensitive live RoleStore re-check (design doc Goal 8) — blocked on the wider live-role plumbing.
  • Design doc rename _proposed__implemented_ once Phase 6 lands (just the rename — Phases 2/3/4/5 are functionally complete).

Summary by CodeRabbit

  • New Features

    • Added message peeking with pagination for SQS queues
    • Introduced Dead Letter Queue (DLQ) indicators and source queue references
    • Added queue purge workflow with confirmation modal
    • Added "Copy as JSON" functionality for message details
  • Updates

    • Enhanced SQS error response formatting for improved API consistency

Review Change Stack

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 21, 2026

Warning

Rate limit exceeded

@bootjp has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 34 minutes and 47 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d07ed41d-d75d-4c3b-b55b-00cd5d1fc98c

📥 Commits

Reviewing files that changed from the base of the PR and between 97ad686 and 0370c0c.

📒 Files selected for processing (3)
  • internal/admin/sqs_handler.go
  • web/admin/src/api/client.ts
  • web/admin/src/pages/SqsDetail.tsx
📝 Walkthrough

Walkthrough

The 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 code to error, the API client extends TypeScript typings for DLQ metadata and adds a new peekQueue operation, and the SqsDetail page now displays DLQ information and implements a message-peeking interface with purge confirmation.

Changes

SQS Queue Admin - Message Browsing & DLQ Display

Layer / File(s) Summary
Backend purge rate-limit response format
internal/admin/sqs_handler.go, internal/admin/sqs_handler_test.go
The writePurgeInProgress 429 response adds X-Content-Type-Options: nosniff header and changes JSON payload key from code to error (value remains PurgeQueueInProgress). Comment clarifies Retry-After whole-second rounding. Test assertions updated to validate the new field name.
API client - Message peek & DLQ contracts
web/admin/src/api/client.ts
SqsQueueSummary gains is_dlq: boolean and optional dlq_sources?: string[] fields. New interfaces define the peek message workflow: SqsPeekedAttribute, SqsPeekedMessage, SqsPeekResult (with cursor for pagination), and SqsPeekOptions. New api.peekQueue(name, opts, signal) method maps peek options to query parameters and calls the /sqs/queues/:name/messages endpoint.
SqsDetail page - Queue info & message browsing UI
web/admin/src/pages/SqsDetail.tsx
Imports useCallback/useEffect and API functions. Defines constants for peek page size and eager body fetch size. Header renders a "DLQ" badge and optionally a "DLQ for" card listing source queue links. Introduces MessagesSection component managing peek state, pagination (first/next/refresh), message selection, and a purge modal requiring exact queue-name confirmation. MessagesTable and MessageDetail components render message lists and details, with "Copy as JSON" supporting clipboard API fallback to window.prompt. Utility functions compute UTF-8 byte lengths, format byte sizes, and truncate body previews.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • bootjp/elastickv#757: Outlines the admin peek-and-purge queue design that this PR implements, including DLQ awareness and message browsing workflows.
  • bootjp/elastickv#794: Backend implementation of AdminPeekQueue endpoint and peeked-message/cursor data models that the API client in this PR consumes via api.peekQueue.

Poem

🐰 A peek into the queue, what secrets it holds,
DLQ badges gleaming, their story unfolds,
Copy as JSON, a purge with a name,
Message browsing delight—the admin's new game!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main changes: a new Messages tab with Purge button functionality and DLQ visual indicators for the admin SPA, labeled as Phase 5 of a larger design.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sqs-admin-peek-purge-spa

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 21, 2026

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Claude finished @bootjp's task in 4m 21s —— View job


Code Review — PR #798: web/admin: SPA Messages tab + Purge button + DLQ chips

  • Gather context and understand the request
  • Read and analyze all changed files
  • Post review findings

Overall the PR is well-structured. The eager full-body fetch design is sound, the codeerror key rename is correct, and the X-Content-Type-Options: nosniff addition to writePurgeInProgress matches writeJSONError. Six issues worth addressing before merge are listed below.


Bugs

1. setLoading(false) fires on AbortError due to finallySqsDetail.tsx:225–232

The finally block always executes, even when catch returns early for AbortError:

} 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 useEffect cleanup calls ctl.abort(). The catch block returns early, but finally still calls setLoading(false) on the unmounted component. React 18 doesn't warn about this anymore, but the update is unnecessary noise. Fix: move setLoading(false) into the success branch and the non-abort error branch explicitly (mirroring the cancelled pattern in useApiQuery), or restructure to not use finally for abort paths. Fix this →

2. purgeRetryAfter hardcoded to 60 regardless of actual remaining time — SqsDetail.tsx:265

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 Retry-After header and retry_after_seconds in the JSON body, but neither is exposed through ApiError. The hardcoded 60 could show "retry after about 60 seconds" when only 3 seconds remain. The server's message field already says "only one PurgeQueue operation on each queue is allowed every 60 seconds", so the extra suffix is also redundant. Consider removing the purgeRetryAfter state entirely and relying solely on formatApiError(err), or extend ApiError to carry retry_after_seconds from the parsed JSON body. Fix this →

3. Silent clipboard failure in MessageDetailSqsDetail.tsx:452

void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
  setCopied(true);
  setTimeout(() => setCopied(false), 1500);
});

There is no .catch(). If the clipboard API fails (document not focused, insecure context, permission denied), the error is silently discarded and the button continues to show "Copy as JSON" with no feedback. Add a .catch that sets an error state or at minimum falls back to window.prompt. Fix this →


Accuracy issue

4. message.body.length ≠ bytes in truncation display — SqsDetail.tsx:487

Truncated to {formatBytes(message.body.length)} of {formatBytes(message.body_original_size)}

message.body.length is JavaScript's UTF-16 string length. For non-ASCII content (CJK, emoji, etc.) this is less than the actual byte count. Since body_max_bytes is applied server-side on raw UTF-8 bytes, the "Truncated to X of Y" display will be wrong for non-ASCII bodies. Use new TextEncoder().encode(message.body).byteLength instead. Fix this →


Minor issues

5. Post-purge fetchPage(undefined) has no AbortController — SqsDetail.tsx:256

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 useEffect cleanup, but this follow-up call is not. Low risk in practice (purge is a rare operator action), but worth noting for consistency.

6. Multi-line comments in sqs_handler.go and client.ts violate CLAUDE.md conventions

CLAUDE.md specifies: "Never write multi-paragraph docstrings or multi-line comment blocks — one short line max." The 7-line comment block added to writePurgeInProgress and the multi-line JSDoc-style comments on SqsPeekedAttribute, peekQueue, and purgeQueue in client.ts all exceed this. The semantic content is useful but should be distilled to a single line per item.


internal/admin/sqs_handler.go — looks good

The codeerror key rename is correct: apiFetch reads payload.error (client.ts:97) so this was a real inconsistency. Adding X-Content-Type-Options: nosniff to writePurgeInProgress brings it in line with writeJSONError (router.go:390). Test assertion updated correctly.

web/admin/src/api/client.ts — looks good

buildURL already filters undefined query params, so the optional SqsPeekOptions fields pass through cleanly. purgeQueue returning apiFetch<void> is correct: 429 responses carry JSON and hit the error branch in apiFetch, not the 204 short-circuit.

web/admin/src/pages/SqsDetail.tsx — looks good overall

DLQ chip + source list, pagination cursor semantics (tracking pageCursor passed to the request, not the server-returned cursor), purge confirmation requiring exact queue name typed, and AbortController in the initial useEffect are all implemented correctly. The eager 256 KiB body fetch eliminates the modal re-peek race as described.


Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread web/admin/src/pages/SqsDetail.tsx Outdated
Comment on lines +452 to +455
void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
setCopied(true);
setTimeout(() => setCopied(false), 1500);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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);
});

Comment on lines +535 to +539
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`;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
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";
}

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread web/admin/src/pages/SqsDetail.tsx Outdated
exported_at: new Date().toISOString(),
message,
};
void navigator.clipboard.writeText(JSON.stringify(payload, null, 2)).then(() => {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Base automatically changed from feat/sqs-admin-peek-purge-http to main May 21, 2026 14:39
bootjp added 2 commits May 22, 2026 01:34
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).
@bootjp bootjp force-pushed the feat/sqs-admin-peek-purge-spa branch from f206f1f to 97ad686 Compare May 21, 2026 16:37
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 21, 2026

@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
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Claude finished @bootjp's task in 4m 1s —— View job


Code Review — PR #798 (r2)

  • Gather context and understand the request
  • Read all changed files
  • Verify r1 feedback was addressed
  • Review for new issues
  • Post findings

All six r1 items from the previous review are verified:

r1 item Status
Bug #1setLoading 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 #4body.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:

  1. Sets copyError (informing the user to use the prompt)
  2. 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.");

Fix this →


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 the body["error"] assertion
  • sqs_handler.go:43–46, 48–53 — 4- and 5-line field comments on IsDLQ/DLQSources
  • SqsDetail.tsx:11–14, 17–23 — 4- and 7-line constant comments
  • SqsDetail.tsx:195–199, 225–227, 256–262, 441–444, 458–460 — 3–5 line inline blocks
  • client.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_sources fully wired in main_admin.go:217–221, 340–341 and adapter/sqs_admin.go; the TypeScript required field is safe since IsDLQ bool has no omitempty tag so Go always emits it.
  • cursor state semantics (tracks last-requested cursor, not server-returned next_cursor) are correct; "First page" disabled state and "Refresh" semantics are consistent.
  • fetchPage useCallback dependency 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.
  • previewBody using .length for the 96-char display truncation is appropriate — that's a character count for visual layout, not a byte count.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +221 to +223
setResult(res);
setCursor(pageCursor);
setLoading(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 21, 2026

@claude review

Addressed r2 in 2397ca9:

  • Codex P2 (peek stale-response race): added pendingAbortRef AbortController-per-call; fresh fetch aborts previous one, ignored aborted responses on race.
  • Bot bug (clipboard window.prompt fallback): dropped the prompt fallback; on insecure context / clipboard failure, point operator at the body
     already in the modal (avoids blocking modal + 350KiB truncation risk).
  • Bot accuracy (binary attribute length): added base64DecodedByteLength helper using padded-length arithmetic; now shows '<binary, N bytes>' (decoded count) instead of base64 char count.
  • Bot minor (multi-line comments): shortened the IsDLQ / DLQSources docstrings on QueueSummary.

@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Claude finished @bootjp's task in 3m 46s —— View job


Code Review — PR #798 (r3)

  • Gather context and understand the request
  • Read all changed files (2397ca9)
  • Verify r2 feedback was addressed
  • Review for new issues
  • Post findings

All four substantive r2 items verified:

r2 item Status
Bug — window.prompt fallback (blocks before setCopyError renders, truncates 350 KiB) ✅ Dropped entirely. Capability check + try/catch route to setCopyError pointing at the <pre> already in the modal.
Accuracy — binary attribute displays base64 char count (~4/3× inflation) base64DecodedByteLength(b64) helper added using padded-length arithmetic (Math.floor(b64.length * 3/4) - padding). Correct for standard padded base64, which Go's encoding/json always emits.
Codex P2 — stale-response race on button-triggered peeks pendingAbortRef = useRef<AbortController|null>(null). Each fetchPage call aborts the previous controller, creates a new one, and guards both the success and error paths with if (ctl.signal.aborted) return.
r2 minor #5 — post-purge fetchPage(undefined) had no AbortController ✅ Implicitly fixed: fetchPage now owns its own controller, so the useEffect cleanup (() => pendingAbortRef.current?.abort()) covers the post-purge fetch too.

The abort logic is sound. After pendingAbortRef.current?.abort() at line 219, the old invocation's captured ctl is aborted, so the guard at line 230 (if (ctl.signal.aborted) return) discards the stale result before touching state. The new invocation's setLoading(true) at line 222 is the authoritative loading indicator for the current fetch.

base64DecodedByteLength is correct for standard padded base64. A string whose length is not a multiple of 4 (malformed) would produce an off-by-one, but that can't happen with Go's encoder output.


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:

  • pendingAbortRef comment — 4 lines (SqsDetail.tsx:211–214)
  • base64DecodedByteLength comment — 4 lines (SqsDetail.tsx:564–567)

And client.ts is_dlq / dlq_sources comments (lines 213–220) remain 4 and 3 lines respectively — the Go struct fields were condensed but the TypeScript counterparts were not.

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

  • The abort-check double-guard in the catch block (ctl.signal.aborted + DOMException/AbortError) is correct: the first covers environments that abort without throwing, the second is the standard browser path.
  • onPurgeSubmit's void fetchPage(undefined) is fire-and-forget; the finally block's setPurging(false) runs synchronously after the purge resolves and the post-purge fetch begins asynchronously with its own controller. No interleaving issue.
  • setCopyError(null) is called on successful clipboard write (line 456) so a prior failure clears. ✅
  • Go side: sqs_handler.go struct field comments at lines 43 and 45 are now one-liners. ✅

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.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 21, 2026

@claude review

r3 style follow-up in 3a023a1: condensed the remaining multi-line comments the bot flagged (pendingAbortRef, base64DecodedByteLength, is_dlq, dlq_sources) to single-line summaries. No behaviour change. Bot's verdict on r2 was 'PR is ready to merge'.

@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Claude finished @bootjp's task in 2m 4s —— View job


Code Review — PR #798 (r4)

  • Gather context and understand the request
  • Read changed files and verify style cleanup
  • Post findings

All three items specifically flagged in the r3 review are verified fixed:

r3 item Status
pendingAbortRef comment — 4 lines (SqsDetail.tsx:211–214) SqsDetail.tsx:210 — condensed to 1 line: // Cancels the prior peek so a slow response cannot overwrite newer state.
base64DecodedByteLength comment — 4 lines (SqsDetail.tsx:564–567) SqsDetail.tsx:560 — condensed to 1 line: // Decoded length without actually decoding: 4 chars → 3 bytes, minus padding.
is_dlq / dlq_sources in client.ts — 4 + 3 lines client.ts:213–215 — condensed to 1 line each, matching the Go struct field comments.

No new multi-line comment blocks were introduced in 3a023a1. The diff is exactly the four items the commit message claims: pendingAbortRef, base64DecodedByteLength, is_dlq, dlq_sources.

The remaining multi-line comment blocks in the file (kPeekPageSize lines 11–14, kPeekBodyMaxBytes lines 17–23, MessagesSection lines 195–199, the onPurgeSubmit catch block lines 259–265, the clipboard guard lines 445–448) were all present before the r3 review, where the verdict was already "ready to merge." They carry genuine design rationale — the "why" class of comments CLAUDE.md means to preserve — and don't change the verdict.

PR is ready to merge.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +233 to +234
setError(formatApiError(err));
setLoading(false);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented May 21, 2026

@claude review

Addressed Codex r3 P2 in 0370c0c:

  • useEffect now clears result + cursor when queue changes (i.e., fetchPage callback identity changes), so the brief loading window does not show cross-queue data.
  • catch path clears result + cursor before setting the error, so a failed refresh on the new queue does not leave the prior queue's messages visible.

@claude
Copy link
Copy Markdown

claude Bot commented May 21, 2026

Claude finished @bootjp's task in 1m 27s —— View job


Code Review — PR #798 (r4 follow-up)

  • Gather context and read changed files
  • Verify Codex r3 P2 feedback was addressed (queue-change clear + catch-path clear)
  • Review for new issues
  • Post findings

Both Codex r3 P2 items verified ✅:

r3 Codex P2 item Status
useEffect should clear result+cursor when queue changes so loading window doesn't show cross-queue data SqsDetail.tsx:247–248setResult(null) + setCursor(undefined) called before fetchPage(undefined) in the effect body. Since fetchPage is a useCallback([queue]), the effect fires exactly when queue changes.
catch path should clear result+cursor so a failed refresh on the new queue doesn't leave prior queue's messages visible SqsDetail.tsx:235–236setResult(null) + setCursor(undefined) inserted before setError(formatApiError(err)). Failed fetches now always present an empty table + error message rather than stale rows.

The implementation is correct on both counts. The abort-guard at line 231 (if (ctl.signal.aborted) return) still runs before the new clears, so aborted requests don't needlessly wipe state on unmount.


One minor observation (non-blocking)

Two new 2-line comments were introduced in 0370c0c:

// Clear stale rows so a failed fetch after a queue change does
// not leave the prior queue's messages visible (Codex r3 P2).
// Clear prior queue's table when the queue prop changes so the
// brief loading window does not show cross-queue data.

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.

@bootjp bootjp enabled auto-merge May 21, 2026 17:05
@bootjp bootjp merged commit b218dd3 into main May 21, 2026
9 checks passed
@bootjp bootjp deleted the feat/sqs-admin-peek-purge-spa branch May 21, 2026 17:13
bootjp added a commit that referenced this pull request May 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant