Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/orbit.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@
| 2026-05-06 | Orbit | split DynamoDB dashboard advanced UI tasks into a Codex autoloop | `scripts/dynamodb-dashboard-advanced-autoloop/`, `.gitignore` | ready for pagination, saved/recent operations, table wizard, validation, delete confirmation, e2e, docs, and full-advanced-ui gates |
| 2026-05-06 | Orbit | split GCS React dashboard integration into a Codex autoloop | `scripts/gcs-dashboard-react-autoloop/`, `.gitignore` | ready for React route, GCS inspection, guarded management, e2e, docs, and full-react-gcs gates |
| 2026-05-06 | Orbit | split BigQuery dashboard management UI into a Codex autoloop | `scripts/bigquery-dashboard-management-autoloop/`, `.gitignore` | ready for query runner, dataset/table creation, row insert, job detail, validation, e2e, docs, and full-management-ui gates |
| 2026-05-06 | Orbit | split SQS dashboard management UI into a Codex autoloop | `scripts/sqs-dashboard-management-autoloop/`, `.gitignore` | ready for queue creation, send/receive/delete, visibility, purge, DLQ, e2e, docs, and full-management-ui gates |
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ scripts/sqs-autoloop/progress.md
scripts/sqs-autoloop/done.md
scripts/sqs-autoloop/iteration-*.out
scripts/sqs-autoloop/prompt-*.*
scripts/sqs-dashboard-management-autoloop/.run-loop.lock
scripts/sqs-dashboard-management-autoloop/.circuit-state
scripts/sqs-dashboard-management-autoloop/runner.jsonl
scripts/sqs-dashboard-management-autoloop/runner.log
scripts/sqs-dashboard-management-autoloop/state.env
scripts/sqs-dashboard-management-autoloop/state.env.sha256
scripts/sqs-dashboard-management-autoloop/progress.md
scripts/sqs-dashboard-management-autoloop/done.md
scripts/sqs-dashboard-management-autoloop/iteration-*.out
scripts/sqs-dashboard-management-autoloop/verify-*.out
scripts/sqs-dashboard-management-autoloop/prompt-*.*
scripts/pubsub-autoloop/.run-loop.lock
scripts/pubsub-autoloop/.circuit-state
scripts/pubsub-autoloop/runner.jsonl
Expand Down
96 changes: 0 additions & 96 deletions internal/dashboard/assets/react/assets/index-BGtXVHIp.js

This file was deleted.

1 change: 1 addition & 0 deletions internal/dashboard/assets/react/assets/index-BxS7z6Cv.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion internal/dashboard/assets/react/assets/index-CFkAsiRY.css

This file was deleted.

96 changes: 96 additions & 0 deletions internal/dashboard/assets/react/assets/index-CQKKPOyV.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions internal/dashboard/assets/react/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>devcloud Dashboard</title>
<script type="module" crossorigin src="/dashboard/assets/index-BGtXVHIp.js"></script>
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-CFkAsiRY.css">
<script type="module" crossorigin src="/dashboard/assets/index-CQKKPOyV.js"></script>
<link rel="stylesheet" crossorigin href="/dashboard/assets/index-BxS7z6Cv.css">
</head>
<body>
<div id="root"></div>
Expand Down
87 changes: 83 additions & 4 deletions internal/dashboard/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -556,14 +556,18 @@ func (s *Server) handleSQSStatus(w http.ResponseWriter, r *http.Request) {
}

func (s *Server) handleSQSQueues(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w, "GET")
if r.Method != http.MethodGet && r.Method != http.MethodPost {
methodNotAllowed(w, "GET, POST")
return
}
if s.sqs == nil {
http.Error(w, "sqs service is disabled", http.StatusServiceUnavailable)
return
}
if r.Method == http.MethodPost {
s.forwardSQSDashboardOperation(w, r, "CreateQueue", "", "")
return
}
snapshot := s.sqs.Snapshot()
writeJSON(w, map[string]any{
"queues": snapshot.Queues,
Expand Down Expand Up @@ -602,14 +606,24 @@ func (s *Server) handleSQSQueue(w http.ResponseWriter, r *http.Request) {
}
switch parts[1] {
case "messages":
if r.Method != http.MethodGet {
methodNotAllowed(w, "GET")
if r.Method != http.MethodGet && r.Method != http.MethodPost {
methodNotAllowed(w, "GET, POST")
return
}
if r.Method == http.MethodPost {
s.forwardSQSDashboardOperation(w, r, "SendMessage", queueName, detail.Queue.URL)
return
}
writeJSON(w, map[string]any{
"queueName": queueName,
"messages": detail.Messages,
})
case "receive":
s.forwardSQSDashboardOperation(w, r, "ReceiveMessage", queueName, detail.Queue.URL)
case "delete":
s.forwardSQSDashboardOperation(w, r, "DeleteMessage", queueName, detail.Queue.URL)
case "visibility":
s.forwardSQSDashboardOperation(w, r, "ChangeMessageVisibility", queueName, detail.Queue.URL)
case "leases":
if r.Method != http.MethodGet {
methodNotAllowed(w, "GET")
Expand Down Expand Up @@ -649,6 +663,71 @@ func (s *Server) handleSQSQueue(w http.ResponseWriter, r *http.Request) {
}
}

type dashboardSQSOperationRequest struct {
Input json.RawMessage `json:"input"`
}

func (s *Server) forwardSQSDashboardOperation(w http.ResponseWriter, r *http.Request, operation string, queueName string, queueURL string) {
if r.Method != http.MethodPost {
methodNotAllowed(w, "POST")
return
}
var request dashboardSQSOperationRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
http.Error(w, "invalid json request", http.StatusBadRequest)
return
}
input, err := normalizeSQSDashboardInput(request.Input, queueName, queueURL)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
req := r.Clone(r.Context())
req.Method = http.MethodPost
req.URL = &url.URL{Path: "/"}
req.RequestURI = ""
req.Body = io.NopCloser(bytes.NewReader(input))
req.ContentLength = int64(len(input))
req.Header = make(http.Header)
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
req.Header.Set("X-Amz-Target", "AmazonSQS."+operation)
s.sqs.ServeHTTP(w, req)
}

func normalizeSQSDashboardInput(raw json.RawMessage, queueName string, queueURL string) ([]byte, error) {
if len(raw) == 0 {
return nil, errors.New("input is required")
}
var input map[string]any
if err := json.Unmarshal(raw, &input); err != nil {
return nil, errors.New("input must be valid JSON")
}
if input == nil {
return nil, errors.New("input must be a JSON object")
}
if queueName != "" {
if existing, ok := input["QueueName"]; ok {
if existingName, ok := existing.(string); !ok || existingName != queueName {
return nil, errors.New("input QueueName must match the selected queue")
}
}
}
if queueURL != "" {
if existing, ok := input["QueueUrl"]; ok {
if existingURL, ok := existing.(string); !ok || existingURL != queueURL {
return nil, errors.New("input QueueUrl must match the selected queue")
}
} else {
input["QueueUrl"] = queueURL
}
}
encoded, err := json.Marshal(input)
if err != nil {
return nil, errors.New("input could not be encoded")
}
return encoded, nil
}

func (s *Server) handlePubSubStatus(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w, "GET")
Expand Down
171 changes: 171 additions & 0 deletions internal/dashboard/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,177 @@ func TestSQSQueueDetailAPIsExposeMessagesLeasesAndPurge(t *testing.T) {
}
}

func TestSQSDashboardManagementAPIsCreateQueueAndSendMessage(t *testing.T) {
sqsServer := sqssvc.NewServer(sqssvc.Config{Addr: "127.0.0.1:9324"})
server := NewServer(Config{}, newDashboardStore(nil, nil))
server.SetSQS(sqsServer)
routes := server.routes()

createRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues", `{
"input":{
"QueueName":"dashboard-managed.fifo",
"Attributes":{"FifoQueue":"true","ContentBasedDeduplication":"true","VisibilityTimeout":"30"},
"Tags":{"source":"dashboard"}
}
}`)
if createRec.Code != http.StatusOK {
t.Fatalf("create status = %d, body = %s", createRec.Code, createRec.Body.String())
}
if !strings.Contains(createRec.Body.String(), `"QueueUrl"`) || !strings.Contains(createRec.Body.String(), "dashboard-managed.fifo") {
t.Fatalf("create body = %s", createRec.Body.String())
}

sendRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-managed.fifo/messages", `{
"input":{
"MessageBody":"dashboard send",
"MessageGroupId":"workers",
"MessageAttributes":{"kind":{"DataType":"String","StringValue":"test"}}
}
}`)
if sendRec.Code != http.StatusOK {
t.Fatalf("send status = %d, body = %s", sendRec.Code, sendRec.Body.String())
}
if !strings.Contains(sendRec.Body.String(), `"MessageId"`) || !strings.Contains(sendRec.Body.String(), `"MD5OfMessageAttributes"`) {
t.Fatalf("send body = %s", sendRec.Body.String())
}

messagesRec := performRequest(routes, http.MethodGet, "/api/sqs/queues/dashboard-managed.fifo/messages")
if messagesRec.Code != http.StatusOK {
t.Fatalf("messages status = %d, body = %s", messagesRec.Code, messagesRec.Body.String())
}
if !strings.Contains(messagesRec.Body.String(), `"body":"dashboard send"`) || !strings.Contains(messagesRec.Body.String(), `"messageGroupId":"workers"`) {
t.Fatalf("messages body = %s", messagesRec.Body.String())
}
}

func TestSQSDashboardSendMessageRejectsMismatchedQueueURL(t *testing.T) {
sqsServer := sqssvc.NewServer(sqssvc.Config{Addr: "127.0.0.1:9324"})
server := NewServer(Config{}, newDashboardStore(nil, nil))
server.SetSQS(sqsServer)
routes := server.routes()

createRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues", `{"input":{"QueueName":"dashboard-safe"}}`)
if createRec.Code != http.StatusOK {
t.Fatalf("create status = %d, body = %s", createRec.Code, createRec.Body.String())
}

sendRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-safe/messages", `{
"input":{"QueueUrl":"http://127.0.0.1:9324/000000000000/other","MessageBody":"wrong queue"}
}`)
if sendRec.Code != http.StatusBadRequest {
t.Fatalf("send status = %d, want %d, body = %s", sendRec.Code, http.StatusBadRequest, sendRec.Body.String())
}
if !strings.Contains(sendRec.Body.String(), "QueueUrl must match") {
t.Fatalf("send body = %s", sendRec.Body.String())
}
}

func TestSQSDashboardManagementAPIsReceiveAndDeleteMessage(t *testing.T) {
sqsServer := sqssvc.NewServer(sqssvc.Config{Addr: "127.0.0.1:9324"})
server := NewServer(Config{}, newDashboardStore(nil, nil))
server.SetSQS(sqsServer)
routes := server.routes()

createRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues", `{"input":{"QueueName":"dashboard-receive"}}`)
if createRec.Code != http.StatusOK {
t.Fatalf("create status = %d, body = %s", createRec.Code, createRec.Body.String())
}
sendRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-receive/messages", `{
"input":{"MessageBody":"dashboard receive","MessageAttributes":{"kind":{"DataType":"String","StringValue":"test"}}}
}`)
if sendRec.Code != http.StatusOK {
t.Fatalf("send status = %d, body = %s", sendRec.Code, sendRec.Body.String())
}

receiveRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-receive/receive", `{
"input":{"MaxNumberOfMessages":1,"VisibilityTimeout":30,"WaitTimeSeconds":0,"AttributeNames":["All"],"MessageAttributeNames":["All"]}
}`)
if receiveRec.Code != http.StatusOK {
t.Fatalf("receive status = %d, body = %s", receiveRec.Code, receiveRec.Body.String())
}
var receiveBody struct {
Messages []struct {
MessageID string `json:"MessageId"`
ReceiptHandle string `json:"ReceiptHandle"`
Body string `json:"Body"`
} `json:"Messages"`
}
if err := json.Unmarshal(receiveRec.Body.Bytes(), &receiveBody); err != nil {
t.Fatalf("decode receive body: %v", err)
}
if len(receiveBody.Messages) != 1 || receiveBody.Messages[0].ReceiptHandle == "" || receiveBody.Messages[0].Body != "dashboard receive" {
t.Fatalf("receive body = %s", receiveRec.Body.String())
}

deleteRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-receive/delete", `{
"input":{"ReceiptHandle":"`+receiveBody.Messages[0].ReceiptHandle+`"}
}`)
if deleteRec.Code != http.StatusOK {
t.Fatalf("delete status = %d, body = %s", deleteRec.Code, deleteRec.Body.String())
}

afterDeleteRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-receive/receive", `{
"input":{"MaxNumberOfMessages":1,"WaitTimeSeconds":0}
}`)
if afterDeleteRec.Code != http.StatusOK {
t.Fatalf("after delete status = %d, body = %s", afterDeleteRec.Code, afterDeleteRec.Body.String())
}
if strings.Contains(afterDeleteRec.Body.String(), "dashboard receive") {
t.Fatalf("message was not deleted: %s", afterDeleteRec.Body.String())
}
}

func TestSQSDashboardManagementAPIsChangeMessageVisibility(t *testing.T) {
sqsServer := sqssvc.NewServer(sqssvc.Config{Addr: "127.0.0.1:9324"})
server := NewServer(Config{}, newDashboardStore(nil, nil))
server.SetSQS(sqsServer)
routes := server.routes()

createRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues", `{"input":{"QueueName":"dashboard-visibility"}}`)
if createRec.Code != http.StatusOK {
t.Fatalf("create status = %d, body = %s", createRec.Code, createRec.Body.String())
}
sendRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-visibility/messages", `{
"input":{"MessageBody":"dashboard visibility"}
}`)
if sendRec.Code != http.StatusOK {
t.Fatalf("send status = %d, body = %s", sendRec.Code, sendRec.Body.String())
}
receiveRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-visibility/receive", `{
"input":{"MaxNumberOfMessages":1,"VisibilityTimeout":30,"WaitTimeSeconds":0}
}`)
if receiveRec.Code != http.StatusOK {
t.Fatalf("receive status = %d, body = %s", receiveRec.Code, receiveRec.Body.String())
}
var receiveBody struct {
Messages []struct {
ReceiptHandle string `json:"ReceiptHandle"`
} `json:"Messages"`
}
if err := json.Unmarshal(receiveRec.Body.Bytes(), &receiveBody); err != nil {
t.Fatalf("decode receive body: %v", err)
}
if len(receiveBody.Messages) != 1 || receiveBody.Messages[0].ReceiptHandle == "" {
t.Fatalf("receive body = %s", receiveRec.Body.String())
}

changeRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-visibility/visibility", `{
"input":{"ReceiptHandle":"`+receiveBody.Messages[0].ReceiptHandle+`","VisibilityTimeout":0}
}`)
if changeRec.Code != http.StatusOK {
t.Fatalf("change visibility status = %d, body = %s", changeRec.Code, changeRec.Body.String())
}
againRec := performRequestWithBody(routes, http.MethodPost, "/api/sqs/queues/dashboard-visibility/receive", `{
"input":{"MaxNumberOfMessages":1,"WaitTimeSeconds":0}
}`)
if againRec.Code != http.StatusOK {
t.Fatalf("receive again status = %d, body = %s", againRec.Code, againRec.Body.String())
}
if !strings.Contains(againRec.Body.String(), "dashboard visibility") {
t.Fatalf("message was not made visible: %s", againRec.Body.String())
}
}

func TestSQSQueuesAPIMarksDisabled(t *testing.T) {
server := NewServer(Config{}, newDashboardStore(nil, nil))

Expand Down
5 changes: 5 additions & 0 deletions internal/services/sqs/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,11 @@ type Server struct {
}

func NewServer(cfg Config) *Server {
if storagePath := strings.TrimSpace(cfg.StoragePath); storagePath != "" {
if absolutePath, err := filepath.Abs(storagePath); err == nil {
cfg.StoragePath = absolutePath
}
}
server := &Server{
config: cfg,
queues: map[string]*queueState{},
Expand Down
4 changes: 4 additions & 0 deletions scripts/sqs-autoloop/verify.sh
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ services:
enabled: true
project: devcloud
location: US
redshift:
enabled: false
pubsub:
enabled: false
sqs:
enabled: true
region: us-east-1
Expand Down
31 changes: 31 additions & 0 deletions scripts/sqs-dashboard-management-autoloop/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# SQS Dashboard Management Autoloop

Codex loop for upgrading the SQS React dashboard from inspection plus purge into a local management console.

## Usage

```bash
bash scripts/sqs-dashboard-management-autoloop/bootstrap.sh
bash scripts/sqs-dashboard-management-autoloop/run-loop.sh
```

Useful variants:

```bash
MAX_ITERATIONS=40 bash scripts/sqs-dashboard-management-autoloop/run-loop.sh
AUTOCOMMIT=true bash scripts/sqs-dashboard-management-autoloop/run-loop.sh
VERIFY_STAGE=queue-send bash scripts/sqs-dashboard-management-autoloop/run-loop.sh
DONE_VERIFY_STAGE=full-management-ui bash scripts/sqs-dashboard-management-autoloop/run-loop.sh
VERIFY_STAGE=full-management-ui bash scripts/sqs-dashboard-management-autoloop/verify.sh
```

## Stages

- `foundation`: loop scripts and existing SQS full compatibility gate.
- `queue-send`: CreateQueue and SendMessage flows.
- `receive-delete`: ReceiveMessage and DeleteMessage workflows.
- `visibility-purge-dlq`: visibility timeout, safer purge, and DLQ detail.
- `e2e-docs`: tests/E2E and docs.
- `full-management-ui`: all gates plus `go test ./...`.

Runtime files such as `progress.md`, `state.env`, `runner.log`, `runner.jsonl`, `done.md`, and `iteration-*.out` are ignored by git.
Loading
Loading