stackport 0.4.2__tar.gz → 0.4.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (148) hide show
  1. {stackport-0.4.2/stackport.egg-info → stackport-0.4.3}/PKG-INFO +31 -8
  2. {stackport-0.4.2 → stackport-0.4.3}/README.md +29 -6
  3. {stackport-0.4.2 → stackport-0.4.3}/backend/cli.py +37 -35
  4. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/resources.py +1 -0
  5. {stackport-0.4.2 → stackport-0.4.3}/pyproject.toml +2 -2
  6. {stackport-0.4.2 → stackport-0.4.3/stackport.egg-info}/PKG-INFO +31 -8
  7. {stackport-0.4.2 → stackport-0.4.3}/stackport.egg-info/SOURCES.txt +26 -26
  8. {stackport-0.4.2 → stackport-0.4.3}/tests/test_cli.py +65 -0
  9. {stackport-0.4.2 → stackport-0.4.3}/tests/test_registries.py +17 -1
  10. stackport-0.4.2/ui/dist/assets/CloudscapeAbout-DccVyQFu.js → stackport-0.4.3/ui/dist/assets/CloudscapeAbout-BRbsDskR.js +1 -1
  11. stackport-0.4.2/ui/dist/assets/CloudscapeDashboard-DtV4-eQq.js → stackport-0.4.3/ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js +1 -1
  12. stackport-0.4.2/ui/dist/assets/CloudscapeLearn-DW6RZAj4.js → stackport-0.4.3/ui/dist/assets/CloudscapeLearn-CVW3k3X0.js +1 -1
  13. stackport-0.4.2/ui/dist/assets/CloudscapeResourceBrowser-FpsXtjf_.js → stackport-0.4.3/ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js +10 -10
  14. stackport-0.4.2/ui/dist/assets/CloudscapeSettings-UdgiI6dm.js → stackport-0.4.3/ui/dist/assets/CloudscapeSettings-CIXFvPKv.js +1 -1
  15. stackport-0.4.2/ui/dist/assets/CloudscapeShell-Be6uFzsz.js → stackport-0.4.3/ui/dist/assets/CloudscapeShell-DNfhSg0O.js +1 -1
  16. stackport-0.4.2/ui/dist/assets/StateMachineGraph-G76o42iO.js → stackport-0.4.3/ui/dist/assets/StateMachineGraph-DvF_kIOO.js +1 -1
  17. stackport-0.4.2/ui/dist/assets/index-B3zGVRXf.js → stackport-0.4.3/ui/dist/assets/index-BC7E-Fm4.js +1 -1
  18. stackport-0.4.2/ui/dist/assets/index-hhBdqMqs.js → stackport-0.4.3/ui/dist/assets/index-BLdRlH9d.js +2 -2
  19. stackport-0.4.2/ui/dist/assets/index-B3bka_BF.js → stackport-0.4.3/ui/dist/assets/index-Ba-oOcd3.js +1 -1
  20. stackport-0.4.2/ui/dist/assets/index-DhD1oh4N.js → stackport-0.4.3/ui/dist/assets/index-BkSgVQ0X.js +1 -1
  21. stackport-0.4.2/ui/dist/assets/index-CNWD72L_.js → stackport-0.4.3/ui/dist/assets/index-C5z3cir-.js +1 -1
  22. stackport-0.4.2/ui/dist/assets/index-B33aGwWE.js → stackport-0.4.3/ui/dist/assets/index-Dw6TGweb.js +1 -1
  23. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/index.html +1 -1
  24. {stackport-0.4.2 → stackport-0.4.3}/LICENSE +0 -0
  25. {stackport-0.4.2 → stackport-0.4.3}/MANIFEST.in +0 -0
  26. {stackport-0.4.2 → stackport-0.4.3}/backend/__init__.py +0 -0
  27. {stackport-0.4.2 → stackport-0.4.3}/backend/aws_client.py +0 -0
  28. {stackport-0.4.2 → stackport-0.4.3}/backend/cache.py +0 -0
  29. {stackport-0.4.2 → stackport-0.4.3}/backend/config.py +0 -0
  30. {stackport-0.4.2 → stackport-0.4.3}/backend/endpoint_store.py +0 -0
  31. {stackport-0.4.2 → stackport-0.4.3}/backend/learn/__init__.py +0 -0
  32. {stackport-0.4.2 → stackport-0.4.3}/backend/learn/content.py +0 -0
  33. {stackport-0.4.2 → stackport-0.4.3}/backend/learn/hotspots.py +0 -0
  34. {stackport-0.4.2 → stackport-0.4.3}/backend/learn/trails/aws-first-steps.json +0 -0
  35. {stackport-0.4.2 → stackport-0.4.3}/backend/learn/verify.py +0 -0
  36. {stackport-0.4.2 → stackport-0.4.3}/backend/learn_store.py +0 -0
  37. {stackport-0.4.2 → stackport-0.4.3}/backend/main.py +0 -0
  38. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/__init__.py +0 -0
  39. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/common.py +0 -0
  40. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/dynamodb.py +0 -0
  41. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/ec2.py +0 -0
  42. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/endpoints.py +0 -0
  43. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/iam.py +0 -0
  44. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/lambda_svc.py +0 -0
  45. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/learn.py +0 -0
  46. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/logs.py +0 -0
  47. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/monitoring.py +0 -0
  48. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/rds.py +0 -0
  49. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/s3.py +0 -0
  50. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/secretsmanager.py +0 -0
  51. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/sns.py +0 -0
  52. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/sqs.py +0 -0
  53. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/stats.py +0 -0
  54. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/stepfunctions.py +0 -0
  55. {stackport-0.4.2 → stackport-0.4.3}/backend/routes/tags.py +0 -0
  56. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/__init__.py +0 -0
  57. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/dynamodb.py +0 -0
  58. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/endpoints.py +0 -0
  59. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/lambda_svc.py +0 -0
  60. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/learn.py +0 -0
  61. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/logs.py +0 -0
  62. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/monitoring.py +0 -0
  63. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/s3.py +0 -0
  64. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/secretsmanager.py +0 -0
  65. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/sns.py +0 -0
  66. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/sqs.py +0 -0
  67. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/stepfunctions.py +0 -0
  68. {stackport-0.4.2 → stackport-0.4.3}/backend/schemas/tags.py +0 -0
  69. {stackport-0.4.2 → stackport-0.4.3}/backend/websocket.py +0 -0
  70. {stackport-0.4.2 → stackport-0.4.3}/setup.cfg +0 -0
  71. {stackport-0.4.2 → stackport-0.4.3}/stackport.egg-info/dependency_links.txt +0 -0
  72. {stackport-0.4.2 → stackport-0.4.3}/stackport.egg-info/entry_points.txt +0 -0
  73. {stackport-0.4.2 → stackport-0.4.3}/stackport.egg-info/requires.txt +0 -0
  74. {stackport-0.4.2 → stackport-0.4.3}/stackport.egg-info/top_level.txt +0 -0
  75. {stackport-0.4.2 → stackport-0.4.3}/tests/test_cache.py +0 -0
  76. {stackport-0.4.2 → stackport-0.4.3}/tests/test_client.py +0 -0
  77. {stackport-0.4.2 → stackport-0.4.3}/tests/test_config.py +0 -0
  78. {stackport-0.4.2 → stackport-0.4.3}/tests/test_create_resources.py +0 -0
  79. {stackport-0.4.2 → stackport-0.4.3}/tests/test_dynamodb_routes.py +0 -0
  80. {stackport-0.4.2 → stackport-0.4.3}/tests/test_ec2_routes.py +0 -0
  81. {stackport-0.4.2 → stackport-0.4.3}/tests/test_endpoint_store.py +0 -0
  82. {stackport-0.4.2 → stackport-0.4.3}/tests/test_endpoints.py +0 -0
  83. {stackport-0.4.2 → stackport-0.4.3}/tests/test_iam_routes.py +0 -0
  84. {stackport-0.4.2 → stackport-0.4.3}/tests/test_lambda_routes.py +0 -0
  85. {stackport-0.4.2 → stackport-0.4.3}/tests/test_learn_content.py +0 -0
  86. {stackport-0.4.2 → stackport-0.4.3}/tests/test_learn_routes.py +0 -0
  87. {stackport-0.4.2 → stackport-0.4.3}/tests/test_learn_store.py +0 -0
  88. {stackport-0.4.2 → stackport-0.4.3}/tests/test_logs_routes.py +0 -0
  89. {stackport-0.4.2 → stackport-0.4.3}/tests/test_logs_tail.py +0 -0
  90. {stackport-0.4.2 → stackport-0.4.3}/tests/test_monitoring_routes.py +0 -0
  91. {stackport-0.4.2 → stackport-0.4.3}/tests/test_multi_endpoint.py +0 -0
  92. {stackport-0.4.2 → stackport-0.4.3}/tests/test_rds_routes.py +0 -0
  93. {stackport-0.4.2 → stackport-0.4.3}/tests/test_readonly_middleware.py +0 -0
  94. {stackport-0.4.2 → stackport-0.4.3}/tests/test_routes.py +0 -0
  95. {stackport-0.4.2 → stackport-0.4.3}/tests/test_s3_routes.py +0 -0
  96. {stackport-0.4.2 → stackport-0.4.3}/tests/test_s3_upload_limit_env.py +0 -0
  97. {stackport-0.4.2 → stackport-0.4.3}/tests/test_secretsmanager_routes.py +0 -0
  98. {stackport-0.4.2 → stackport-0.4.3}/tests/test_sns_routes.py +0 -0
  99. {stackport-0.4.2 → stackport-0.4.3}/tests/test_sqs_routes.py +0 -0
  100. {stackport-0.4.2 → stackport-0.4.3}/tests/test_tags_routes.py +0 -0
  101. {stackport-0.4.2 → stackport-0.4.3}/tests/test_websocket.py +0 -0
  102. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css +0 -0
  103. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/CloudscapeShell-BXHVWjDE.css +0 -0
  104. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/index-DE--A2EE.css +0 -0
  105. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/index-DKIdXFX0.css +0 -0
  106. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/index-DNBdeKId.css +0 -0
  107. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/index-DoY0HscW.css +0 -0
  108. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/index-DrkkXAoG.css +0 -0
  109. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/index-Vf0UFiCT.css +0 -0
  110. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/stepfunctions-graph-B6nvU4OB.js +0 -0
  111. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/assets/utils-Cfmwk2UT.js +0 -0
  112. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/acm.svg +0 -0
  113. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/apigateway.svg +0 -0
  114. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/appsync.svg +0 -0
  115. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/athena.svg +0 -0
  116. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/cloudformation.svg +0 -0
  117. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/cloudfront.svg +0 -0
  118. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/cognito-idp.svg +0 -0
  119. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/dynamodb.svg +0 -0
  120. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/ec2.svg +0 -0
  121. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/ecr.svg +0 -0
  122. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/ecs.svg +0 -0
  123. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/elasticache.svg +0 -0
  124. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/elasticfilesystem.svg +0 -0
  125. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/elasticloadbalancing.svg +0 -0
  126. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/elasticmapreduce.svg +0 -0
  127. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/events.svg +0 -0
  128. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/firehose.svg +0 -0
  129. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/glue.svg +0 -0
  130. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/iam.svg +0 -0
  131. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/kinesis.svg +0 -0
  132. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/kms.svg +0 -0
  133. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/lambda.svg +0 -0
  134. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/logs.svg +0 -0
  135. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/monitoring.svg +0 -0
  136. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/rds.svg +0 -0
  137. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/route53.svg +0 -0
  138. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/s3.svg +0 -0
  139. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/secretsmanager.svg +0 -0
  140. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/ses.svg +0 -0
  141. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/sns.svg +0 -0
  142. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/sqs.svg +0 -0
  143. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/ssm.svg +0 -0
  144. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/stepfunctions.svg +0 -0
  145. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/aws-icons/wafv2.svg +0 -0
  146. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/favicon.png +0 -0
  147. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/favicon.svg +0 -0
  148. {stackport-0.4.2 → stackport-0.4.3}/ui/dist/stackport-logo.svg +0 -0
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/DaviReisVieira/stackport
8
8
  Project-URL: Repository, https://github.com/DaviReisVieira/stackport
9
9
  Project-URL: Issues, https://github.com/DaviReisVieira/stackport/issues
10
- Keywords: aws,ministack,localstack,browser,devtools,emulator
10
+ Keywords: aws,ministack,floci,localstack,browser,devtools,emulator
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Framework :: FastAPI
13
13
  Classifier: Intended Audience :: Developers
@@ -75,15 +75,18 @@ Dynamic: license-file
75
75
  - **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
76
76
  - **Real-time dashboard** with WebSocket-powered live updates
77
77
  - **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
78
- - Single Docker image, works with MiniStack, LocalStack, Moto, or any AWS-compatible endpoint
78
+ - Single Docker image, works with [MiniStack](https://github.com/ministackorg/ministack), [Floci](https://github.com/floci-io/floci), LocalStack, Moto, or any AWS-compatible endpoint
79
79
 
80
80
  ## Quick Start
81
81
 
82
- ### With a local emulator (recommended)
82
+ ### With a local emulator
83
+
84
+ Start whichever emulator you use, then point StackPort at it:
83
85
 
84
86
  ```bash
85
- # Start MiniStack (or LocalStack, Moto, etc.)
86
- pip install ministack && ministack
87
+ # Start an emulator on :4566 (pick one)
88
+ docker run -d -p 4566:4566 ministackorg/ministack # MiniStack
89
+ docker run -d -p 4566:4566 floci/floci # Floci
87
90
 
88
91
  # Start StackPort
89
92
  pip install stackport
@@ -108,16 +111,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
108
111
 
109
112
  When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
110
113
 
111
- ### Docker Compose (MiniStack + StackPort)
114
+ ### Docker Compose
115
+
116
+ Each example runs the emulator, StackPort, and a one-shot `seed` container that creates a few resources (S3, SQS, SNS, DynamoDB, Secrets Manager, IAM, Logs, Step Functions) so the dashboard has something to show on first load. Both use the same seed. Swap in LocalStack, Moto, or any AWS-compatible endpoint by replacing the emulator service and updating `AWS_ENDPOINT_URL`.
112
117
 
113
- This example uses [MiniStack](https://github.com/Nahuel990/ministack) as the emulator, but you can swap it for LocalStack, Moto, or any AWS-compatible endpoint — just update `AWS_ENDPOINT_URL`.
118
+ **MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
114
119
 
115
120
  ```bash
116
121
  curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
117
122
  docker compose up -d
118
123
  # Open http://localhost:8080
124
+ # If the dashboard is empty: docker compose logs seed
119
125
  ```
120
126
 
127
+ [MiniStack](https://github.com/ministackorg/ministack) derives the URLs it returns (SQS queue URLs, pre-signed URLs) from the request `Host`, so they resolve from other containers and from the host without extra configuration.
128
+
129
+ **Floci + StackPort** — [`examples/docker-compose.floci.yml`](examples/docker-compose.floci.yml)
130
+
131
+ ```bash
132
+ curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.floci.yml
133
+ docker compose -f docker-compose.floci.yml up -d
134
+ # Open http://localhost:8080
135
+ # If the dashboard is empty: docker compose -f docker-compose.floci.yml logs seed
136
+ ```
137
+
138
+ [Floci](https://github.com/floci-io/floci) takes the hostname for the URLs it returns from `FLOCI_HOSTNAME`, which the example sets to `floci` so they resolve from other containers. From the host, reach the same resources through `http://localhost:4566`.
139
+
121
140
  ### Docker (standalone)
122
141
 
123
142
  ```bash
@@ -351,6 +370,10 @@ ui/src/
351
370
  lib/ API client, types, service icons, utils
352
371
  ```
353
372
 
373
+ ## Origin
374
+
375
+ StackPort started as a web UI pull request to [MiniStack](https://github.com/ministackorg/ministack) ([ministack#116](https://github.com/ministackorg/ministack/pull/116)). The MiniStack maintainers suggested it would be more useful as a standalone tool that talks to any emulator over the standard AWS API, and that is what it became. Thanks to [@Nahuel990](https://github.com/Nahuel990) and [@McDoit](https://github.com/McDoit) for pushing it in that direction.
376
+
354
377
  ## Contributors
355
378
 
356
379
  <a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
@@ -39,15 +39,18 @@
39
39
  - **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
40
40
  - **Real-time dashboard** with WebSocket-powered live updates
41
41
  - **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
42
- - Single Docker image, works with MiniStack, LocalStack, Moto, or any AWS-compatible endpoint
42
+ - Single Docker image, works with [MiniStack](https://github.com/ministackorg/ministack), [Floci](https://github.com/floci-io/floci), LocalStack, Moto, or any AWS-compatible endpoint
43
43
 
44
44
  ## Quick Start
45
45
 
46
- ### With a local emulator (recommended)
46
+ ### With a local emulator
47
+
48
+ Start whichever emulator you use, then point StackPort at it:
47
49
 
48
50
  ```bash
49
- # Start MiniStack (or LocalStack, Moto, etc.)
50
- pip install ministack && ministack
51
+ # Start an emulator on :4566 (pick one)
52
+ docker run -d -p 4566:4566 ministackorg/ministack # MiniStack
53
+ docker run -d -p 4566:4566 floci/floci # Floci
51
54
 
52
55
  # Start StackPort
53
56
  pip install stackport
@@ -72,16 +75,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
72
75
 
73
76
  When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
74
77
 
75
- ### Docker Compose (MiniStack + StackPort)
78
+ ### Docker Compose
79
+
80
+ Each example runs the emulator, StackPort, and a one-shot `seed` container that creates a few resources (S3, SQS, SNS, DynamoDB, Secrets Manager, IAM, Logs, Step Functions) so the dashboard has something to show on first load. Both use the same seed. Swap in LocalStack, Moto, or any AWS-compatible endpoint by replacing the emulator service and updating `AWS_ENDPOINT_URL`.
76
81
 
77
- This example uses [MiniStack](https://github.com/Nahuel990/ministack) as the emulator, but you can swap it for LocalStack, Moto, or any AWS-compatible endpoint — just update `AWS_ENDPOINT_URL`.
82
+ **MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
78
83
 
79
84
  ```bash
80
85
  curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
81
86
  docker compose up -d
82
87
  # Open http://localhost:8080
88
+ # If the dashboard is empty: docker compose logs seed
83
89
  ```
84
90
 
91
+ [MiniStack](https://github.com/ministackorg/ministack) derives the URLs it returns (SQS queue URLs, pre-signed URLs) from the request `Host`, so they resolve from other containers and from the host without extra configuration.
92
+
93
+ **Floci + StackPort** — [`examples/docker-compose.floci.yml`](examples/docker-compose.floci.yml)
94
+
95
+ ```bash
96
+ curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.floci.yml
97
+ docker compose -f docker-compose.floci.yml up -d
98
+ # Open http://localhost:8080
99
+ # If the dashboard is empty: docker compose -f docker-compose.floci.yml logs seed
100
+ ```
101
+
102
+ [Floci](https://github.com/floci-io/floci) takes the hostname for the URLs it returns from `FLOCI_HOSTNAME`, which the example sets to `floci` so they resolve from other containers. From the host, reach the same resources through `http://localhost:4566`.
103
+
85
104
  ### Docker (standalone)
86
105
 
87
106
  ```bash
@@ -315,6 +334,10 @@ ui/src/
315
334
  lib/ API client, types, service icons, utils
316
335
  ```
317
336
 
337
+ ## Origin
338
+
339
+ StackPort started as a web UI pull request to [MiniStack](https://github.com/ministackorg/ministack) ([ministack#116](https://github.com/ministackorg/ministack/pull/116)). The MiniStack maintainers suggested it would be more useful as a standalone tool that talks to any emulator over the standard AWS API, and that is what it became. Thanks to [@Nahuel990](https://github.com/Nahuel990) and [@McDoit](https://github.com/McDoit) for pushing it in that direction.
340
+
318
341
  ## Contributors
319
342
 
320
343
  <a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
@@ -10,13 +10,37 @@ import click
10
10
  import uvicorn
11
11
 
12
12
  from backend.aws_client import get_client
13
- from backend.config import AWS_ENDPOINT_URL, AWS_REGION, LOG_LEVEL, STACKPORT_PORT, STACKPORT_SERVICES
14
- from backend.routes.resources import DESCRIBE_REGISTRY, _extract_id, _serialize
13
+ from backend.config import (
14
+ AWS_ENDPOINT_URL,
15
+ AWS_REGION,
16
+ LOG_LEVEL,
17
+ STACKPORT_PORT,
18
+ STACKPORT_SERVICES,
19
+ endpoint_store,
20
+ )
21
+ from backend.routes.resources import (
22
+ DESCRIBE_REGISTRY,
23
+ _PREFERRED_ID_FIELD,
24
+ _extract_id,
25
+ _serialize,
26
+ )
15
27
  from backend.routes.stats import SERVICE_REGISTRY, _METHOD_KWARGS, _count_items, _probe_service
16
28
 
17
29
  logger = logging.getLogger(__name__)
18
30
 
19
31
 
32
+ def _client_kwargs(endpoint: str | None, region: str | None) -> dict:
33
+ """Resolve where a CLI command talks to.
34
+
35
+ An explicit --endpoint (or AWS_ENDPOINT_URL at startup) wins. Without one,
36
+ fall back to the saved default endpoint, the same one the web UI uses.
37
+ Passing the result to get_client explicitly is what makes the flag work
38
+ once ~/.stackport/endpoints.json exists.
39
+ """
40
+ url = endpoint if endpoint is not None else endpoint_store.get_default_url()
41
+ return {"endpoint_url": url, "region": region}
42
+
43
+
20
44
  @click.group(invoke_without_command=True)
21
45
  @click.version_option(package_name="stackport")
22
46
  @click.pass_context
@@ -39,22 +63,14 @@ def serve(port):
39
63
  @click.option("--output", type=click.Choice(["json", "table"]), default="table", help="Output format")
40
64
  def status(endpoint, region, output):
41
65
  """Show all services with availability and resource counts."""
42
- # Override endpoint/region if provided
43
- if endpoint != AWS_ENDPOINT_URL:
44
- import os
45
-
46
- os.environ["AWS_ENDPOINT_URL"] = endpoint
47
- if region != AWS_REGION:
48
- import os
49
-
50
- os.environ["AWS_REGION"] = region
66
+ client_kwargs = _client_kwargs(endpoint, region)
51
67
 
52
68
  enabled_services = [s.strip() for s in STACKPORT_SERVICES.split(",") if s.strip()]
53
69
  services = {}
54
70
 
55
71
  try:
56
72
  with ThreadPoolExecutor(max_workers=min(len(enabled_services), 10)) as executor:
57
- futures = {executor.submit(_probe_service, svc): svc for svc in enabled_services}
73
+ futures = {executor.submit(_probe_service, svc, **client_kwargs): svc for svc in enabled_services}
58
74
  for future in as_completed(futures):
59
75
  svc_name, result = future.result()
60
76
  services[svc_name] = result
@@ -89,15 +105,7 @@ def status(endpoint, region, output):
89
105
  @click.option("--output", type=click.Choice(["json", "table", "csv"]), default="table", help="Output format")
90
106
  def list(service, endpoint, region, output):
91
107
  """List resources for a service."""
92
- # Override endpoint/region if provided
93
- if endpoint != AWS_ENDPOINT_URL:
94
- import os
95
-
96
- os.environ["AWS_ENDPOINT_URL"] = endpoint
97
- if region != AWS_REGION:
98
- import os
99
-
100
- os.environ["AWS_REGION"] = region
108
+ client_kwargs = _client_kwargs(endpoint, region)
101
109
 
102
110
  registry_entries = SERVICE_REGISTRY.get(service)
103
111
  if not registry_entries:
@@ -109,7 +117,7 @@ def list(service, endpoint, region, output):
109
117
  resources = {}
110
118
  for resource_type, boto3_service, method_name, response_key in registry_entries:
111
119
  try:
112
- client = get_client(boto3_service)
120
+ client = get_client(boto3_service, **client_kwargs)
113
121
  method = getattr(client, method_name)
114
122
  kwargs = _METHOD_KWARGS.get((boto3_service, method_name), {})
115
123
  resp = method(**kwargs)
@@ -129,8 +137,9 @@ def list(service, endpoint, region, output):
129
137
  writer = csv.writer(sys.stdout)
130
138
  writer.writerow(["service", "resource_type", "resource_id", "name"])
131
139
  for resource_type, items in resources.items():
140
+ preferred = _PREFERRED_ID_FIELD.get((service, resource_type))
132
141
  for item in items:
133
- resource_id = _extract_id(item)
142
+ resource_id = _extract_id(item, preferred)
134
143
  name = item.get("Name", item.get("FunctionName", item.get("TableName", resource_id))) if isinstance(item, dict) else resource_id
135
144
  writer.writerow([service, resource_type, resource_id, name])
136
145
  else:
@@ -139,8 +148,9 @@ def list(service, endpoint, region, output):
139
148
  if items:
140
149
  click.echo(f"\n{resource_type.upper()} ({len(items)}):")
141
150
  click.echo("-" * 60)
151
+ preferred = _PREFERRED_ID_FIELD.get((service, resource_type))
142
152
  for item in items[:20]: # Limit to first 20 for readability
143
- resource_id = _extract_id(item)
153
+ resource_id = _extract_id(item, preferred)
144
154
  if isinstance(item, dict):
145
155
  name = item.get("Name", item.get("FunctionName", item.get("TableName", "")))
146
156
  if name and name != resource_id:
@@ -164,20 +174,12 @@ def list(service, endpoint, region, output):
164
174
  @click.option("--output", type=click.Choice(["json", "table"]), default="json", help="Output format")
165
175
  def describe(service, resource_type, resource_id, endpoint, region, output):
166
176
  """Describe a specific resource."""
167
- # Override endpoint/region if provided
168
- if endpoint != AWS_ENDPOINT_URL:
169
- import os
170
-
171
- os.environ["AWS_ENDPOINT_URL"] = endpoint
172
- if region != AWS_REGION:
173
- import os
174
-
175
- os.environ["AWS_REGION"] = region
177
+ client_kwargs = _client_kwargs(endpoint, region)
176
178
 
177
179
  # Special case for WAFv2
178
180
  if (service, resource_type) == ("wafv2", "web_acls"):
179
181
  try:
180
- client = get_client("wafv2")
182
+ client = get_client("wafv2", **client_kwargs)
181
183
  acls = client.list_web_acls(Scope="REGIONAL").get("WebACLs", [])
182
184
  match = next((a for a in acls if a.get("Name") == resource_id), None)
183
185
  if not match:
@@ -211,7 +213,7 @@ def describe(service, resource_type, resource_id, endpoint, region, output):
211
213
  }
212
214
 
213
215
  try:
214
- client = get_client(boto3_service)
216
+ client = get_client(boto3_service, **client_kwargs)
215
217
  method = getattr(client, method_name)
216
218
  if id_param in _LIST_PARAMS:
217
219
  resp = method(**{id_param: [resource_id]})
@@ -84,6 +84,7 @@ _PREFERRED_ID_FIELD: dict[tuple[str, str], str] = {
84
84
  ("elasticmapreduce", "clusters"): "Id",
85
85
  ("cognito-idp", "user_pools"): "Id",
86
86
  ("apigateway", "rest_apis"): "id",
87
+ ("apigateway", "apis"): "ApiId",
87
88
  }
88
89
 
89
90
  # Known ID field names for extracting a resource identifier from list results
@@ -1,12 +1,12 @@
1
1
  [project]
2
2
  name = "stackport"
3
- version = "0.4.2"
3
+ version = "0.4.3"
4
4
  description = "Universal AWS resource browser for local emulators"
5
5
  readme = "README.md"
6
6
  license = {text = "MIT"}
7
7
  requires-python = ">=3.10"
8
8
  authors = [{name = "Davi Reis Vieira"}]
9
- keywords = ["aws", "ministack", "localstack", "browser", "devtools", "emulator"]
9
+ keywords = ["aws", "ministack", "floci", "localstack", "browser", "devtools", "emulator"]
10
10
  classifiers = [
11
11
  "Development Status :: 4 - Beta",
12
12
  "Framework :: FastAPI",
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stackport
3
- Version: 0.4.2
3
+ Version: 0.4.3
4
4
  Summary: Universal AWS resource browser for local emulators
5
5
  Author: Davi Reis Vieira
6
6
  License: MIT
7
7
  Project-URL: Homepage, https://github.com/DaviReisVieira/stackport
8
8
  Project-URL: Repository, https://github.com/DaviReisVieira/stackport
9
9
  Project-URL: Issues, https://github.com/DaviReisVieira/stackport/issues
10
- Keywords: aws,ministack,localstack,browser,devtools,emulator
10
+ Keywords: aws,ministack,floci,localstack,browser,devtools,emulator
11
11
  Classifier: Development Status :: 4 - Beta
12
12
  Classifier: Framework :: FastAPI
13
13
  Classifier: Intended Audience :: Developers
@@ -75,15 +75,18 @@ Dynamic: license-file
75
75
  - **CLI** — `stackport status`, `list`, `describe`, `export` with JSON/CSV/table output
76
76
  - **Real-time dashboard** with WebSocket-powered live updates
77
77
  - **Keyboard shortcuts** — 16 shortcuts for fast navigation (press `?` to view)
78
- - Single Docker image, works with MiniStack, LocalStack, Moto, or any AWS-compatible endpoint
78
+ - Single Docker image, works with [MiniStack](https://github.com/ministackorg/ministack), [Floci](https://github.com/floci-io/floci), LocalStack, Moto, or any AWS-compatible endpoint
79
79
 
80
80
  ## Quick Start
81
81
 
82
- ### With a local emulator (recommended)
82
+ ### With a local emulator
83
+
84
+ Start whichever emulator you use, then point StackPort at it:
83
85
 
84
86
  ```bash
85
- # Start MiniStack (or LocalStack, Moto, etc.)
86
- pip install ministack && ministack
87
+ # Start an emulator on :4566 (pick one)
88
+ docker run -d -p 4566:4566 ministackorg/ministack # MiniStack
89
+ docker run -d -p 4566:4566 floci/floci # Floci
87
90
 
88
91
  # Start StackPort
89
92
  pip install stackport
@@ -108,16 +111,32 @@ You can also configure per-endpoint authentication from the Settings UI — sele
108
111
 
109
112
  When connected to real AWS, StackPort shows a warning banner and operates in read-only mode unless writes are explicitly enabled.
110
113
 
111
- ### Docker Compose (MiniStack + StackPort)
114
+ ### Docker Compose
115
+
116
+ Each example runs the emulator, StackPort, and a one-shot `seed` container that creates a few resources (S3, SQS, SNS, DynamoDB, Secrets Manager, IAM, Logs, Step Functions) so the dashboard has something to show on first load. Both use the same seed. Swap in LocalStack, Moto, or any AWS-compatible endpoint by replacing the emulator service and updating `AWS_ENDPOINT_URL`.
112
117
 
113
- This example uses [MiniStack](https://github.com/Nahuel990/ministack) as the emulator, but you can swap it for LocalStack, Moto, or any AWS-compatible endpoint — just update `AWS_ENDPOINT_URL`.
118
+ **MiniStack + StackPort** — [`examples/docker-compose.yml`](examples/docker-compose.yml)
114
119
 
115
120
  ```bash
116
121
  curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.yml
117
122
  docker compose up -d
118
123
  # Open http://localhost:8080
124
+ # If the dashboard is empty: docker compose logs seed
119
125
  ```
120
126
 
127
+ [MiniStack](https://github.com/ministackorg/ministack) derives the URLs it returns (SQS queue URLs, pre-signed URLs) from the request `Host`, so they resolve from other containers and from the host without extra configuration.
128
+
129
+ **Floci + StackPort** — [`examples/docker-compose.floci.yml`](examples/docker-compose.floci.yml)
130
+
131
+ ```bash
132
+ curl -O https://raw.githubusercontent.com/DaviReisVieira/stackport/main/examples/docker-compose.floci.yml
133
+ docker compose -f docker-compose.floci.yml up -d
134
+ # Open http://localhost:8080
135
+ # If the dashboard is empty: docker compose -f docker-compose.floci.yml logs seed
136
+ ```
137
+
138
+ [Floci](https://github.com/floci-io/floci) takes the hostname for the URLs it returns from `FLOCI_HOSTNAME`, which the example sets to `floci` so they resolve from other containers. From the host, reach the same resources through `http://localhost:4566`.
139
+
121
140
  ### Docker (standalone)
122
141
 
123
142
  ```bash
@@ -351,6 +370,10 @@ ui/src/
351
370
  lib/ API client, types, service icons, utils
352
371
  ```
353
372
 
373
+ ## Origin
374
+
375
+ StackPort started as a web UI pull request to [MiniStack](https://github.com/ministackorg/ministack) ([ministack#116](https://github.com/ministackorg/ministack/pull/116)). The MiniStack maintainers suggested it would be more useful as a standalone tool that talks to any emulator over the standard AWS API, and that is what it became. Thanks to [@Nahuel990](https://github.com/Nahuel990) and [@McDoit](https://github.com/McDoit) for pushing it in that direction.
376
+
354
377
  ## Contributors
355
378
 
356
379
  <a href="https://github.com/DaviReisVieira/stackport/graphs/contributors">
@@ -15,27 +15,27 @@ backend/../ui/dist/favicon.png
15
15
  backend/../ui/dist/favicon.svg
16
16
  backend/../ui/dist/index.html
17
17
  backend/../ui/dist/stackport-logo.svg
18
- backend/../ui/dist/assets/CloudscapeAbout-DccVyQFu.js
19
- backend/../ui/dist/assets/CloudscapeDashboard-DtV4-eQq.js
20
- backend/../ui/dist/assets/CloudscapeLearn-DW6RZAj4.js
18
+ backend/../ui/dist/assets/CloudscapeAbout-BRbsDskR.js
19
+ backend/../ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js
20
+ backend/../ui/dist/assets/CloudscapeLearn-CVW3k3X0.js
21
+ backend/../ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js
21
22
  backend/../ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css
22
- backend/../ui/dist/assets/CloudscapeResourceBrowser-FpsXtjf_.js
23
- backend/../ui/dist/assets/CloudscapeSettings-UdgiI6dm.js
23
+ backend/../ui/dist/assets/CloudscapeSettings-CIXFvPKv.js
24
24
  backend/../ui/dist/assets/CloudscapeShell-BXHVWjDE.css
25
- backend/../ui/dist/assets/CloudscapeShell-Be6uFzsz.js
26
- backend/../ui/dist/assets/StateMachineGraph-G76o42iO.js
27
- backend/../ui/dist/assets/index-B33aGwWE.js
28
- backend/../ui/dist/assets/index-B3bka_BF.js
29
- backend/../ui/dist/assets/index-B3zGVRXf.js
30
- backend/../ui/dist/assets/index-CNWD72L_.js
25
+ backend/../ui/dist/assets/CloudscapeShell-DNfhSg0O.js
26
+ backend/../ui/dist/assets/StateMachineGraph-DvF_kIOO.js
27
+ backend/../ui/dist/assets/index-BC7E-Fm4.js
28
+ backend/../ui/dist/assets/index-BLdRlH9d.js
29
+ backend/../ui/dist/assets/index-Ba-oOcd3.js
30
+ backend/../ui/dist/assets/index-BkSgVQ0X.js
31
+ backend/../ui/dist/assets/index-C5z3cir-.js
31
32
  backend/../ui/dist/assets/index-DE--A2EE.css
32
33
  backend/../ui/dist/assets/index-DKIdXFX0.css
33
34
  backend/../ui/dist/assets/index-DNBdeKId.css
34
- backend/../ui/dist/assets/index-DhD1oh4N.js
35
35
  backend/../ui/dist/assets/index-DoY0HscW.css
36
36
  backend/../ui/dist/assets/index-DrkkXAoG.css
37
+ backend/../ui/dist/assets/index-Dw6TGweb.js
37
38
  backend/../ui/dist/assets/index-Vf0UFiCT.css
38
- backend/../ui/dist/assets/index-hhBdqMqs.js
39
39
  backend/../ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
40
40
  backend/../ui/dist/assets/utils-Cfmwk2UT.js
41
41
  backend/../ui/dist/aws-icons/acm.svg
@@ -148,27 +148,27 @@ ui/dist/favicon.png
148
148
  ui/dist/favicon.svg
149
149
  ui/dist/index.html
150
150
  ui/dist/stackport-logo.svg
151
- ui/dist/assets/CloudscapeAbout-DccVyQFu.js
152
- ui/dist/assets/CloudscapeDashboard-DtV4-eQq.js
153
- ui/dist/assets/CloudscapeLearn-DW6RZAj4.js
151
+ ui/dist/assets/CloudscapeAbout-BRbsDskR.js
152
+ ui/dist/assets/CloudscapeDashboard-j2f4Elz2.js
153
+ ui/dist/assets/CloudscapeLearn-CVW3k3X0.js
154
+ ui/dist/assets/CloudscapeResourceBrowser-CgWdgn20.js
154
155
  ui/dist/assets/CloudscapeResourceBrowser-CzwePG-V.css
155
- ui/dist/assets/CloudscapeResourceBrowser-FpsXtjf_.js
156
- ui/dist/assets/CloudscapeSettings-UdgiI6dm.js
156
+ ui/dist/assets/CloudscapeSettings-CIXFvPKv.js
157
157
  ui/dist/assets/CloudscapeShell-BXHVWjDE.css
158
- ui/dist/assets/CloudscapeShell-Be6uFzsz.js
159
- ui/dist/assets/StateMachineGraph-G76o42iO.js
160
- ui/dist/assets/index-B33aGwWE.js
161
- ui/dist/assets/index-B3bka_BF.js
162
- ui/dist/assets/index-B3zGVRXf.js
163
- ui/dist/assets/index-CNWD72L_.js
158
+ ui/dist/assets/CloudscapeShell-DNfhSg0O.js
159
+ ui/dist/assets/StateMachineGraph-DvF_kIOO.js
160
+ ui/dist/assets/index-BC7E-Fm4.js
161
+ ui/dist/assets/index-BLdRlH9d.js
162
+ ui/dist/assets/index-Ba-oOcd3.js
163
+ ui/dist/assets/index-BkSgVQ0X.js
164
+ ui/dist/assets/index-C5z3cir-.js
164
165
  ui/dist/assets/index-DE--A2EE.css
165
166
  ui/dist/assets/index-DKIdXFX0.css
166
167
  ui/dist/assets/index-DNBdeKId.css
167
- ui/dist/assets/index-DhD1oh4N.js
168
168
  ui/dist/assets/index-DoY0HscW.css
169
169
  ui/dist/assets/index-DrkkXAoG.css
170
+ ui/dist/assets/index-Dw6TGweb.js
170
171
  ui/dist/assets/index-Vf0UFiCT.css
171
- ui/dist/assets/index-hhBdqMqs.js
172
172
  ui/dist/assets/stepfunctions-graph-B6nvU4OB.js
173
173
  ui/dist/assets/utils-Cfmwk2UT.js
174
174
  ui/dist/aws-icons/acm.svg
@@ -120,3 +120,68 @@ class TestCLI:
120
120
  result = runner.invoke(cli, ["export", "s3", "--format", format_type])
121
121
  # Should either succeed or fail gracefully
122
122
  assert result.exit_code in [0, 1, 2]
123
+
124
+
125
+ class _FakeClient:
126
+ """Answers any boto3 method with an empty response."""
127
+
128
+ def __getattr__(self, name):
129
+ return lambda **kwargs: {}
130
+
131
+
132
+ class TestEndpointResolution:
133
+ """--endpoint must reach get_client even when ~/.stackport has a saved default (#171)."""
134
+
135
+ def _capture_get_client(self, monkeypatch):
136
+ calls = []
137
+
138
+ def fake_get_client(service_name, **kwargs):
139
+ calls.append((service_name, kwargs))
140
+ return _FakeClient()
141
+
142
+ monkeypatch.setattr("backend.cli.get_client", fake_get_client)
143
+ return calls
144
+
145
+ def test_list_passes_explicit_endpoint(self, monkeypatch):
146
+ calls = self._capture_get_client(monkeypatch)
147
+ monkeypatch.setattr("backend.cli.endpoint_store.get_default_url", lambda: "http://saved:4566")
148
+ result = CliRunner().invoke(cli, ["list", "s3", "--endpoint", "http://flag:4599", "--region", "eu-west-1"])
149
+ assert result.exit_code == 0, result.output
150
+ assert calls, "list never created a client"
151
+ for _, kwargs in calls:
152
+ assert kwargs["endpoint_url"] == "http://flag:4599"
153
+ assert kwargs["region"] == "eu-west-1"
154
+
155
+ def test_list_falls_back_to_saved_default(self, monkeypatch):
156
+ calls = self._capture_get_client(monkeypatch)
157
+ # Other test modules put AWS_ENDPOINT_URL back into the environment; click reads it via envvar.
158
+ monkeypatch.delenv("AWS_ENDPOINT_URL", raising=False)
159
+ monkeypatch.setattr("backend.cli.AWS_ENDPOINT_URL", None)
160
+ monkeypatch.setattr("backend.cli.endpoint_store.get_default_url", lambda: "http://saved:4566")
161
+ # Rebuild the option default: click captured AWS_ENDPOINT_URL at import time.
162
+ for param in cli.commands["list"].params:
163
+ if param.name == "endpoint":
164
+ monkeypatch.setattr(param, "default", None)
165
+ result = CliRunner().invoke(cli, ["list", "s3"])
166
+ assert result.exit_code == 0, result.output
167
+ assert calls and all(kwargs["endpoint_url"] == "http://saved:4566" for _, kwargs in calls)
168
+
169
+ def test_describe_passes_explicit_endpoint(self, monkeypatch):
170
+ calls = self._capture_get_client(monkeypatch)
171
+ result = CliRunner().invoke(cli, ["describe", "s3", "buckets", "demo", "--endpoint", "http://flag:4599"])
172
+ assert result.exit_code == 0, result.output
173
+ assert calls and all(kwargs["endpoint_url"] == "http://flag:4599" for _, kwargs in calls)
174
+
175
+ def test_status_probes_every_service_at_the_given_endpoint(self, monkeypatch):
176
+ probed = []
177
+
178
+ def fake_probe(service, endpoint_url, region=None, **_):
179
+ probed.append((service, endpoint_url, region))
180
+ return service, {"status": "available", "resources": {}}
181
+
182
+ monkeypatch.setattr("backend.cli._probe_service", fake_probe)
183
+ result = CliRunner().invoke(cli, ["status", "--endpoint", "http://flag:4599", "--output", "json"])
184
+ assert result.exit_code == 0, result.output
185
+ assert probed, "status never probed anything"
186
+ assert {url for _, url, _ in probed} == {"http://flag:4599"}
187
+ assert isinstance(json.loads(result.output), dict)
@@ -1,6 +1,12 @@
1
1
  """Validate SERVICE_REGISTRY, DESCRIBE_REGISTRY, and _METHOD_KWARGS consistency."""
2
2
 
3
- from backend.routes.resources import DESCRIBE_REGISTRY, _ID_FIELDS
3
+ from backend.routes.resources import (
4
+ DESCRIBE_REGISTRY,
5
+ _ID_FIELDS,
6
+ _PREFERRED_ID_FIELD,
7
+ _extract_id,
8
+ _summarize_item,
9
+ )
4
10
  from backend.routes.stats import SERVICE_REGISTRY, _METHOD_KWARGS
5
11
 
6
12
 
@@ -67,3 +73,13 @@ class TestIdFields:
67
73
 
68
74
  def test_no_duplicates(self):
69
75
  assert len(_ID_FIELDS) == len(set(_ID_FIELDS))
76
+
77
+
78
+ class TestPreferredIdField:
79
+ def test_apigatewayv2_api_id_wins_over_name(self):
80
+ """get_api takes ApiId, but the list item also has Name, which ranks first
81
+ in _ID_FIELDS — without the override the detail lookup 404s."""
82
+ item = {"ApiId": "acf2a85b", "Name": "my-api", "ProtocolType": "HTTP"}
83
+ preferred = _PREFERRED_ID_FIELD.get(("apigateway", "apis"))
84
+ assert _extract_id(item, preferred) == "acf2a85b"
85
+ assert _summarize_item(item, preferred)["id"] == "acf2a85b"
@@ -1 +1 @@
1
- import{b as j,r as m,dY as f,u as y,j as e,S as o,B as p,A as b,e as i,d as u,L as v}from"./index-hhBdqMqs.js";import{C as S,H as a,K as d,B as h}from"./CloudscapeShell-Be6uFzsz.js";import{C}from"./index-CNWD72L_.js";import{f as g}from"./utils-Cfmwk2UT.js";const k=[{label:"GitHub Repository",url:"https://github.com/DaviReisVieira/stackport"},{label:"Report an Issue",url:"https://github.com/DaviReisVieira/stackport/issues"}];function r(t,n){return e.jsxs("div",{children:[e.jsx(u,{variant:"awsui-key-label",children:t}),e.jsx(u,{fontSize:"body-s",children:n})]},t)}function z(){const{activeEndpoint:t}=j(),n=m.useCallback(()=>f(t),[t]),{data:s,error:c,refresh:x}=y(n,1e4);return e.jsx(S,{activeHref:"/about",children:e.jsxs(o,{size:"l",children:[e.jsx(a,{variant:"h1",description:"System information and project details",actions:e.jsx(p,{iconName:"refresh",onClick:()=>x(),ariaLabel:"Refresh system info"}),children:"About"}),c&&!s&&e.jsx(b,{type:"error",header:"Failed to load system info",action:e.jsx(p,{onClick:()=>x(),children:"Retry"}),children:c}),!s&&!c&&e.jsx(i,{type:"loading",children:"Loading system info"}),s&&e.jsxs(C,{columns:3,children:[e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Project"}),children:e.jsxs(o,{size:"s",children:[r("Name","StackPort"),r("Version",e.jsx(h,{color:"blue",children:s.version})),r("License","MIT"),e.jsx(u,{color:"text-body-secondary",fontSize:"body-s",children:"Universal AWS resource browser for local emulators"})]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Connection"}),children:e.jsxs(o,{size:"s",children:[r("Mode",s.connection_type==="local"?e.jsx(h,{color:"grey",children:"Local Emulator"}):e.jsx(h,{color:"severity-medium",children:"Real AWS"})),r("Endpoint",s.endpoint_url??"AWS (default)"),r("Region",s.region),r("Services",String(s.services_count)),r("Writes",s.writes_enabled?e.jsx(i,{type:"success",children:"Enabled"}):e.jsx(i,{type:"stopped",children:"Disabled"})),r("Uptime",g(s.uptime_seconds)),r("Status",s.status==="ok"?e.jsx(i,{type:"success",children:"ok"}):e.jsx(i,{type:"error",children:s.status}))]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Links"}),children:e.jsx(o,{size:"s",children:k.map(l=>e.jsx(v,{href:l.url,external:!0,children:l.label},l.url))})})]})]})})}export{z as default};
1
+ import{b as j,r as m,dY as f,u as y,j as e,S as o,B as p,A as b,e as i,d as u,L as v}from"./index-BLdRlH9d.js";import{C as S,H as a,K as d,B as h}from"./CloudscapeShell-DNfhSg0O.js";import{C}from"./index-C5z3cir-.js";import{f as g}from"./utils-Cfmwk2UT.js";const k=[{label:"GitHub Repository",url:"https://github.com/DaviReisVieira/stackport"},{label:"Report an Issue",url:"https://github.com/DaviReisVieira/stackport/issues"}];function r(t,n){return e.jsxs("div",{children:[e.jsx(u,{variant:"awsui-key-label",children:t}),e.jsx(u,{fontSize:"body-s",children:n})]},t)}function z(){const{activeEndpoint:t}=j(),n=m.useCallback(()=>f(t),[t]),{data:s,error:c,refresh:x}=y(n,1e4);return e.jsx(S,{activeHref:"/about",children:e.jsxs(o,{size:"l",children:[e.jsx(a,{variant:"h1",description:"System information and project details",actions:e.jsx(p,{iconName:"refresh",onClick:()=>x(),ariaLabel:"Refresh system info"}),children:"About"}),c&&!s&&e.jsx(b,{type:"error",header:"Failed to load system info",action:e.jsx(p,{onClick:()=>x(),children:"Retry"}),children:c}),!s&&!c&&e.jsx(i,{type:"loading",children:"Loading system info"}),s&&e.jsxs(C,{columns:3,children:[e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Project"}),children:e.jsxs(o,{size:"s",children:[r("Name","StackPort"),r("Version",e.jsx(h,{color:"blue",children:s.version})),r("License","MIT"),e.jsx(u,{color:"text-body-secondary",fontSize:"body-s",children:"Universal AWS resource browser for local emulators"})]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Connection"}),children:e.jsxs(o,{size:"s",children:[r("Mode",s.connection_type==="local"?e.jsx(h,{color:"grey",children:"Local Emulator"}):e.jsx(h,{color:"severity-medium",children:"Real AWS"})),r("Endpoint",s.endpoint_url??"AWS (default)"),r("Region",s.region),r("Services",String(s.services_count)),r("Writes",s.writes_enabled?e.jsx(i,{type:"success",children:"Enabled"}):e.jsx(i,{type:"stopped",children:"Disabled"})),r("Uptime",g(s.uptime_seconds)),r("Status",s.status==="ok"?e.jsx(i,{type:"success",children:"ok"}):e.jsx(i,{type:"error",children:s.status}))]})}),e.jsx(d,{header:e.jsx(a,{variant:"h3",children:"Links"}),children:e.jsx(o,{size:"s",children:k.map(l=>e.jsx(v,{href:l.url,external:!0,children:l.label},l.url))})})]})]})})}export{z as default};
@@ -1 +1 @@
1
- import{r as a,u as L,a as W,b as O,c as A,f as D,j as t,S as $,A as E,B as R,d,e as F,L as M}from"./index-hhBdqMqs.js";import{u as P,a as H,H as B,C as U,B as _,g as V}from"./CloudscapeShell-Be6uFzsz.js";import{S as J,C as G,T as K}from"./index-DhD1oh4N.js";import{C as q}from"./index-CNWD72L_.js";import{C as Q}from"./index-B3bka_BF.js";import{T as X}from"./index-B3zGVRXf.js";import{f as Y}from"./utils-Cfmwk2UT.js";function Z({fallbackFetcher:n,fallbackInterval:r=5e3,messageType:i,endpoint:x}){const[s,g]=a.useState(null),[y,j]=a.useState(!1),[p,C]=a.useState(!1),h=a.useRef(null),c=a.useRef(0),b=a.useRef(null),l=a.useRef(),k=3;a.useEffect(()=>{let v=!1;c.current=0;function S(){var o;if(v||((o=h.current)==null?void 0:o.readyState)===WebSocket.OPEN)return;const m=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${m}//${window.location.host}/ws`);h.current=e,e.onopen=()=>{if(v){e.close();return}j(!0),c.current=0,e.send(JSON.stringify({type:"subscribe",services:["all"],endpoint:x??void 0}))},e.onmessage=w=>{try{const f=JSON.parse(w.data);f.type===i&&f.data&&g(f.data)}catch{}},e.onclose=()=>{if(j(!1),h.current=null,!v)if(c.current++,c.current>=k)C(!0);else{const w=Math.min(1e3*Math.pow(2,c.current),3e4);b.current=setTimeout(()=>{var f;return(f=l.current)==null?void 0:f.call(l)},w)}},e.onerror=()=>{e.close()}}return l.current=S,S(),()=>{var m;v=!0,b.current&&clearTimeout(b.current),(m=h.current)==null||m.close(),g(null),C(!1)}},[i,x]);const u=L(n,p?r:void 0),T=s??u.data,N=!T&&u.loading;return{data:T,loading:N,error:p?u.error:null,connected:y,refresh:u.refresh}}function ee(){return localStorage.getItem("stackport:view-mode")==="list"?"list":"grid"}function z(n,r){const i=V(n.name);return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(i,{className:"h-5 w-5 flex-shrink-0"}),t.jsx(M,{fontSize:"heading-m",href:`/resources/${n.name}`,onFollow:x=>{x.preventDefault(),r(n.name)},children:n.name})]})}function I(n){return n.total===0?t.jsx(d,{color:"text-status-inactive",children:"No resources"}):t.jsx($,{direction:"horizontal",size:"xs",children:Object.entries(n.resources).filter(([,r])=>r>0).map(([r,i])=>t.jsxs(_,{color:"grey",children:[r,": ",i]},r))})}function ce(){const n=W(),{activeEndpoint:r}=O(),{data:i}=A(),x=a.useCallback(()=>D(r),[r]),{data:s,loading:g,error:y,connected:j,refresh:p}=Z({fallbackFetcher:x,fallbackInterval:5e3,messageType:"stats",endpoint:r}),{toggleFavorite:C,isFavorite:h}=P(),[c,b]=a.useState(""),[l,k]=a.useState(ee);a.useEffect(()=>{localStorage.setItem("stackport:view-mode",l)},[l]),H([{key:"r",handler:()=>p()},{key:"v",handler:()=>k(e=>e==="grid"?"list":"grid")}]);const u=a.useMemo(()=>s?Object.entries(s.services).map(([e,o])=>({name:e,status:o.status,resources:o.resources,total:Object.values(o.resources).reduce((w,f)=>w+f,0),favorite:h(e)})).filter(e=>e.name.toLowerCase().includes(c.toLowerCase())).sort((e,o)=>Number(o.favorite)-Number(e.favorite)||o.total-e.total||e.name.localeCompare(o.name)):[],[s,c,h]),T=s?Object.values(s.services).filter(e=>e.status==="available").length:0,N=a.useCallback(e=>n(`/resources/${e}`),[n]),v=e=>t.jsx(R,{variant:"inline-icon",iconName:e.favorite?"star-filled":"star",ariaLabel:e.favorite?`Remove ${e.name} from favorites`:`Add ${e.name} to favorites`,onClick:()=>C(e.name)}),S=t.jsx(B,{variant:"h2",counter:s?`(${u.length})`:void 0,actions:t.jsx(J,{selectedId:l,onChange:({detail:e})=>k(e.selectedId),label:"View mode",options:[{id:"grid",text:"Grid"},{id:"list",text:"List"}]}),children:"Services"}),m=t.jsx(K,{filteringText:c,filteringPlaceholder:"Find a service",onChange:({detail:e})=>b(e.filteringText)});return t.jsx(U,{activeHref:"/",children:t.jsx(Q,{header:t.jsx(B,{variant:"h1",description:r?`Connected to ${r}`:i!=null&&i.endpoint_url?`Connected to ${i.endpoint_url}`:"Using the default endpoint",actions:t.jsxs($,{direction:"horizontal",size:"xs",children:[t.jsx(F,{type:j?"success":"in-progress",children:j?"Live":"Polling"}),t.jsx(R,{iconName:"refresh",onClick:()=>p(),loading:g,children:"Refresh"})]}),children:"Dashboard"}),children:t.jsxs($,{size:"l",children:[!s&&y&&t.jsx(E,{type:"error",header:"Could not reach the endpoint",action:t.jsx(R,{onClick:()=>p(),children:"Retry"}),children:y}),t.jsxs(q,{columns:3,variant:"text-grid",children:[t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Services available"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?`${T}/${Object.keys(s.services).length}`:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Total resources"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?s.total_resources:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Uptime"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?Y(s.uptime_seconds):"—"})]})]}),l==="grid"?t.jsx(G,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",cardsPerRow:[{cards:1},{minWidth:500,cards:2},{minWidth:900,cards:3}],header:S,filter:m,cardDefinition:{header:e=>t.jsxs("div",{className:"flex items-center justify-between gap-2",children:[z(e,N),v(e)]}),sections:[{id:"status",content:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",content:e=>I(e)}]},empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})}):t.jsx(X,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",header:S,filter:m,variant:"container",columnDefinitions:[{id:"favorite",header:"",width:60,cell:e=>v(e)},{id:"service",header:"Service",cell:e=>z(e,N),sortingField:"name"},{id:"status",header:"Status",cell:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",cell:e=>I(e)},{id:"total",header:"Total",width:90,cell:e=>e.total}],empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})})]})})})}export{ce as default};
1
+ import{r as a,u as L,a as W,b as O,c as A,f as D,j as t,S as $,A as E,B as R,d,e as F,L as M}from"./index-BLdRlH9d.js";import{u as P,a as H,H as B,C as U,B as _,g as V}from"./CloudscapeShell-DNfhSg0O.js";import{S as J,C as G,T as K}from"./index-BkSgVQ0X.js";import{C as q}from"./index-C5z3cir-.js";import{C as Q}from"./index-Ba-oOcd3.js";import{T as X}from"./index-BC7E-Fm4.js";import{f as Y}from"./utils-Cfmwk2UT.js";function Z({fallbackFetcher:n,fallbackInterval:r=5e3,messageType:i,endpoint:x}){const[s,g]=a.useState(null),[y,j]=a.useState(!1),[p,C]=a.useState(!1),h=a.useRef(null),c=a.useRef(0),b=a.useRef(null),l=a.useRef(),k=3;a.useEffect(()=>{let v=!1;c.current=0;function S(){var o;if(v||((o=h.current)==null?void 0:o.readyState)===WebSocket.OPEN)return;const m=window.location.protocol==="https:"?"wss:":"ws:",e=new WebSocket(`${m}//${window.location.host}/ws`);h.current=e,e.onopen=()=>{if(v){e.close();return}j(!0),c.current=0,e.send(JSON.stringify({type:"subscribe",services:["all"],endpoint:x??void 0}))},e.onmessage=w=>{try{const f=JSON.parse(w.data);f.type===i&&f.data&&g(f.data)}catch{}},e.onclose=()=>{if(j(!1),h.current=null,!v)if(c.current++,c.current>=k)C(!0);else{const w=Math.min(1e3*Math.pow(2,c.current),3e4);b.current=setTimeout(()=>{var f;return(f=l.current)==null?void 0:f.call(l)},w)}},e.onerror=()=>{e.close()}}return l.current=S,S(),()=>{var m;v=!0,b.current&&clearTimeout(b.current),(m=h.current)==null||m.close(),g(null),C(!1)}},[i,x]);const u=L(n,p?r:void 0),T=s??u.data,N=!T&&u.loading;return{data:T,loading:N,error:p?u.error:null,connected:y,refresh:u.refresh}}function ee(){return localStorage.getItem("stackport:view-mode")==="list"?"list":"grid"}function z(n,r){const i=V(n.name);return t.jsxs("div",{className:"flex items-center gap-2",children:[t.jsx(i,{className:"h-5 w-5 flex-shrink-0"}),t.jsx(M,{fontSize:"heading-m",href:`/resources/${n.name}`,onFollow:x=>{x.preventDefault(),r(n.name)},children:n.name})]})}function I(n){return n.total===0?t.jsx(d,{color:"text-status-inactive",children:"No resources"}):t.jsx($,{direction:"horizontal",size:"xs",children:Object.entries(n.resources).filter(([,r])=>r>0).map(([r,i])=>t.jsxs(_,{color:"grey",children:[r,": ",i]},r))})}function ce(){const n=W(),{activeEndpoint:r}=O(),{data:i}=A(),x=a.useCallback(()=>D(r),[r]),{data:s,loading:g,error:y,connected:j,refresh:p}=Z({fallbackFetcher:x,fallbackInterval:5e3,messageType:"stats",endpoint:r}),{toggleFavorite:C,isFavorite:h}=P(),[c,b]=a.useState(""),[l,k]=a.useState(ee);a.useEffect(()=>{localStorage.setItem("stackport:view-mode",l)},[l]),H([{key:"r",handler:()=>p()},{key:"v",handler:()=>k(e=>e==="grid"?"list":"grid")}]);const u=a.useMemo(()=>s?Object.entries(s.services).map(([e,o])=>({name:e,status:o.status,resources:o.resources,total:Object.values(o.resources).reduce((w,f)=>w+f,0),favorite:h(e)})).filter(e=>e.name.toLowerCase().includes(c.toLowerCase())).sort((e,o)=>Number(o.favorite)-Number(e.favorite)||o.total-e.total||e.name.localeCompare(o.name)):[],[s,c,h]),T=s?Object.values(s.services).filter(e=>e.status==="available").length:0,N=a.useCallback(e=>n(`/resources/${e}`),[n]),v=e=>t.jsx(R,{variant:"inline-icon",iconName:e.favorite?"star-filled":"star",ariaLabel:e.favorite?`Remove ${e.name} from favorites`:`Add ${e.name} to favorites`,onClick:()=>C(e.name)}),S=t.jsx(B,{variant:"h2",counter:s?`(${u.length})`:void 0,actions:t.jsx(J,{selectedId:l,onChange:({detail:e})=>k(e.selectedId),label:"View mode",options:[{id:"grid",text:"Grid"},{id:"list",text:"List"}]}),children:"Services"}),m=t.jsx(K,{filteringText:c,filteringPlaceholder:"Find a service",onChange:({detail:e})=>b(e.filteringText)});return t.jsx(U,{activeHref:"/",children:t.jsx(Q,{header:t.jsx(B,{variant:"h1",description:r?`Connected to ${r}`:i!=null&&i.endpoint_url?`Connected to ${i.endpoint_url}`:"Using the default endpoint",actions:t.jsxs($,{direction:"horizontal",size:"xs",children:[t.jsx(F,{type:j?"success":"in-progress",children:j?"Live":"Polling"}),t.jsx(R,{iconName:"refresh",onClick:()=>p(),loading:g,children:"Refresh"})]}),children:"Dashboard"}),children:t.jsxs($,{size:"l",children:[!s&&y&&t.jsx(E,{type:"error",header:"Could not reach the endpoint",action:t.jsx(R,{onClick:()=>p(),children:"Retry"}),children:y}),t.jsxs(q,{columns:3,variant:"text-grid",children:[t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Services available"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?`${T}/${Object.keys(s.services).length}`:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Total resources"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?s.total_resources:"—"})]}),t.jsxs("div",{children:[t.jsx(d,{variant:"awsui-key-label",children:"Uptime"}),t.jsx(d,{fontSize:"display-l",fontWeight:"bold",children:s?Y(s.uptime_seconds):"—"})]})]}),l==="grid"?t.jsx(G,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",cardsPerRow:[{cards:1},{minWidth:500,cards:2},{minWidth:900,cards:3}],header:S,filter:m,cardDefinition:{header:e=>t.jsxs("div",{className:"flex items-center justify-between gap-2",children:[z(e,N),v(e)]}),sections:[{id:"status",content:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",content:e=>I(e)}]},empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})}):t.jsx(X,{items:u,loading:g&&!s,loadingText:"Probing services",trackBy:"name",header:S,filter:m,variant:"container",columnDefinitions:[{id:"favorite",header:"",width:60,cell:e=>v(e)},{id:"service",header:"Service",cell:e=>z(e,N),sortingField:"name"},{id:"status",header:"Status",cell:e=>t.jsx(F,{type:e.status==="available"?"success":"error",children:e.status==="available"?"Available":"Unavailable"})},{id:"resources",header:"Resources",cell:e=>I(e)},{id:"total",header:"Total",width:90,cell:e=>e.total}],empty:t.jsx(d,{textAlign:"center",children:"No services match the filter"})})]})})})}export{ce as default};