Skip to content

feat(sqs/admin): SigV4-bypass admin entrypoints + SPA queues pages#670

Merged
bootjp merged 11 commits intomainfrom
feat/sqs-admin-queues-handler
Apr 26, 2026
Merged

feat(sqs/admin): SigV4-bypass admin entrypoints + SPA queues pages#670
bootjp merged 11 commits intomainfrom
feat/sqs-admin-queues-handler

Conversation

@bootjp
Copy link
Copy Markdown
Owner

@bootjp bootjp commented Apr 26, 2026

Summary

Replaces #659, which has unresolvable conflicts now that main has moved on (PR #649 squashed into main; PR #658 added the S3 admin endpoints; the Approximate counters implementation now lives directly in adapter/sqs_catalog.go via scanApproxCounters). Rather than a multi-day rebase, this PR re-applies the unique SQS admin code on a fresh branch off current main.

What survived from #659:

  • adapter/sqs_admin.goSQSServer.AdminListQueues / AdminDescribeQueue / AdminDeleteQueue (SigV4-bypass entrypoints, same shape as AdminListTables / AdminListBuckets).
  • internal/admin/sqs_handler.go — HTTP handler for /admin/api/v1/sqs/queues{,/{name}} with role re-evaluation on DELETE.
  • web/admin/src/pages/SqsList.tsx / SqsDetail.tsx — SPA pages for the queues view + delete confirmation.

What changed during the re-apply:

  • AdminQueueCounters is now int64 (matches sqsApproxCounters from main; bridge does no width conversion).
  • AdminDescribeQueue calls main's scanApproxCounters instead of the duplicate computeApproxCounters from the old branch — same numeric output, single implementation.
  • Dropped the CountersTruncated field; main's counter type doesn't expose truncation. SPA's "truncated" pill came out with it.
  • apiRouteTable.dispatch refactored to extract resourceHandlerFor so the dispatcher stays under cyclop=10 as new resources land.

Backend

  • Re-evaluates the principal's role against the live MapRoleStore on every DELETE so a downgraded key cannot keep mutating with a still-valid JWT (Codex P1 pattern from earlier admin PRs).
  • admin.QueuesSource is opt-in: deployments without --sqsAddress leave /admin/api/v1/sqs/* off the wire; the SPA renders a soft "endpoint pending" notice on the 404, mirroring the Tables / Buckets nil contract.
  • The bridge in main_admin.go (sqsQueuesBridge, convertAdminQueueSummary, translateAdminQueuesError) keeps internal/admin free of the heavy adapter dependency tree, same architectural pattern as Dynamo and S3.

Frontend

  • /sqs queue list with refresh + per-row link to detail.
  • /sqs/:name detail showing FIFO badge, counters card (Visible / In-flight / Delayed), raw attributes table, and a Delete confirmation Modal gated by RequireFullAccess.
  • api/client.ts gains listQueues / describeQueue / deleteQueue with the same AbortSignal pattern used for cluster / dynamo / s3 reads.
  • Layout nav adds an SQS tab between DynamoDB and S3.

Out of scope (recorded in docs/design/2026_04_24_proposed_sqs_compatible_adapter.md Section 14, deferred per the SQS partial doc §16.2)

  • PurgeQueue from the SPA — the underlying purgeQueueWithRetry adapter method is on main; the admin entrypoint is a trivial follow-up.
  • Send / Peek / CreateQueue from the SPA — each needs its own SigV4-bypass adapter entrypoint and form UX; deferred to keep this PR focused.

Test plan

  • go build ./... — clean
  • go test -race ./internal/admin/... — passes
  • go test -race -run TestSQS ./adapter/ — passes
  • go test -run TestStartAdmin . — passes
  • golangci-lint run ./adapter/... ./internal/admin/... ./...0 issues., no //nolint
  • cd web/admin && npm run build — 49 modules, 199 KB JS / 61 KB gzip + 14.7 KB CSS
  • Manual smoke (after PR lands): start a node with --sqsAddress :4566 --adminEnabled --adminAllowInsecureDevCookie, create a queue, send a few messages, hit /admin/sqs/<name> → counters match GetQueueAttributes("All"), Delete dialog returns to list.

Self-review (5 lenses)

  1. Data loss — Delete reuses the existing deleteQueueWithRetry OCC path; counters are read-only. No new write paths.
  2. Concurrency — Per-request leader check on Delete; counters scan uses one snapshot read TS.
  3. Performance — Counters bounded by main's existing sqsApproxCounterScanLimit; admin reads are cheap point lookups + one bounded scan.
  4. Data consistencyAdminDescribeQueue and SigV4 GetQueueAttributes both call scanApproxCounters at a fresh nextTxnReadTS, so a single point in time produces the same counters via either surface.
  5. Test coverage — Existing admin / SQS race suites stay green via the new nil Queues argument added to startAdminServer call sites; the new bridge is exercised by the cross-package build itself.

Stacking

This PR is independent — branched from current main (which has the merged versions of #649 / #658 / #650 / counter implementation). Closing #659 in favour of this clean rewrite.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 26, 2026

Warning

Rate limit exceeded

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

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 25 minutes and 6 seconds.

⌛ 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: 18863ff7-8614-4088-aa70-4cc448bc755f

📥 Commits

Reviewing files that changed from the base of the PR and between ee521be and 5ee0175.

📒 Files selected for processing (14)
  • adapter/sqs_admin.go
  • adapter/sqs_admin_test.go
  • internal/admin/server.go
  • internal/admin/sqs_handler.go
  • internal/admin/sqs_handler_test.go
  • main.go
  • main_admin.go
  • main_admin_test.go
  • main_sqs.go
  • web/admin/src/App.tsx
  • web/admin/src/api/client.ts
  • web/admin/src/components/Layout.tsx
  • web/admin/src/pages/SqsDetail.tsx
  • web/admin/src/pages/SqsList.tsx
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/sqs-admin-queues-handler

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.

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 introduces SQS admin capabilities to the dashboard, allowing users to list, describe, and delete queues. It includes a new SQS handler in the admin package, an adapter bridge, and a React-based frontend for queue management. Feedback focuses on removing a stale field from the queue summary DTO and ensuring that empty queue lists are serialized as empty arrays to prevent potential frontend crashes.

Comment thread internal/admin/sqs_handler.go Outdated
CreatedAt time.Time `json:"created_at,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"`
Counters QueueCounters `json:"counters"`
CountersTruncated bool `json:"counters_truncated,omitempty"`
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 CountersTruncated field appears to be stale. The pull request description explicitly mentions that this field was dropped because the underlying counter implementation in main does not expose truncation information, and the corresponding frontend UI was also removed. Removing this field from the DTO aligns with the rule to remove dead or unreachable code paths to improve code clarity and maintainability.

References
  1. Remove dead or unreachable code paths, such as asynchronous handlers for tasks that are always performed synchronously, to improve code clarity and maintainability.

Comment on lines +166 to +170
if err := json.NewEncoder(w).Encode(listQueuesResponse{Queues: names}); err != nil {
h.logger.LogAttrs(r.Context(), slog.LevelWarn, "admin sqs list response encode failed",
slog.String("error", err.Error()),
)
}
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

If the names slice is nil (which can occur if the underlying storage returns a nil slice for an empty result set), the JSON response will be {"queues": null}. This can cause the frontend to crash when it attempts to access .length or .map() on the queues property in SqsList.tsx. It is safer to ensure that an empty list is always represented as an empty array [] in the JSON output.

Suggested change
if err := json.NewEncoder(w).Encode(listQueuesResponse{Queues: names}); err != nil {
h.logger.LogAttrs(r.Context(), slog.LevelWarn, "admin sqs list response encode failed",
slog.String("error", err.Error()),
)
}
if names == nil {
names = []string{}
}
if err := json.NewEncoder(w).Encode(listQueuesResponse{Queues: names}); err != nil {
h.logger.LogAttrs(r.Context(), slog.LevelWarn, "admin sqs list response encode failed",
slog.String("error", err.Error()),
)
}

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: ba690577de

ℹ️ 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 main_admin.go Outdated
Comment on lines +214 to +215
default:
return err
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 Map SQS leader-churn errors to not-leader sentinel

When AdminDeleteQueue loses leadership between its initial leader check and deleteQueueWithRetry dispatch, the adapter can return wrapped KV leader errors (not adapter.ErrAdminNotLeader), and this switch falls through to default, so the handler emits a 500 instead of the intended 503/Retry-After retry signal. This is a real runtime path in multi-node clusters during elections; the Dynamo bridge already handles the same churn via isLeaderChurnError, so SQS deletes currently regress to non-retryable failures under failover.

Useful? React with 👍 / 👎.

@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Pushed addressing both medium findings from your review:

  1. Removed the stale CountersTruncated field from internal/admin.QueueSummary — already gone from adapter.AdminQueueSummary and the SPA, this was the leftover JSON-side mirror.
  2. handleList now normalises a nil names slice to []string{} before encoding so the empty-catalog response is {"queues": []} instead of {"queues": null} — the SPA iterates the array directly and would crash on null.

go build / go test -race ./internal/admin/... / go test -race -run TestSQS ./adapter/ / golangci-lint run all clean (0 issues, no //nolint).

@claude review

bootjp added a commit that referenced this pull request Apr 26, 2026
…s []

Two medium findings from Gemini review on PR #670:

1. CountersTruncated field was stale on internal/admin.QueueSummary.
   The PR description called out that the field was dropped because
   main's sqsApproxCounters does not expose truncation, but I missed
   removing the JSON-side mirror in internal/admin/sqs_handler.go.
   The SPA was never reading it (SqsDetail.tsx already had the
   reference removed in this PR), so the field was load-bearing for
   nothing. Removed.

2. handleList serialised an empty queue catalog as
   {"queues": null} when AdminListQueues returned nil. The SPA
   iterates the array directly and would crash on null. Normalise
   nil to []string{} immediately before encoding so the response
   shape is always {"queues": []} even on the empty case.

No new tests because the existing internal/admin race test suite
already exercises the encoder path (would have caught a json.Marshal
break) and the empty-catalog case is exactly what the SPA hits the
moment a fresh node comes up — manual smoke during PR #649 already
verified the SPA renders an empty list cleanly when the array is [].

Verified with go build ./..., go test -race ./internal/admin/...,
go test -race -run TestSQS ./adapter/, golangci-lint run
./adapter/... ./internal/admin/... — all clean, no //nolint.
@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read changed files (adapter, admin handler, main wiring, frontend)
  • Verify the two pushed fixes (CountersTruncated removal + nil→[] normalization)
  • Assess Codex P2: leader-churn error mapping in translateAdminQueuesError
  • Review adapter/sqs_admin.go
  • Review internal/admin/sqs_handler.go + server.go
  • Review main_admin.go bridge + test
  • Review frontend (SqsList.tsx, SqsDetail.tsx, client.ts)

Confirmed fixes ✓

Both pushed fixes landed correctly:

  1. CountersTruncated removedadmin.QueueSummary in sqs_handler.go:27–43 has no such field; adapter.AdminQueueSummary in sqs_admin.go:20–27 likewise clean.
  2. nil→[] normalizationsqs_handler.go:167–169 normalises before encoding; {"queues": []} on empty catalog is now guaranteed.

P1 — translateAdminQueuesError does not catch leader-churn errors (Codex P2 confirmed)

The Codex finding is real and the failure path is straightforward to trace:

  1. AdminDeleteQueue passes the isVerifiedSQSLeader upfront check.
  2. Leadership is lost before deleteQueueWithRetry's coordinator.Dispatch call.
  3. Dispatch returns kv.ErrLeaderNotFound or adapter.ErrNotLeader.
  4. isRetryableTransactWriteError returns false (only covers ErrWriteConflict / ErrTxnLockeddynamodb.go:4758), so the error exits the retry loop.
  5. isSQSAdminQueueDoesNotExist doesn't match → errors.Wrap(err, "admin delete queue").
  6. The bridge's translateAdminQueuesError (main_admin.go:202–217) has no arm for these errors → falls to default → returns the raw wrapped error.
  7. writeQueuesError logs and returns 500, not 503+Retry-After.

The Dynamo bridge fixed the identical gap in PR #634 (translateAdminTablesError:437–444); translateAdminQueuesError needs the same treatment:

// main_admin.go translateAdminQueuesError — add before default:
case isLeaderChurnError(err):
    return admin.ErrQueuesNotLeader

Fix this →


P2 — No tests for translateAdminQueuesError

The existing test file has thorough coverage of the Dynamo equivalent (TestTranslateAdminTablesError_LeaderChurn, _LeaderPhraseInMiddleOfMessage, _UnrelatedErrorPassesThrough), but there are no parallel cases for the SQS bridge. CLAUDE.md requires unit tests for new/changed branches; the error-translation path is the highest-risk part of the bridge. Adding three analogous table-driven cases would lock down the regression and make the P1 fix verifiable.


Minor — scanApproxCounters error not wrapped at callsite

adapter/sqs_admin.go:68–70:

counters, err := s.scanApproxCounters(ctx, name, meta.Generation, readTS)
if err != nil {
    return nil, false, err   // no errors.WithStack
}

The adjacent loadQueueMetaAt error (sqs_admin.go:61) uses errors.WithStack(err). If scanApproxCounters already wraps internally, omitting the stack frame here is fine; if it doesn't, the callsite frame is lost. Worth checking or adding for consistency — wrapcheck apparently doesn't flag it so the linter is satisfied.


Low — Internal implementation note in SPA subtitle

SqsList.tsx:14–15:

Backed by the SigV4-bypass admin entrypoints in
<code className="font-mono ml-1">adapter/sqs_admin.go</code>.
Detail pages show the new approximate counters from Phase 3.A.

This surfaces a Go source file path and an internal milestone name to end users. The other list pages (DynamoList, S3List) don't do this. Fine for dev/internal tooling, but worth noting if this surface ever faces external operators.


Everything else looks good ✓

  • CSRF / session pattern in SqsHandler is consistent with DynamoHandler / S3Handler.
  • buildSqsHandlerForDeps wires MapRoleStore on the handler the same way Dynamo does; S3 currently doesn't (read-only), and that distinction is intentional.
  • handleDelete checks the principal role before the name validation, so a read-only caller never learns the leader's identity by indirection — correct ordering per the Codex P1 pattern.
  • nil Queues → routes off the wire contract is consistent with Tables / Buckets.
  • Frontend null-safety: queues.data.queues.length is now safe with the nil→[] fix; detail.data?.attributes uses optional chaining correctly.
  • encodeURIComponent(name) on links in SqsList.tsx — correct.
  • deleteQueue omitting AbortSignal is consistent with deleteTable / deleteBucket.
  • main_admin_test.go startAdminServer call sites now pass nil for the new queues parameter — confirms the opt-in nil contract is covered by the existing test build.

Bottom line: The two Gemini findings are confirmed fixed. The Codex P2 is a real runtime regression under elections and needs the isLeaderChurnError arm + test before 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: 3a8a1d7603

ℹ️ 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".

writeJSONError(w, http.StatusBadRequest, "invalid_queue_name", "queue name is required")
return
}
if err := h.source.AdminDeleteQueue(r.Context(), principal, name); err != nil {
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 Pass live role to queue delete source

handleDelete checks live authorization with principalCanWrite, but it still calls AdminDeleteQueue with the original JWT principal. In the specific case where a key is promoted from read_only to full while the JWT is still valid, this path authorizes the request and then immediately fails in the adapter because the forwarded role is stale, so queue deletes incorrectly return forbidden until the user logs in again.

Useful? React with 👍 / 👎.

bootjp added a commit that referenced this pull request Apr 26, 2026
Three findings from Claude review on PR #670:

1. (P1) translateAdminQueuesError did not catch leader-churn errors.
   AdminDeleteQueue passes the upfront isVerifiedSQSLeader check,
   then dispatches deleteQueueWithRetry which can hit a kv coordinator
   that just lost leadership. The resulting kv.ErrLeaderNotFound /
   adapter.ErrNotLeader / wrapped "not leader" suffixes were not in
   the translator's switch — they fell to default and the admin
   handler rendered a generic 500 instead of the spec'd 503 +
   Retry-After: 1. Added the `case isLeaderChurnError(err)` arm
   mirroring translateAdminTablesError's identical fix from PR #634.

2. (P2) No tests for translateAdminQueuesError. Mirrored the three
   Dynamo equivalents in main_admin_test.go:
     - TestTranslateAdminQueuesError_LeaderChurn covers every kv
       sentinel + canonical wrapped-suffix variant.
     - TestTranslateAdminQueuesError_LeaderPhraseInMiddleOfMessage
       pins the HasSuffix matcher behaviour against false positives
       on user-supplied error messages mid-string.
     - TestTranslateAdminQueuesError_UnrelatedErrorPassesThrough
       confirms the detector does not swallow innocent "leader"
       mentions outside the canonical phrase set.

3. (Low / polish) SqsList.tsx subtitle leaked the Go file path
   "adapter/sqs_admin.go" and the internal milestone name
   "Phase 3.A" to end users — DynamoList / S3List don't do this.
   Replaced with operator-facing prose describing what the page does.

Verified:
  - go test -run TestTranslateAdminQueuesError . — passes
  - go build ./... clean
  - go test -race ./internal/admin/... + go test -race -run TestSQS ./adapter/ — pass
  - golangci-lint run ./adapter/... ./internal/admin/... ./... — 0 issues
  - cd web/admin && npm run lint (tsc --strict) clean
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Pushed addressing all three findings from your review (1 P1, 1 P2, 1 polish):

P1 — leader-churn mapping: translateAdminQueuesError now has the isLeaderChurnError arm that maps kv.ErrLeaderNotFound / adapter.ErrNotLeader / wrapped "not leader" suffixes to admin.ErrQueuesNotLeader (503 + Retry-After:1) instead of the generic 500 fallthrough. Mirrors translateAdminTablesError's same fix from PR #634.

P2 — error-translator tests: added three table-driven tests parallel to the Dynamo equivalents:

  • TestTranslateAdminQueuesError_LeaderChurn — every kv sentinel + canonical wrapped-suffix.
  • TestTranslateAdminQueuesError_LeaderPhraseInMiddleOfMessage — pins HasSuffix matcher against mid-message false positives.
  • TestTranslateAdminQueuesError_UnrelatedErrorPassesThrough — confirms innocent "leader" mentions pass through.

Polish — SPA subtitle: dropped the leaked adapter/sqs_admin.go path and "Phase 3.A" milestone reference. Now reads as operator-facing prose, matching DynamoList / S3List style.

Skipped the scanApproxCounters wrap-check minor (the linter is satisfied; the function does its own errors.WithStack internally so the callsite frame is fine).

go test -run TestTranslateAdminQueuesError . ✅, go test -race -run TestSQS ./adapter/ ✅, golangci-lint run 0 issues, npm run lint clean.

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read main_admin.go — verify P1 isLeaderChurnError arm in translateAdminQueuesError
  • Read main_admin_test.go — verify three new TestTranslateAdminQueuesError_* tests
  • Read web/admin/src/pages/SqsList.tsx — verify SPA subtitle polish
  • Read internal/admin/sqs_handler.go — check Codex P2 role-passing finding
  • Read adapter/sqs_admin.go — overall check
  • Compare DynamoHandler.principalForWrite pattern vs SQS handler
  • Post review findings

Confirmed fixes ✓

All three findings from the previous Claude review landed correctly:

P1 — isLeaderChurnError arm in translateAdminQueuesErrormain_admin.go:214–226 has the new case correctly positioned before default, maps to admin.ErrQueuesNotLeader (503 + Retry-After:1), and includes a detailed comment tracing the failure path. Mirrors translateAdminTablesError:450–457 exactly.

P2 — Three new table-driven testsmain_admin_test.go:437–488:

  • TestTranslateAdminQueuesError_LeaderChurn — 7 input cases: all three kv/adapter sentinels plus the four wrapped-suffix variants. Parallel to the Dynamo equivalent.
  • TestTranslateAdminQueuesError_LeaderPhraseInMiddleOfMessage — 3 mid-message cases; pins that HasSuffix prevents false positives.
  • TestTranslateAdminQueuesError_UnrelatedErrorPassesThrough — confirms "team leader misconfigured" falls through.

Polish — SPA subtitleSqsList.tsx:13–15 now reads as operator-facing prose ("List, describe, and delete SQS queues. Detail pages also surface the approximate visible / in-flight / delayed message counts."). No file path, no milestone reference.


P1 — handleDelete forwards JWT role, not live role to adapter

The Codex P2 about live role forwarding is still present. Tracing the path:

sqs_handler.go:218  h.principalCanWrite(principal)  // gates request, returns bool only
sqs_handler.go:227  h.source.AdminDeleteQueue(r.Context(), principal, name)  // principal still has JWT role

principalCanWrite (sqs_handler.go:239–252) resolves the live role from the RoleStore and uses it for the gate decision, but it returns only a bool and never writes back to principal.Role. The original JWT-embedded role is what flows through to the adapter.

The adapter then independently checks principal.Role.canWrite() (sqs_admin.go:87). In the key-promotion case (JWT says read_only, live store says full), principalCanWrite returns true and the request proceeds — but AdminDeleteQueue gets a principal with Role=read_only, returns ErrAdminForbidden, and the handler sends 403. The user must log out and back in before queue deletes work.

Compare to DynamoHandler.principalForWrite (dynamo_handler.go:413–448), which explicitly updates principal.Role = liveRole at line 445 before returning so the downstream call always sees the live role.

The fix is to extract an equivalent helper in the SQS handler:

// principalForWrite is the SQS counterpart of DynamoHandler.principalForWrite.
// Returns the principal with the live role applied, or false if the caller
// is not authorised to write.
func (h *SqsHandler) principalForWrite(w http.ResponseWriter, r *http.Request) (AuthPrincipal, bool) {
    principal, ok := PrincipalFromContext(r.Context())
    if !ok {
        writeJSONError(w, http.StatusInternalServerError, "internal", "missing session principal")
        return AuthPrincipal{}, false
    }
    if h.roles != nil {
        live, exists := h.roles.LookupRole(principal.AccessKey)
        if !exists || !live.AllowsWrite() {
            writeJSONError(w, http.StatusForbidden, "forbidden",
                "this access key is not authorised to delete queues")
            return AuthPrincipal{}, false
        }
        principal.Role = live  // forward live role, not JWT role
    } else if !principal.Role.AllowsWrite() {
        writeJSONError(w, http.StatusForbidden, "forbidden",
            "this access key is not authorised to delete queues")
        return AuthPrincipal{}, false
    }
    return principal, true
}

Then handleDelete becomes:

func (h *SqsHandler) handleDelete(w http.ResponseWriter, r *http.Request, name string) {
    principal, ok := h.principalForWrite(w, r)
    if !ok {
        return
    }
    // ... name validation and source.AdminDeleteQueue(r.Context(), principal, name)
}

Fix this →


Everything else looks good ✓

  • CountersTruncated confirmed absent — QueueSummary (sqs_handler.go:27–44) and AdminQueueSummary (sqs_admin.go:20–27) both clean.
  • nil→[] normalisation at sqs_handler.go:167–169 — confirmed, comment explains rationale.
  • buildSqsHandlerForDeps (server.go:183–189) wires MapRoleStore via WithRoleStore, same as DynamoHandler (server.go:157–161).
  • scanApproxCounters callsite (sqs_admin.go:67–70) — as noted, scanApproxCounters wraps internally so the missing errors.WithStack at the callsite doesn't lose the frame. Linter is satisfied; minor is closed.
  • SqsDetail.tsx — FIFO badge, counter cards, attribute table, delete modal all clean. detail.data?.attributes uses optional chaining correctly; created_at null-check is safe.
  • Three new tests are logically correct and parallel their Dynamo equivalents exactly.

Bottom line: P1, P2, and the SPA polish are all confirmed fixed. The Codex P2 about live role forwarding (JWT role vs live role passed to AdminDeleteQueue) is still present and is the one remaining issue before merge.

bootjp added 4 commits April 26, 2026 21:16
Replaces PR #659, which conflicted heavily after main moved (PR #649
squashed; PR #658 added S3 admin endpoints; the Approximate counters
implementation now lives directly in adapter/sqs_catalog.go).

This PR:

Backend (adapter/sqs_admin.go + internal/admin/sqs_handler.go):
- SQSServer.AdminListQueues / AdminDescribeQueue / AdminDeleteQueue
  are SigV4-bypass entrypoints, mirroring the AdminListTables /
  AdminListBuckets pattern.
- AdminDescribeQueue uses the existing scanApproxCounters from
  sqs_catalog.go (already on main) so the admin path returns the
  same Visible / NotVisible / Delayed numbers as
  GetQueueAttributes("All") would, taken at one snapshot read TS.
- sqsQueuesBridge in main_admin.go re-shapes
  adapter.AdminQueueSummary into admin.QueueSummary, keeping
  internal/admin free of the heavy adapter dependency tree —
  same pattern as dynamoTablesBridge / s3BucketsBridge.
- admin.QueuesSource is opt-in; deployments that don't run
  --sqsAddress leave /admin/api/v1/sqs/* off the wire and the
  SPA renders a soft "endpoint pending" notice on the 404.
- Role re-evaluation against the live RoleStore on DELETE so a
  downgraded key cannot keep mutating with a still-valid JWT.
- apiRouteTable.dispatch refactored: resourceHandlerFor extracted
  so the dispatcher stays under cyclop=10 as new resources land
  (Dynamo, S3, SQS, future).

Frontend (web/admin/src/pages/SqsList.tsx, SqsDetail.tsx):
- /sqs queue list with refresh + per-row link to detail.
- /sqs/:name detail showing FIFO badge, counters card (Visible /
  In-flight / Delayed), raw attributes table, and a Delete
  confirmation Modal gated by RequireFullAccess.
- api/client.ts gains listQueues / describeQueue / deleteQueue
  with the same AbortSignal pattern used for cluster / dynamo /
  s3 reads.
- Layout nav adds an SQS tab between DynamoDB and S3.

Out of scope (recorded in the SQS partial design doc §16.2):
- PurgeQueue from the SPA. Underlying purgeQueueWithRetry is on
  main; the admin entrypoint is a trivial follow-up.
- Send / Peek / CreateQueue from the SPA. Each needs its own
  adapter entrypoint and form UX; deferred to keep this PR
  focused.

Verified with go build ./..., go test -race ./internal/admin/...,
go test -race -run TestSQS ./adapter/, go test -run TestStartAdmin .,
golangci-lint run ./adapter/... ./internal/admin/... ./...
(0 issues, no //nolint), and cd web/admin && npm run build.
…s []

Two medium findings from Gemini review on PR #670:

1. CountersTruncated field was stale on internal/admin.QueueSummary.
   The PR description called out that the field was dropped because
   main's sqsApproxCounters does not expose truncation, but I missed
   removing the JSON-side mirror in internal/admin/sqs_handler.go.
   The SPA was never reading it (SqsDetail.tsx already had the
   reference removed in this PR), so the field was load-bearing for
   nothing. Removed.

2. handleList serialised an empty queue catalog as
   {"queues": null} when AdminListQueues returned nil. The SPA
   iterates the array directly and would crash on null. Normalise
   nil to []string{} immediately before encoding so the response
   shape is always {"queues": []} even on the empty case.

No new tests because the existing internal/admin race test suite
already exercises the encoder path (would have caught a json.Marshal
break) and the empty-catalog case is exactly what the SPA hits the
moment a fresh node comes up — manual smoke during PR #649 already
verified the SPA renders an empty list cleanly when the array is [].

Verified with go build ./..., go test -race ./internal/admin/...,
go test -race -run TestSQS ./adapter/, golangci-lint run
./adapter/... ./internal/admin/... — all clean, no //nolint.
Three findings from Claude review on PR #670:

1. (P1) translateAdminQueuesError did not catch leader-churn errors.
   AdminDeleteQueue passes the upfront isVerifiedSQSLeader check,
   then dispatches deleteQueueWithRetry which can hit a kv coordinator
   that just lost leadership. The resulting kv.ErrLeaderNotFound /
   adapter.ErrNotLeader / wrapped "not leader" suffixes were not in
   the translator's switch — they fell to default and the admin
   handler rendered a generic 500 instead of the spec'd 503 +
   Retry-After: 1. Added the `case isLeaderChurnError(err)` arm
   mirroring translateAdminTablesError's identical fix from PR #634.

2. (P2) No tests for translateAdminQueuesError. Mirrored the three
   Dynamo equivalents in main_admin_test.go:
     - TestTranslateAdminQueuesError_LeaderChurn covers every kv
       sentinel + canonical wrapped-suffix variant.
     - TestTranslateAdminQueuesError_LeaderPhraseInMiddleOfMessage
       pins the HasSuffix matcher behaviour against false positives
       on user-supplied error messages mid-string.
     - TestTranslateAdminQueuesError_UnrelatedErrorPassesThrough
       confirms the detector does not swallow innocent "leader"
       mentions outside the canonical phrase set.

3. (Low / polish) SqsList.tsx subtitle leaked the Go file path
   "adapter/sqs_admin.go" and the internal milestone name
   "Phase 3.A" to end users — DynamoList / S3List don't do this.
   Replaced with operator-facing prose describing what the page does.

Verified:
  - go test -run TestTranslateAdminQueuesError . — passes
  - go build ./... clean
  - go test -race ./internal/admin/... + go test -race -run TestSQS ./adapter/ — pass
  - golangci-lint run ./adapter/... ./internal/admin/... ./... — 0 issues
  - cd web/admin && npm run lint (tsc --strict) clean
Codex P2 + Claude P1 follow-up on PR #670.

Before this commit, handleDelete called principalCanWrite(principal)
which resolved the live role from the RoleStore for the *gate
decision* but never updated principal.Role itself. The unmodified
principal — still carrying the JWT's stale role — flowed downstream
to AdminDeleteQueue, which independently checked
principal.Role.canWrite() and rejected the request as ErrAdminForbidden
on the promotion edge case (JWT read_only / store full). The user
saw 403 and had to log out + back in for the new role to take effect.

Refactor mirrors DynamoHandler.principalForWrite (dynamo_handler.go):
principalCanWrite is replaced with principalForWrite that returns
(AuthPrincipal, bool). On success, the principal carries the *live*
role override; failure paths write the response and return false so
the caller short-circuits.

Five new test cases in internal/admin/sqs_handler_test.go:

  - DeleteQueue_LivePromotion: pins the actual fix — JWT read_only
    + store full → 204 succeeds AND src.lastDeletePrincipal.Role
    is RoleFull (not RoleReadOnly) at the source.
  - DeleteQueue_LiveRevocation: symmetric — JWT full + store
    read_only → 403, source not reached.
  - DeleteQueue_KeyRemovedFromStore: JWT authenticated but key
    rotated out of the role config → 403.
  - DeleteQueue_NoRoleStore: single-tenant default — handler
    trusts JWT role; full→204, read_only→403.
  - ListQueues_EmptyArrayNotNull: regression net for the prior
    nil→[] fix; pins {"queues":[]} on empty catalog.

Verified: go build ./..., go test -race ./internal/admin/...,
golangci-lint run ./adapter/... ./internal/admin/... — all clean.
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: 741b5619b1

ℹ️ 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 adapter/sqs_admin.go Outdated
Name: name,
IsFIFO: meta.IsFIFO,
Generation: meta.Generation,
CreatedAt: hlcToTime(meta.CreatedAtHLC),
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 Use wall-clock timestamp for queue created_at

AdminDescribeQueue fills created_at from meta.CreatedAtHLC, but SQS metadata explicitly tracks wall-clock creation time in CreatedAtMillis and CreatedAtHLC can be zero (e.g., older records or when no clock was available). In that case hlcToTime(0) resolves to the Unix epoch, so the admin API returns a bogus 1970-01-01 creation time instead of the real timestamp (or omitting it). This makes queue metadata inaccurate in the SPA for valid persisted queues.

Useful? React with 👍 / 👎.

…lsites

Rebase residue: ee521be added the keyvizSampler parameter to
startAdminServer; this updates the six test callsites that still
pass the pre-rebase argument count.
@bootjp bootjp force-pushed the feat/sqs-admin-queues-handler branch from 741b561 to d665c4b Compare April 26, 2026 12:21
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Rebased on main (resolved keyviz wiring conflicts in main.go, main_admin.go, internal/admin/server.go) and addressed the latest Claude review P1.

P1 — handleDelete forwards live role, not stale JWT role

internal/admin/sqs_handler.go now factors the auth gate into principalForWrite(w, r) (mirroring DynamoHandler). When a RoleStore is wired, the live role overrides principal.Role before the principal flows into AdminDeleteQueue, so a key promoted read_only → full after login no longer hits the adapter with stale read_only and gets a confusing 403.

New tests (internal/admin/sqs_handler_test.go, 5 cases):

  • LivePromotion — JWT=read_only, store=full → 204 (the bug)
  • LiveRevocation — JWT=full, store=read_only → 403, source not called
  • KeyRemovedFromStore → 403, source not called
  • NoRoleStore — falls back to JWT role
  • ListQueues_EmptyArrayNotNull — guards the [] vs null regression from earlier round

Verification

  • go build ./... clean
  • go test ./internal/admin/... . passing
  • golangci-lint run ./adapter/... ./internal/admin/... ./... → 0 issues

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read internal/admin/sqs_handler.go — verify principalForWrite implementation
  • Read internal/admin/sqs_handler_test.go — verify 5 new test cases
  • Read adapter/sqs_admin.go — check created_at timestamp (Codex P2 open)
  • Read main_admin.go and main.go — verify rebase conflict resolutions
  • Read internal/admin/server.go — verify keyviz wiring conflicts resolved
  • Compare with Dynamo handler pattern
  • Post findings

Confirmed fixes ✓

P1 — principalForWrite live-role forwardingsqs_handler.go:237–273 is correct and clean. When a RoleStore is configured, the live role is the sole gate and principal.Role = live is written before returning — so AdminDeleteQueue always receives the live role, never the JWT's. The no-rolestore path (} else if !principal.Role.AllowsWrite()) correctly falls back to JWT semantics for single-tenant deployments. The comment accurately traces the bug and the fix.

5 new testssqs_handler_test.go:

  • TestSqsHandler_DeleteQueue_LivePromotion (line 69) — JWT=read_only, store=full → 204; checks that src.lastDeletePrincipal.Role == RoleFull. Pins the exact bug.
  • TestSqsHandler_DeleteQueue_LiveRevocation (line 95) — JWT=full, store=read_only → 403; source not reached.
  • TestSqsHandler_DeleteQueue_KeyRemovedFromStore (line 115) — key absent from store → 403; source not reached.
  • TestSqsHandler_DeleteQueue_NoRoleStore (line 134) — two subcases: JWT-full succeeds, JWT-read_only is rejected.
  • TestSqsHandler_ListQueues_EmptyArrayNotNull (line 162) — "queues":[] not null.

All five are logically correct and the stub's lastDeletePrincipal recorder is the right observation point.

Rebase conflict resolutionsmain.go captures sqsServer from startSQSServer return and threads it through correctly (main.go:1371–1374). main_admin.go bridge is clean. server.go resourceHandlerFor refactor is a genuine improvement that keeps dispatch under the cyclop ceiling.


P1 — created_at uses HLC, not the canonical wall-clock field

adapter/sqs_admin.go:75:

CreatedAt: hlcToTime(meta.CreatedAtHLC),

sqsQueueMeta has two timestamp fields and the struct comment is explicit about which is correct for wall-clock display:

// CreatedAtMillis / LastModifiedAtMillis are wall-clock timestamps
// surfaced by GetQueueAttributes (AWS reports them in second
// granularity). HLC is unsuitable for this — it is a logical
// counter, not a wall clock — so we record the local Now() at
// commit time and trust HLC monotonicity to keep ordering sane.
CreatedAtMillis int64 `json:"created_at_millis,omitempty"`

The SigV4 path confirms the correct field (sqs_catalog.go:942):

if created := meta.CreatedAtMillis; created > 0 {
    all["CreatedTimestamp"] = strconv.FormatInt(created/sqsMillisPerSecond, 10)
}

The admin path contradicts this by using hlcToTime(meta.CreatedAtHLC). Two failure modes:

  1. Older records where CreatedAtHLC == 0 (stored before HLC was populated, or set with zero): hlcToTime(0) returns time.Unix(0, 0).UTC() which is not a zero time.Time, so it is not omitted by omitempty — the SPA renders 1/1/1970 instead of "—".
  2. New records where both fields are set: the HLC physical half approximates wall time but could differ by up to hlcPhysicalWindowMs = 3s from CreatedAtMillis. The per-struct comment calls this out as "unsuitable."

Fix in adapter/sqs_admin.go:

var createdAt time.Time
if meta.CreatedAtMillis > 0 {
    createdAt = time.UnixMilli(meta.CreatedAtMillis).UTC()
}
summary := &AdminQueueSummary{
    Name:       name,
    IsFIFO:     meta.IsFIFO,
    Generation: meta.Generation,
    CreatedAt:  createdAt,
    Attributes: metaAttributesForAdmin(meta),
    Counters:   AdminQueueCounters(counters),
}

Zero time.Time{} propagates through the bridge as-is; the SPA's existing detail.data.created_at ? … : "—" guard handles it correctly because goccy/go-json omits zero time.Time with omitempty (year-1 is the JSON zero, not epoch).

Fix this →


Minor observation — principalForWrite checks live store before JWT (intentional asymmetry with Dynamo)

DynamoHandler.principalForWrite checks the JWT role first (!principal.Role.AllowsWrite() at line 424) and then checks the live store. This means a key promoted read_only → full would still get 403 from the Dynamo handler until re-login — the JWT pre-check blocks the live-store check.

SqsHandler.principalForWrite inverts this: when a RoleStore is configured, the live store is checked first; JWT role is only the fallback when no store is configured. This is demonstrably more correct (it's why TestSqsHandler_DeleteQueue_LivePromotion passes), but it means the two handlers have different semantics for the promotion case. Worth a follow-up to fix DynamoHandler.principalForWrite to use the same ordering — out of scope here, but SqsHandler has the better behavior.


Everything else ✓

  • translateAdminQueuesError isLeaderChurnError arm confirmed at main_admin.go:214–226.
  • Three TestTranslateAdminQueuesError_* tests are exact mirrors of the Dynamo suite.
  • resourceHandlerFor refactor in server.go is clean; the t.keyviz != nil guard is correct defensive code (keyviz is always non-nil in production but the check prevents a panic if a future test leaves it unset).
  • isSqsPath helper (server.go:401) correctly reuses the handler's own path constants.
  • Frontend changes are identical in shape to the previous round; created_at null-check in SqsDetail.tsx:68 (detail.data.created_at ? … : "—") will correctly show "—" for zero time once the P1 fix lands.

Bottom line: The principalForWrite P1 is clean. The only remaining issue before merge is the created_at field using HLC instead of the canonical wall-clock CreatedAtMillis.

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: d665c4bfff

ℹ️ 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 adapter/sqs_admin.go
Comment on lines +115 to +119
out := map[string]string{
"VisibilityTimeout": strconv.FormatInt(meta.VisibilityTimeoutSeconds, 10),
"MessageRetentionPeriod": strconv.FormatInt(meta.MessageRetentionSeconds, 10),
"DelaySeconds": strconv.FormatInt(meta.DelaySeconds, 10),
"ReceiveMessageWaitTimeSeconds": strconv.FormatInt(meta.ReceiveMessageWaitSeconds, 10),
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 Expose full non-counter queue attributes in admin describe

The helper says it mirrors GetQueueAttributes("All") minus counters, but this map omits non-counter fields that queueMetaToAttributes includes, notably QueueArn, CreatedTimestamp, and LastModifiedTimestamp. That makes admin describe payloads incomplete versus the canonical SQS attribute surface and breaks parity checks between admin and SigV4 outputs.

Useful? React with 👍 / 👎.

Claude P1 on PR #670 sixth-round review. AdminDescribeQueue was
producing CreatedAt from hlcToTime(meta.CreatedAtHLC), but
sqsQueueMeta documents HLC as "unsuitable for wall-clock display"
and the SigV4 path (sqs_catalog.go:942) reads CreatedAtMillis.

Two failure modes the old code exhibited:

1. Records where CreatedAtHLC == 0 (older entries, or zero by
   accident) rendered as 1970-01-01T00:00:00Z because hlcToTime(0)
   returns time.Unix(0, 0).UTC() -- not a Go zero time, so
   omitempty would not drop the field and the SPA created_at guard
   (created_at ? formatted : "-") would happily render the epoch
   as a real date.

2. Even with both fields populated, the HLC physical half can
   differ from the wall-clock CreatedAtMillis by up to
   hlcPhysicalWindowMs (3 s).

Fix: extract adminQueueSummary helper that derives CreatedAt as
time.UnixMilli(meta.CreatedAtMillis).UTC() when positive, else a
zero time.Time. Mirrors the SigV4 catalog path. New unit tests pin
both branches:

- TestAdminQueueSummary_CreatedAtUsesMillisNotHLC/zero_millis_yields_zero_time_even_with_HLC_populated
- TestAdminQueueSummary_CreatedAtUsesMillisNotHLC/positive_millis_round-trips_via_time.UnixMilli_UTC
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Pushed addressing the P1 from your sixth-round review.

P1 — CreatedAt uses CreatedAtMillis, not HLC

adapter/sqs_admin.go now extracts an adminQueueSummary(name, meta, counters) helper. CreatedAt is derived from the canonical wall-clock meta.CreatedAtMillis (mirroring the SigV4 path at sqs_catalog.go:942):

var createdAt time.Time
if meta.CreatedAtMillis > 0 {
    createdAt = time.UnixMilli(meta.CreatedAtMillis).UTC()
}

Both failure modes you flagged are now addressed:

  1. meta.CreatedAtMillis == 0 → returns a Go zero time.Time{}, which omitempty drops, and the SPA renders "—" via its existing created_at ? formatted : "—" guard. The previous code returned time.Unix(0, 0).UTC() from hlcToTime(0), which is not a Go zero time and rendered as 1970-01-01T00:00:00Z.
  2. meta.CreatedAtMillis > 0 → exact wall-clock round-trip via time.UnixMilli. No more HLC-physical/wall-clock skew (hlcPhysicalWindowMs = 3s).

New tests (adapter/sqs_admin_test.go):

  • TestAdminQueueSummary_CreatedAtUsesMillisNotHLC/zero_millis_yields_zero_time_even_with_HLC_populated — pins regression Configure Renovate #1 by setting CreatedAtHLC = 42 << s3HLCPhysicalShift, CreatedAtMillis = 0 and asserting summary.CreatedAt.IsZero().
  • TestAdminQueueSummary_CreatedAtUsesMillisNotHLC/positive_millis_round-trips_via_time.UnixMilli_UTC — pins regression Update dependency go to v1.21.4 #2 by asserting the exact time.UnixMilli(wantMillis).UTC() round-trip and the UTC location.

Verified:

  • go build ./... clean
  • go test -run TestAdminQueueSummary ./adapter/... → ok
  • golangci-lint run (pre-commit) → 0 issues
  • go vet ./... clean

The minor observation about DynamoHandler.principalForWrite JWT-first ordering is filed mentally as a follow-up — agreed it should mirror the SQS handler's live-store-first behaviour, but out of scope for this PR.

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read adapter/sqs_admin.go — verify CreatedAt P1 fix and adminQueueSummary helper
  • Read adapter/sqs_admin_test.go — verify two new CreatedAt tests
  • Assess Codex P2: metaAttributesForAdmin attribute completeness vs queueMetaToAttributes
  • Read internal/admin/sqs_handler.go — confirm principalForWrite and prior fixes intact
  • Post findings

Confirmed fix ✓

P1 — CreatedAt uses wall-clock CreatedAtMillis, not HLCadapter/sqs_admin.go:83–86 is correct:

var createdAt time.Time
if meta.CreatedAtMillis > 0 {
    createdAt = time.UnixMilli(meta.CreatedAtMillis).UTC()
}

Both failure modes are pinned by the two new tests (sqs_admin_test.go:24–55):

  1. zero_millis_yields_zero_time_even_with_HLC_populated — sets CreatedAtHLC = 42 << s3HLCPhysicalShift, CreatedAtMillis = 0, asserts summary.CreatedAt.IsZero(). Pins the epoch-1970 regression.
  2. positive_millis_round-trips_via_time.UnixMilli_UTC — exact round-trip and UTC location check. Pins the HLC skew regression.

The adminQueueSummary helper extraction is clean and the comment accurately traces the prior bug and the fix.

All prior fixes (principalForWrite live-role forwarding, translateAdminQueuesError leader-churn arm, nil→[] normalisation) are confirmed intact.


P1 — metaAttributesForAdmin docstring contradicts its output

adapter/sqs_admin.go:124–126 says:

renders the queue meta into the same shape queueMetaToAttributes("All") would, minus the counters

Compared against queueMetaToAttributes (sqs_catalog.go:925–967), three fields are absent:

Field Source Gap
QueueArn s.queueArn(name)"arn:aws:sqs:"+region+":000000000000:"+name Absent
CreatedTimestamp meta.CreatedAtMillis / sqsMillisPerSecond Absent (but created_at is surfaced as a typed field)
LastModifiedTimestamp meta.LastModifiedAtMillis / sqsMillisPerSecond Absent — not surfaced anywhere

CreatedTimestamp is redundant with the dedicated AdminQueueSummary.CreatedAt field, so its absence from the attributes map is defensible and the SPA renders it correctly from detail.data.created_at. No real gap there.

LastModifiedTimestamp is the live defect: it's missing from both the typed struct and the attributes map, so operators have no way to see when a queue was last modified from the admin UI — SetQueueAttributes updates LastModifiedAtMillis (sqs_catalog.go:1036) but that value never surfaces here.

QueueArn is architecturally constrained — adminQueueSummary is a pure helper (by design, for unit-testability) and doesn't have the server's region. However, the ARN formula is mechanical ("arn:aws:sqs:" + region + ":000000000000:" + name); it could be passed as a parameter to metaAttributesForAdmin from AdminDescribeQueue, which does have access to s.effectiveRegion().

Minimal fix — corrects the two real gaps and the docstring:

// metaAttributesForAdmin renders the non-counter queue config attributes.
// Mirrors queueMetaToAttributes("All") except counters (surfaced as a typed
// struct) and CreatedTimestamp (surfaced as AdminQueueSummary.CreatedAt).
// Includes QueueArn and LastModifiedTimestamp.
func metaAttributesForAdmin(meta *sqsQueueMeta, queueArn string) map[string]string {
    out := map[string]string{
        "QueueArn":                      queueArn,
        "VisibilityTimeout":             strconv.FormatInt(meta.VisibilityTimeoutSeconds, 10),
        // ... (existing fields unchanged) ...
    }
    if meta.LastModifiedAtMillis > 0 {
        out["LastModifiedTimestamp"] = strconv.FormatInt(meta.LastModifiedAtMillis/sqsMillisPerSecond, 10)
    }
    if meta.RedrivePolicy != "" {
        out["RedrivePolicy"] = meta.RedrivePolicy
    }
    return out
}

Then adminQueueSummary passes s.queueArn(name) through — but adminQueueSummary is currently a package-level function, not a method. The cleanest approach is either to pass queueArn string as a fourth parameter to adminQueueSummary, or to move the metaAttributesForAdmin call up into AdminDescribeQueue (which is a method and has s.queueArn).

Fix this →


Everything else ✓

  • principalForWrite live-role forwarding confirmed at sqs_handler.go:246–266.
  • translateAdminQueuesError isLeaderChurnError arm — confirmed at main_admin.go:214–226.
  • Three TestTranslateAdminQueuesError_* tests — confirmed.
  • nil→[] normalisation at sqs_handler.go:167–169 — confirmed.
  • scanApproxCounters error (sqs_admin.go:69) — still unwrapped, but scanApproxCounters wraps internally and the linter is satisfied; no change needed.
  • Both new TestAdminQueueSummary_CreatedAtUsesMillisNotHLC subtests are logically correct and use s3HLCPhysicalShift which is the correct constant for this package.
  • SPA subtitle (SqsList.tsx) — confirmed clean operator prose, no source path or milestone reference.

Bottom line: The CreatedAt P1 and its tests are correct. The one remaining issue before merge is that metaAttributesForAdmin is missing QueueArn and LastModifiedTimestamp while its docstring claims parity with queueMetaToAttributes("All"). LastModifiedTimestamp is the live gap for operators; QueueArn requires passing the ARN as a parameter.

Claude P1 on PR #670 seventh-round review. metaAttributesForAdmin
docstring claimed parity with queueMetaToAttributes("All") but
QueueArn and LastModifiedTimestamp were absent.

LastModifiedTimestamp is the live operator-visible gap:
SetQueueAttributes updates meta.LastModifiedAtMillis but the value
never surfaced in the admin UI, so operators had no way to tell
when a queue config was last touched.

QueueArn is the AWS-shaped identifier the SPA shows for change
tracking; the SigV4 path always emits it.

CreatedTimestamp deliberately stays out of the attrs map -- it is
surfaced as the typed AdminQueueSummary.CreatedAt field instead so
the SPA does not have to round-trip strings.

adminQueueSummary now takes queueArn as a fourth argument; the
caller (AdminDescribeQueue) threads s.queueArn(name) through. The
helper stays method-free so it remains unit-testable without a
coordinator. Updated docstrings to match.

New tests in adapter/sqs_admin_test.go pin the contract:

- TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified/QueueArn_always_present
- /LastModifiedTimestamp_emitted_in_unix_seconds_when_populated
- /LastModifiedTimestamp_omitted_when_zero
- /CreatedTimestamp_deliberately_not_in_map_(typed_field_instead)

Existing TestAdminQueueSummary_CreatedAtUsesMillisNotHLC subtests
updated for the new signature.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Pushed addressing the seventh-round P1.

P1 — metaAttributesForAdmin parity gap (QueueArn, LastModifiedTimestamp)

Both fields are now emitted, mirroring queueMetaToAttributes("All") (sqs_catalog.go:932-948):

out := map[string]string{
    "QueueArn":                      queueArn,
    // ... existing config fields ...
}
if mod := meta.LastModifiedAtMillis; mod > 0 {
    out["LastModifiedTimestamp"] = strconv.FormatInt(mod/sqsMillisPerSecond, 10)
}

adminQueueSummary now takes queueArn string as a fourth argument; the caller AdminDescribeQueue threads s.queueArn(name) through. The helper stays method-free so it remains unit-testable without a coordinator (you flagged this as a constraint in your finding).

CreatedTimestamp is deliberately not in the attrs map — the typed AdminQueueSummary.CreatedAt field is canonical so the SPA does not have to round-trip strings. The updated docstring spells this out: parity with queueMetaToAttributes("All") except for the counters (typed struct) and CreatedTimestamp (typed field). This was the one piece of the prior docstring's "parity" claim that I think is still worth keeping, but now it's explicit rather than hidden.

New tests (adapter/sqs_admin_test.go):

  • TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified/QueueArn_always_present — emits the ARN even when nothing else changes.
  • LastModifiedTimestamp_emitted_in_unix_seconds_when_populated — pins the unit conversion (millis/1000).
  • LastModifiedTimestamp_omitted_when_zero — guards against the SPA seeing a 1970-01-01 value when the timestamp hasn't been written yet (same class of bug as the CreatedAt HLC fix).
  • CreatedTimestamp_deliberately_not_in_map_(typed_field_instead) — pins the deliberate omission so a future contributor doesn't "fix" it by re-adding the string form and creating two sources of truth.

The two existing TestAdminQueueSummary_CreatedAtUsesMillisNotHLC subtests are updated for the new signature (added testQueueArn constant).

Verified:

  • go build ./... clean
  • go test -run "TestAdminQueueSummary|TestMetaAttributesForAdmin" ./adapter/... → ok
  • golangci-lint run (pre-commit) → 0 issues

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read trigger comment and understand the task
  • Read adapter/sqs_admin.go — verify P1 fix: QueueArn + LastModifiedTimestamp in metaAttributesForAdmin, new queueArn param threading
  • Read adapter/sqs_admin_test.go — verify 4 new subtests + updated signature for existing tests
  • Read adapter/sqs_catalog.go — spot-check queueMetaToAttributes for full parity check
  • Verify all prior fixes still intact (sqs_handler.go, main_admin.go)
  • Post comprehensive review

Confirmed fix ✓

P1 — metaAttributesForAdmin parity with queueMetaToAttributes("All")

adapter/sqs_admin.go:144–162 is correct. Full parity comparison against sqs_catalog.go:925–967:

Field queueMetaToAttributes("All") metaAttributesForAdmin
VisibilityTimeout
MessageRetentionPeriod
DelaySeconds
ReceiveMessageWaitTimeSeconds
MaximumMessageSize
FifoQueue
ContentBasedDeduplication
QueueArn (now added)
CreatedTimestamp deliberately omitted (typed AdminQueueSummary.CreatedAt)
LastModifiedTimestamp ✓ (conditional > 0) (now added, same condition)
Approximate* counters deliberately omitted (typed AdminQueueCounters)
RedrivePolicy ✓ (conditional) ✓ (conditional)

Parity is now complete for all non-counter, non-CreatedTimestamp fields. Docstring (sqs_admin.go:127–143) accurately documents the two deliberate omissions with clear rationale.

adminQueueSummary signature updatequeueArn string added as 4th param, threaded from AdminDescribeQueue via s.queueArn(name) (sqs_admin.go:71). Helper stays method-free for unit-testability.

4 new subtestssqs_admin_test.go:69–125 are correct and comprehensive:

  • QueueArn_always_present — verifies field and value.
  • LastModifiedTimestamp_emitted_in_unix_seconds_when_populated — pins the millis→seconds conversion (wantMillis/sqsMillisPerSecond) with a concrete reference timestamp.
  • LastModifiedTimestamp_omitted_when_zero — guards the 1970 epoch regression class.
  • CreatedTimestamp_deliberately_not_in_map_(typed_field_instead) — excellent regression guard; pins the deliberate omission so a future contributor doesn't "fix" it back and create two sources of truth for the same field.

Existing TestAdminQueueSummary_CreatedAtUsesMillisNotHLC subtests — correctly updated for the new testQueueArn constant at sqs_admin_test.go:35 and 50.

SPA renderingSqsDetail.tsx:87–99 uses Object.entries(detail.data.attributes).map(...), so QueueArn and LastModifiedTimestamp surface automatically in the "Configuration" section with no SPA changes needed. ✓

All prior fixes intact:

  • principalForWrite live-role forwarding — confirmed at sqs_handler.go:237.
  • translateAdminQueuesError isLeaderChurnError arm — confirmed at main_admin.go:216, 228.

Nit — PR reference in production docstring (CLAUDE.md §Conventions)

sqs_admin.go:141–143:

// Claude P1 on PR #670 caught both gaps — the prior
// docstring claimed parity with queueMetaToAttributes("All") but
// QueueArn and LastModifiedTimestamp were absent.

CLAUDE.md explicitly says: "Don't reference the current task, fix, or callers... since those belong in the PR description and rot as the codebase evolves." The preceding lines already explain the rationale for each field (operators need it for change-tracking, etc.), so the PR attribution is redundant and will become stale. Drop those three lines and the docstring stands on its own. Small fix if you want to clean it up before merge, but not blocking.


Everything else ✓

  • sqsMillisPerSecond = 1000 (from sqs_messages.go:55) — the test's division is arithmetically correct.
  • adminQueueSummary is method-free as promised; the queueArn param keeps it fully unit-testable.
  • The conditional LastModifiedTimestamp guard (> 0) matches the SigV4 path at sqs_catalog.go:946 exactly.

Bottom line: The P1 is fully fixed and its tests are the best in the file. The nit about the PR reference in the docstring is cosmetic and CLAUDE.md-governed — clean it up or leave it, either way this PR is ready to merge.

CLAUDE.md Conventions: "Don't reference the current task, fix, or
callers (added for the X flow, handles the case from issue #123)
since those belong in the PR description and rot as the codebase
evolves."

Removes four PR-attribution lines added across this PR's earlier
commits:

- adapter/sqs_admin.go: metaAttributesForAdmin docstring tail.
- internal/admin/sqs_handler.go handleList: nil normalisation note.
- internal/admin/sqs_handler.go principalForWrite: docstring tail.
- main_admin.go translateAdminQueuesError: leader-churn arm note.

The why-this-line-exists rationale stays in each spot; only the
"Codex P2 + Claude P1 on PR #N caught the bug" sentences and the
sibling-PR cross-references are dropped.

Flagged as a non-blocking nit by Claude P1 on PR #670 seventh-round
review (the docstring on metaAttributesForAdmin specifically); the
other three are the same convention violation in adjacent code so
they go in the same cleanup.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Cleaned up the CLAUDE.md-governed nit you flagged ("Don't reference the current task, fix, or callers since those belong in the PR description and rot as the codebase evolves").

Dropped four PR-attribution lines added across this PR's earlier commits, in the spots where the convention applies:

  • adapter/sqs_admin.gometaAttributesForAdmin docstring tail (the one you specifically flagged).
  • internal/admin/sqs_handler.gohandleList nil-normalisation comment.
  • internal/admin/sqs_handler.goprincipalForWrite docstring tail.
  • main_admin.gotranslateAdminQueuesError leader-churn arm note.

Same cleanup pattern across all four: the why-this-line-exists rationale stays (the bug class, the failure mode, the mirror with the Dynamo path), only the "Codex P2 + Claude P1 on PR #N caught the bug" sentence and sibling-PR cross-references are removed. The history lives in the PR descriptions and git log -p for anyone who needs it later.

Verified: go build ./... clean, go test ./internal/admin/... -run TestSqs ok, golangci-lint run (pre-commit) → 0 issues.

I didn't touch the older PR references at main_admin.go:302/458/477 since those were added by earlier merged PRs (#634, #658) and out of scope here. Happy to clean those up in a follow-up if you'd like — just let me know.

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read trigger comment — understand scope of cleanup
  • Read adapter/sqs_admin.go — verify metaAttributesForAdmin docstring tail removed
  • Read internal/admin/sqs_handler.go — verify handleList nil comment + principalForWrite docstring tail removed
  • Read main_admin.go — verify translateAdminQueuesError arm attribution comment removed
  • Confirm all prior fixes (CreatedAt, QueueArn, leader-churn, live-role) intact
  • Post final assessment

Confirmed cleanup ✓

All four attribution removals landed cleanly:

  1. adapter/sqs_admin.go metaAttributesForAdmin docstring (sqs_admin.go:127–141) — No "Claude P1 on PR #670 caught both gaps" line. The parity rationale and deliberate-omission reasoning are fully preserved.

  2. internal/admin/sqs_handler.go handleList nil-normalization comment (sqs_handler.go:162–165) — Comment reads as pure operational rationale: why null would crash the SPA and why the normalisation belongs here. No PR reference.

  3. internal/admin/sqs_handler.go principalForWrite docstring (sqs_handler.go:221–235) — Explains the bug class (JWT-stale vs. live-store role), the fix, and the DynamoHandler mirror. No PR or reviewer attribution.

  4. main_admin.go translateAdminQueuesError leader-churn arm (main_admin.go:217–225) — Explains the exact failure path (leader lost between the upfront check and coordinator dispatch, retry loop doesn't catch it) and the Dynamo-side mirror. No "Codex P2" or "Claude P1" reference.

Grep confirms the only remaining PR/reviewer attributions in main_admin.go are at lines 246/300/456/475, which are pre-existing from already-merged PRs #648/#658/#634 — correctly left untouched.


All prior fixes still intact ✓

Fix Location Status
CountersTruncated removed sqs_handler.go:27–34
nil→[] normalization sqs_handler.go:166–168
principalForWrite live-role forwarding sqs_handler.go:236–271
translateAdminQueuesError leader-churn arm main_admin.go:216–226
CreatedAt uses CreatedAtMillis not HLC sqs_admin.go:86–89
metaAttributesForAdmin has QueueArn + LastModifiedTimestamp sqs_admin.go:142–160
SPA subtitle — clean operator prose SqsList.tsx:13–15

This PR is ready to merge. Every finding across all eight review rounds has been addressed. The codebase is in the state the CLAUDE.md conventions require: no PR/reviewer attributions in production comments, full parity between the admin and SigV4 surfaces, live-role forwarding on delete, and 503+Retry-After on leader churn.

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: 0f0e26b992

ℹ️ 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 internal/admin/sqs_handler.go Outdated
Name string `json:"name"`
IsFIFO bool `json:"is_fifo"`
Generation uint64 `json:"generation"`
CreatedAt time.Time `json:"created_at,omitempty"`
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 Make created_at truly optional in SQS queue responses

created_at is declared as time.Time with omitempty, but this handler uses goccy/go-json, which still serializes zero time.Time values (e.g. "0001-01-01T00:00:00Z") instead of omitting them. For queues where CreatedAtMillis is unset, the API therefore returns a bogus timestamp and the SPA renders a fake ancient date instead of , so the current "zero time means omitted" logic does not hold.

Useful? React with 👍 / 👎.

Comment thread web/admin/src/pages/SqsDetail.tsx Outdated
const [deleting, setDeleting] = useState(false);
const [deleteError, setDeleteError] = useState<string | null>(null);
const navigate = useNavigate();
const writeAllowed = session?.role === "full";
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 Avoid gating SQS delete action on stale JWT role

The delete button is shown only when session?.role === "full", but that role is captured at login and can become stale while the token is still valid. In the promoted-key scenario (read_only → full), backend delete now succeeds via live role revalidation, yet the SPA still hides the action until re-login, so operators cannot perform queue deletes from the UI despite being currently authorized.

Useful? React with 👍 / 👎.

…ate (Codex P2)

Two P2 findings from the Codex review on commit 0f0e26b:

(1) created_at "0001-01-01T00:00:00Z" leak. QueueSummary.CreatedAt
was time.Time with json:"created_at,omitempty", but neither
encoding/json nor goccy/go-json drops a zero time.Time value -- it
serialises as "0001-01-01T00:00:00Z" and the SPA renders a fake
ancient date instead of the "--" placeholder its
created_at ? formatted : "--" guard implies.

Fix: changed CreatedAt to *time.Time on the wire-facing
QueueSummary struct. The bridge in main_admin.go converts a zero
time.Time on the adapter side to nil so omitempty drops the field
at the wire layer. New regression test
TestSqsHandler_DescribeQueue_ZeroCreatedAtIsOmittedOnTheWire
exercises the wire body and asserts the literal "0001-01-01..."
string never appears, complementing the Go-side IsZero() check the
adapter unit test already pins.

(2) SPA delete button hidden on JWT role staleness. The promoted
read_only -> full key path principalForWrite added in this PR
correctly forwards the live role to AdminDeleteQueue, but
SqsDetail.tsx gated the delete button on session?.role === "full"
(the JWT role cached in sessionStorage at login). Promoted users
saw the button hidden until re-login, defeating the live-role fix
end-to-end.

Fix: dropped the JWT-based gating on SqsDetail. The backend's
principalForWrite is the authoritative gate. A read_only operator
who clicks delete gets 403 in the modal's error area, which is
clearer feedback than a silently missing button. (S3List/S3Detail
and DynamoList/DynamoDetail have the same pattern but are out of
scope for this PR -- they need the same treatment in a follow-up
or a shared session-refresh endpoint.)
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

Codexの2件のP2指摘を反映しました。

P2 (1) — created_at omitempty が time.Time 値型では機能しない

Codexの指摘どおり、encoding/jsongoccy/go-jsontime.Time のゼロ値を omitempty で落とさず "0001-01-01T00:00:00Z" を出力します。検証もしました(/tmp/zerotime_check.go)。

internal/admin/sqs_handler.goQueueSummary.CreatedAt*time.Time に変更。main_admin.goconvertAdminQueueSummary ブリッジで adapter 側の zero time.Timenil に畳み込み、omitempty がワイヤレベルで実際にフィールドを落とすようにしました。

新しい回帰テスト TestSqsHandler_DescribeQueue_ZeroCreatedAtIsOmittedOnTheWire はレスポンスボディを直接見て "0001-01-01T00:00:00Z" 文字列が出ないこと、"created_at": キー自体が出ないことを確認します。既存の adapter ユニットテストの IsZero() チェックは Go 側のロジックを保証しますが、ワイヤフォーマットの保証は別レイヤなのでここで追加。

P2 (2) — SPA の delete ボタンが JWT の古いロールで隠れる

このPRで入れた principalForWrite のライブロール検証はバックエンドで正しく動くのですが、SqsDetail.tsx 側で session?.role === "full" で UI ゲートしていたため、read_only → full に昇格された操作者は再ログインまでボタンが見えない状態でした。せっかくのライブロール検証が end-to-end で機能していませんでした。

SqsDetail.tsx の JWT ベースの UI ゲートを削除。バックエンドの principalForWrite を権威ある authorization gate として扱い、read_only 操作者がボタンを押した場合は modal の error area に 403 を表示。これは「ボタンが黙って消える」より明確なフィードバックです。

なお S3List/S3Detail/DynamoList/DynamoDetail も同じパターンですが、このPR の scope 外なので follow-up または共通の /me セッションリフレッシュエンドポイントで一斉に直すべき問題として残します。

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read trigger comment — two Codex P2 fixes to review
  • Read internal/admin/sqs_handler.go*time.Time pointer change + wire omit
  • Read internal/admin/sqs_handler_test.go — new wire-format test
  • Read main_admin.go — nil-bridge in convertAdminQueueSummary
  • Read web/admin/src/pages/SqsDetail.tsx — JWT-gate removal
  • Verify correctness and secondary effects
  • Post comprehensive review

Confirmed fixes ✓

P2 (1) — *time.Time + nil-bridge: wire omit is now correct

internal/admin/sqs_handler.go:39:

CreatedAt  *time.Time        `json:"created_at,omitempty"`

main_admin.go:191–195:

var createdAt *time.Time
if !in.CreatedAt.IsZero() {
    t := in.CreatedAt
    createdAt = &t
}

The three-way behavior is fully correct:

  • Adapter returns zero time.Time (no CreatedAtMillis) → nil pointer → field omitted from JSON wire → SPA renders "—". ✓
  • Adapter returns non-zero time.Time → non-nil pointer → field present on wire. ✓
  • Bridge allocates t := in.CreatedAt (copy, not alias) before taking &t — correct, no aliasing hazard. ✓

Adapter side (adapter/sqs_admin.go:24) correctly stays time.Time (not pointer); the conversion is the bridge's job, which is exactly where it should be.

TestSqsHandler_DescribeQueue_ZeroCreatedAtIsOmittedOnTheWire (sqs_handler_test.go:182–197): stub returns &QueueSummary{Name: name} with CreatedAt == nil (pointer zero), test asserts both that "0001-01-01T00:00:00Z" is absent and that "created_at": key is entirely absent. This exercises the wire layer (not the adapter's IsZero() logic), which is the right observation point for this regression. ✓

P2 (2) — SPA delete button ungated from JWT role

SqsDetail.tsx:45–53: Delete button now rendered for any loaded queue (detail.data && (…)), with no session?.role check. The comment at lines 14–22 explains the design decision clearly: backend's live-role gate is authoritative; a read_only user who tries gets a modal 403 rather than a silently hidden button. Error surfacing via {deleteError && <div … text-danger>{deleteError}</div>} at line 116. ✓


Nit — PR/reviewer attributions in test docstrings (CLAUDE.md §Conventions)

The prior cleanup round cleared these from production code. The test file still has them at three spots:

sqs_handler_test.go:16:

// Mirrors stubTablesSource in dynamo_handler_test.go: records the
// principal that flowed through to the source so tests can assert
// live-role forwarding (Codex P2 + Claude P1 on PR #670).

sqs_handler_test.go:161:

// TestSqsHandler_ListQueues_EmptyArrayNotNull pins the nil→[]
// normalisation added in the Gemini PR #670 medium fix. Without it

sqs_handler_test.go:177–181:

// TestSqsHandler_DescribeQueue_ZeroCreatedAtIsOmittedOnTheWire pins
// the wire-level fix for the Codex P2 finding on PR #670: time.Time

CLAUDE.md: "Don't reference the current task, fix, or callers … since those belong in the PR description and rot as the codebase evolves." The rationale in each docstring (what the bug was, what the test pins) is genuinely useful; it's only the "Codex P2 + Claude P1 on PR #670" / "Gemini PR #670 medium fix" attributions that should come out. Small cosmetic fix — same class as the prior round's cleanup. Not blocking.

Fix this →


Everything else ✓

  • convertAdminQueueSummary nil-guard at line 188 is correct (handles a nil input from a future caller).
  • SqsList.tsx is read-only (links only, no delete action) — the JWT-gate concern doesn't apply here. ✓
  • goccy/go-json correctly omits nil pointer fields with omitempty (unlike zero time.Time); the test confirms the wire behaviour. ✓
  • All prior fixes (principalForWrite live-role, translateAdminQueuesError leader-churn arm, nil→[] list normalisation, CreatedAtMillis vs HLC, QueueArn+LastModifiedTimestamp parity) are intact. ✓

Bottom line: Both P2 fixes are correct and well-tested. The only remaining item is the cosmetic PR-attribution cleanup in three test docstrings — same CLAUDE.md convention as the prior round's production-code cleanup, missed in the test file.

Same CLAUDE.md cleanup as 0f0e26b applied to the test file: the
prior round cleaned production code but left the test file
references behind. Why-rationale stays (the bug class, the contract
the test pins); only the "Codex P2 + Claude P1 on PR #670",
"Gemini PR #670 medium fix", and "Codex P2 finding on PR #670"
attributions are dropped.

Three docstrings touched:
- stubQueuesSource live-role recorder.
- TestSqsHandler_DeleteQueue_LivePromotion.
- TestSqsHandler_ListQueues_EmptyArrayNotNull.
- TestSqsHandler_DescribeQueue_ZeroCreatedAtIsOmittedOnTheWire.
@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

テストファイル側の PR/reviewer attribution を CLAUDE.md convention に合わせて削除しました(7c63a7b0)。前回の cleanup (0f0e26b9) で production コードからは消したのですが、test docstring 4箇所が残っていました。

各 docstring の why-rationale (バグの分類、テストが pin する contract) は保持し、Codex P2 + Claude P1 on PR #670 / Gemini PR #670 medium fix / Codex P2 finding on PR #670 の attribution 部分のみを削除しました。

Verified: go test ./internal/admin/... -run TestSqs → ok, golangci-lint run (pre-commit) → 0 issues.

これで全 review round の指摘がクリアになったはずです。マージ可能です。

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read trigger comment — final cleanup of test docstring attributions (7c63a7b0)
  • Verify three attribution removals in internal/admin/sqs_handler_test.go
  • Identify three remaining PR attributions in adapter/sqs_admin_test.go and main_admin_test.go
  • Prepare fix commit (local; push blocked by repo permissions — see nit below)
  • Comprehensive final pass across all changed files

Confirmed — 7c63a7b0 fixes ✓

All three locations from the previous round's nit are clean:


All prior round fixes confirmed intact ✓

Fix Location Status
CountersTruncated absent sqs_handler.go:35–42
nil→[] list normalisation sqs_handler.go:174–176
principalForWrite live-role forwarding sqs_handler.go:244–279
translateAdminQueuesError leader-churn arm main_admin.go:227–237
CreatedAt uses CreatedAtMillis, not HLC sqs_admin.go:86–88
metaAttributesForAdmin has QueueArn + LastModifiedTimestamp sqs_admin.go:142–160
*time.Time pointer + nil-bridge in convertAdminQueueSummary sqs_handler.go:39, main_admin.go:192–196
SPA delete button ungated from JWT role SqsDetail.tsx:14–21
SPA subtitle — clean operator prose SqsList.tsx:13–15

Nit — three remaining PR attributions in adapter test file and main_admin_test.go

The prior cleanup round (7c63a7b0) caught the three instances in sqs_handler_test.go but missed these:

adapter/sqs_admin_test.go:11–14 — change:

// TestAdminQueueSummary_CreatedAtUsesMillisNotHLC pins the regression
// from the fifth-round Claude review on PR #670: the admin
// AdminDescribeQueue path was producing CreatedAt from
// hlcToTime(meta.CreatedAtHLC), but sqsQueueMeta documents HLC as

to:

// TestAdminQueueSummary_CreatedAtUsesMillisNotHLC pins the contract that
// AdminDescribeQueue derives CreatedAt from CreatedAtMillis, not HLC.
// sqsQueueMeta documents HLC as

adapter/sqs_admin_test.go:60–67 — change:

// TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified pins the
// gap from the seventh-round Claude review on PR #670: the prior
// metaAttributesForAdmin docstring claimed parity with
// queueMetaToAttributes("All") but neither QueueArn nor
// LastModifiedTimestamp was emitted. QueueArn is the AWS-shaped
// identifier the SPA shows for change-tracking; LastModifiedTimestamp
// is updated on SetQueueAttributes and is the only handle operators
// have on "when did somebody last touch this queue's config".

to:

// TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified pins parity
// with queueMetaToAttributes("All"): metaAttributesForAdmin must emit
// QueueArn (the AWS-shaped identifier the SPA shows) and
// LastModifiedTimestamp (updated on SetQueueAttributes and the only
// handle operators have on "when did somebody last touch this queue's
// config").

main_admin_test.go:436 — remove the last sentence of the TestTranslateAdminQueuesError_LeaderChurn comment:

// Codex P2 + Claude P1 on PR #670 confirmed the original gap.

I prepared these three edits as a local commit (15d2f63) but push was blocked by repo permissions. The changes are purely cosmetic doc cleanup — same class as the prior rounds. Not blocking; apply the diff above and this PR is ready to merge.


Bottom line: Every substantive finding across all review rounds is confirmed fixed. The three remaining cosmetic lines are in test docstrings only. Once those are pushed, the PR is clean end-to-end.

Catches the three locations the prior round (7c63a7b) missed:
adapter/sqs_admin_test.go (TestAdminQueueSummary_CreatedAtUsesMillisNotHLC,
TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified) and
main_admin_test.go (TestTranslateAdminQueuesError_LeaderChurn). Same
pattern: the why-rationale stays, only the "fifth-round Claude
review on PR #670" / "seventh-round Claude review on PR #670" /
"Codex P2 + Claude P1 on PR #670" attributions are dropped.

Pre-existing PR #634 references at main_admin_test.go:373 / :397
were added by a prior merged PR and are out of scope here.
@chatgpt-codex-connector
Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@bootjp
Copy link
Copy Markdown
Owner Author

bootjp commented Apr 26, 2026

最後の3箇所をクリーンアップしました(5ee01753)。bot は repo permission で push できなかったとのことだったので、こちらから push しています。

  • adapter/sqs_admin_test.goTestAdminQueueSummary_CreatedAtUsesMillisNotHLC (fifth-round attribution 削除)、TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified (seventh-round attribution 削除)
  • main_admin_test.goTestTranslateAdminQueuesError_LeaderChurn (Codex P2 + Claude P1 attribution 削除)

Each docstring の why-rationale (バグ class、test が pin する contract) は保持。

main_admin_test.go:373 / :397 の PR #634 references は前のマージ済み PR で入ったもので、このPRの scope 外なので触っていません。

Verified: go test -run "TestAdminQueueSummary|TestMetaAttributesForAdmin|TestTranslateAdminQueuesError" ./... ok, golangci-lint run (pre-commit) → 0 issues.

これで全ての review round の指摘がクリアです。

@claude review

@claude
Copy link
Copy Markdown

claude Bot commented Apr 26, 2026

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


Review

  • Read adapter/sqs_admin_test.go — verify two attribution removals (5ee01753)
  • Read main_admin_test.go — verify one attribution removal
  • Final integrity check across all changed files
  • Post findings

Confirmed cleanup ✓

All three attribution removals from 5ee01753 landed correctly:

adapter/sqs_admin_test.go:11–23TestAdminQueueSummary_CreatedAtUsesMillisNotHLC docstring: no "fifth-round" or "Claude review on PR #670" phrase. The why-rationale (two failure modes the test pins) is fully preserved. ✓

adapter/sqs_admin_test.go:61–67TestMetaAttributesForAdmin_IncludesQueueArnAndLastModified docstring: no "seventh-round" or "Claude review on PR #670" phrase. The parity contract explanation is preserved. ✓

main_admin_test.go:430–435TestTranslateAdminQueuesError_LeaderChurn docstring: no "Codex P2 + Claude P1 on PR #670". The failure-path rationale (upfront check vs coordinator dispatch gap, 503 vs 500) is preserved. ✓

The two remaining Codex P2 on PR #634 references at main_admin_test.go:373 and :397 are in the pre-existing TestTranslateAdminTables* tests from the already-merged PR #634 — correctly left untouched.


All functional fixes confirmed intact ✓

Fix Location Status
CreatedAt uses CreatedAtMillis not HLC sqs_admin.go:87–88
metaAttributesForAdmin has QueueArn + LastModifiedTimestamp sqs_admin.go:142–160
*time.Time pointer + nil-bridge in convertAdminQueueSummary sqs_handler.go:39, main_admin.go:184–196
principalForWrite live-role forwarding sqs_handler.go:244
translateAdminQueuesError leader-churn arm (isLeaderChurnError) main_admin.go:222–237
nil→[] list normalisation sqs_handler.go:174
SPA delete button ungated from JWT role SqsDetail.tsx:14–21
SPA subtitle — clean operator prose SqsList.tsx:13–15
CountersTruncated absent sqs_handler.go

This PR is ready to merge. All findings across all review rounds are confirmed resolved: no PR/reviewer attributions remain in any of the newly-added files, all functional correctness fixes are intact, and the test suite covers every fixed regression path.

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: 7c63a7b09d

ℹ️ 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 adapter/sqs_admin.go
Comment on lines +56 to +57
if strings.TrimSpace(name) == "" {
return nil, false, ErrAdminSQSValidation
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 Validate admin queue names against SQS naming rules

AdminDescribeQueue currently treats any non-blank string as a valid queue name, so malformed names (for example bad*name or overlength names) flow into metadata lookup and are reported as not found instead of a 400 validation error. That contradicts this file’s own ErrAdminSQSValidation contract (“missing or syntactically-bad queue name”) and diverges from the regular SQS path that uses validateQueueName; AdminDeleteQueue has the same gap. Reusing canonical queue-name validation here would preserve consistent client-visible semantics for invalid input.

Useful? React with 👍 / 👎.

@bootjp bootjp merged commit b1b7a83 into main Apr 26, 2026
11 checks passed
@bootjp bootjp deleted the feat/sqs-admin-queues-handler branch April 26, 2026 17:51
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