aws-inventory-manager 0.17.12__py3-none-any.whl
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.
- aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
- aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
- aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
- aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
- aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
- aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
- src/__init__.py +3 -0
- src/aws/__init__.py +11 -0
- src/aws/client.py +128 -0
- src/aws/credentials.py +191 -0
- src/aws/rate_limiter.py +177 -0
- src/cli/__init__.py +12 -0
- src/cli/config.py +130 -0
- src/cli/main.py +4046 -0
- src/cloudtrail/__init__.py +5 -0
- src/cloudtrail/query.py +642 -0
- src/config_service/__init__.py +21 -0
- src/config_service/collector.py +346 -0
- src/config_service/detector.py +256 -0
- src/config_service/resource_type_mapping.py +328 -0
- src/cost/__init__.py +5 -0
- src/cost/analyzer.py +226 -0
- src/cost/explorer.py +209 -0
- src/cost/reporter.py +237 -0
- src/delta/__init__.py +5 -0
- src/delta/calculator.py +206 -0
- src/delta/differ.py +185 -0
- src/delta/formatters.py +272 -0
- src/delta/models.py +154 -0
- src/delta/reporter.py +234 -0
- src/matching/__init__.py +6 -0
- src/matching/config.py +52 -0
- src/matching/normalizer.py +450 -0
- src/matching/prompts.py +33 -0
- src/models/__init__.py +21 -0
- src/models/config_diff.py +135 -0
- src/models/cost_report.py +87 -0
- src/models/deletion_operation.py +104 -0
- src/models/deletion_record.py +97 -0
- src/models/delta_report.py +122 -0
- src/models/efs_resource.py +80 -0
- src/models/elasticache_resource.py +90 -0
- src/models/group.py +318 -0
- src/models/inventory.py +133 -0
- src/models/protection_rule.py +123 -0
- src/models/report.py +288 -0
- src/models/resource.py +111 -0
- src/models/security_finding.py +102 -0
- src/models/snapshot.py +122 -0
- src/restore/__init__.py +20 -0
- src/restore/audit.py +175 -0
- src/restore/cleaner.py +461 -0
- src/restore/config.py +209 -0
- src/restore/deleter.py +976 -0
- src/restore/dependency.py +254 -0
- src/restore/safety.py +115 -0
- src/security/__init__.py +0 -0
- src/security/checks/__init__.py +0 -0
- src/security/checks/base.py +56 -0
- src/security/checks/ec2_checks.py +88 -0
- src/security/checks/elasticache_checks.py +149 -0
- src/security/checks/iam_checks.py +102 -0
- src/security/checks/rds_checks.py +140 -0
- src/security/checks/s3_checks.py +95 -0
- src/security/checks/secrets_checks.py +96 -0
- src/security/checks/sg_checks.py +142 -0
- src/security/cis_mapper.py +97 -0
- src/security/models.py +53 -0
- src/security/reporter.py +174 -0
- src/security/scanner.py +87 -0
- src/snapshot/__init__.py +6 -0
- src/snapshot/capturer.py +453 -0
- src/snapshot/filter.py +259 -0
- src/snapshot/inventory_storage.py +236 -0
- src/snapshot/report_formatter.py +250 -0
- src/snapshot/reporter.py +189 -0
- src/snapshot/resource_collectors/__init__.py +5 -0
- src/snapshot/resource_collectors/apigateway.py +140 -0
- src/snapshot/resource_collectors/backup.py +136 -0
- src/snapshot/resource_collectors/base.py +81 -0
- src/snapshot/resource_collectors/cloudformation.py +55 -0
- src/snapshot/resource_collectors/cloudwatch.py +109 -0
- src/snapshot/resource_collectors/codebuild.py +69 -0
- src/snapshot/resource_collectors/codepipeline.py +82 -0
- src/snapshot/resource_collectors/dynamodb.py +65 -0
- src/snapshot/resource_collectors/ec2.py +240 -0
- src/snapshot/resource_collectors/ecs.py +215 -0
- src/snapshot/resource_collectors/efs_collector.py +102 -0
- src/snapshot/resource_collectors/eks.py +200 -0
- src/snapshot/resource_collectors/elasticache_collector.py +79 -0
- src/snapshot/resource_collectors/elb.py +126 -0
- src/snapshot/resource_collectors/eventbridge.py +156 -0
- src/snapshot/resource_collectors/glue.py +199 -0
- src/snapshot/resource_collectors/iam.py +188 -0
- src/snapshot/resource_collectors/kms.py +111 -0
- src/snapshot/resource_collectors/lambda_func.py +139 -0
- src/snapshot/resource_collectors/rds.py +109 -0
- src/snapshot/resource_collectors/route53.py +86 -0
- src/snapshot/resource_collectors/s3.py +105 -0
- src/snapshot/resource_collectors/secretsmanager.py +70 -0
- src/snapshot/resource_collectors/sns.py +68 -0
- src/snapshot/resource_collectors/sqs.py +82 -0
- src/snapshot/resource_collectors/ssm.py +160 -0
- src/snapshot/resource_collectors/stepfunctions.py +74 -0
- src/snapshot/resource_collectors/vpcendpoints.py +79 -0
- src/snapshot/resource_collectors/waf.py +159 -0
- src/snapshot/storage.py +351 -0
- src/storage/__init__.py +21 -0
- src/storage/audit_store.py +419 -0
- src/storage/database.py +294 -0
- src/storage/group_store.py +763 -0
- src/storage/inventory_store.py +320 -0
- src/storage/resource_store.py +416 -0
- src/storage/schema.py +339 -0
- src/storage/snapshot_store.py +363 -0
- src/utils/__init__.py +12 -0
- src/utils/export.py +305 -0
- src/utils/hash.py +60 -0
- src/utils/logging.py +63 -0
- src/utils/pagination.py +41 -0
- src/utils/paths.py +51 -0
- src/utils/progress.py +41 -0
- src/utils/unsupported_resources.py +306 -0
- src/web/__init__.py +5 -0
- src/web/app.py +97 -0
- src/web/dependencies.py +69 -0
- src/web/routes/__init__.py +1 -0
- src/web/routes/api/__init__.py +18 -0
- src/web/routes/api/charts.py +156 -0
- src/web/routes/api/cleanup.py +186 -0
- src/web/routes/api/filters.py +253 -0
- src/web/routes/api/groups.py +305 -0
- src/web/routes/api/inventories.py +80 -0
- src/web/routes/api/queries.py +202 -0
- src/web/routes/api/resources.py +393 -0
- src/web/routes/api/snapshots.py +314 -0
- src/web/routes/api/views.py +260 -0
- src/web/routes/pages.py +198 -0
- src/web/services/__init__.py +1 -0
- src/web/templates/base.html +955 -0
- src/web/templates/components/navbar.html +31 -0
- src/web/templates/components/sidebar.html +104 -0
- src/web/templates/pages/audit_logs.html +86 -0
- src/web/templates/pages/cleanup.html +279 -0
- src/web/templates/pages/dashboard.html +227 -0
- src/web/templates/pages/diff.html +175 -0
- src/web/templates/pages/error.html +30 -0
- src/web/templates/pages/groups.html +721 -0
- src/web/templates/pages/queries.html +246 -0
- src/web/templates/pages/resources.html +2429 -0
- src/web/templates/pages/snapshot_detail.html +271 -0
- src/web/templates/pages/snapshots.html +429 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"""Saved views API endpoints."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import List, Optional
|
|
8
|
+
|
|
9
|
+
from fastapi import APIRouter, HTTPException
|
|
10
|
+
from pydantic import BaseModel
|
|
11
|
+
|
|
12
|
+
from ...dependencies import get_database
|
|
13
|
+
|
|
14
|
+
router = APIRouter(prefix="/views")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ColumnConfig(BaseModel):
|
|
18
|
+
"""Column configuration model."""
|
|
19
|
+
|
|
20
|
+
field: str
|
|
21
|
+
label: str
|
|
22
|
+
visible: bool = True
|
|
23
|
+
width: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ViewConfig(BaseModel):
|
|
27
|
+
"""View configuration model."""
|
|
28
|
+
|
|
29
|
+
columns: List[ColumnConfig]
|
|
30
|
+
sort_by: Optional[str] = None
|
|
31
|
+
sort_order: str = "asc"
|
|
32
|
+
filters: Optional[dict] = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SavedView(BaseModel):
|
|
36
|
+
"""Saved view model."""
|
|
37
|
+
|
|
38
|
+
id: Optional[int] = None
|
|
39
|
+
name: str
|
|
40
|
+
description: Optional[str] = None
|
|
41
|
+
view_config: ViewConfig
|
|
42
|
+
is_default: bool = False
|
|
43
|
+
is_favorite: bool = False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Define available columns for resources
|
|
47
|
+
AVAILABLE_COLUMNS = [
|
|
48
|
+
{"field": "name", "label": "Name", "default": True},
|
|
49
|
+
{"field": "arn", "label": "ARN", "default": True},
|
|
50
|
+
{"field": "resource_type", "label": "Type", "default": True},
|
|
51
|
+
{"field": "region", "label": "Region", "default": True},
|
|
52
|
+
{"field": "snapshot_name", "label": "Snapshot", "default": True},
|
|
53
|
+
{"field": "tags", "label": "Tags", "default": False},
|
|
54
|
+
{"field": "created_at", "label": "Created", "default": False},
|
|
55
|
+
{"field": "config_hash", "label": "Config Hash", "default": False},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@router.get("/columns")
|
|
60
|
+
async def get_available_columns():
|
|
61
|
+
"""Get list of available columns for customization."""
|
|
62
|
+
return {"columns": AVAILABLE_COLUMNS}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.get("")
|
|
66
|
+
async def list_saved_views(favorites_only: bool = False):
|
|
67
|
+
"""List saved views."""
|
|
68
|
+
db = get_database()
|
|
69
|
+
|
|
70
|
+
sql = "SELECT * FROM saved_views WHERE 1=1"
|
|
71
|
+
params: List = []
|
|
72
|
+
|
|
73
|
+
if favorites_only:
|
|
74
|
+
sql += " AND is_favorite = 1"
|
|
75
|
+
|
|
76
|
+
sql += " ORDER BY is_default DESC, is_favorite DESC, last_used_at DESC NULLS LAST, name"
|
|
77
|
+
|
|
78
|
+
rows = db.fetchall(sql, tuple(params))
|
|
79
|
+
|
|
80
|
+
views = []
|
|
81
|
+
for row in rows:
|
|
82
|
+
row_dict = dict(row)
|
|
83
|
+
if row_dict.get("view_config"):
|
|
84
|
+
row_dict["view_config"] = json.loads(row_dict["view_config"])
|
|
85
|
+
views.append(row_dict)
|
|
86
|
+
|
|
87
|
+
return {"views": views}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.get("/default")
|
|
91
|
+
async def get_default_view():
|
|
92
|
+
"""Get the default view configuration."""
|
|
93
|
+
db = get_database()
|
|
94
|
+
|
|
95
|
+
row = db.fetchone("SELECT * FROM saved_views WHERE is_default = 1")
|
|
96
|
+
|
|
97
|
+
if row:
|
|
98
|
+
row_dict = dict(row)
|
|
99
|
+
if row_dict.get("view_config"):
|
|
100
|
+
row_dict["view_config"] = json.loads(row_dict["view_config"])
|
|
101
|
+
return row_dict
|
|
102
|
+
|
|
103
|
+
# Return a default configuration if none is set
|
|
104
|
+
return {
|
|
105
|
+
"id": None,
|
|
106
|
+
"name": "Default View",
|
|
107
|
+
"view_config": {
|
|
108
|
+
"columns": [
|
|
109
|
+
{"field": c["field"], "label": c["label"], "visible": c["default"]}
|
|
110
|
+
for c in AVAILABLE_COLUMNS
|
|
111
|
+
],
|
|
112
|
+
"sort_by": "name",
|
|
113
|
+
"sort_order": "asc",
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
@router.post("")
|
|
119
|
+
async def create_saved_view(view: SavedView):
|
|
120
|
+
"""Save a new view."""
|
|
121
|
+
db = get_database()
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
# If this is set as default, unset other defaults
|
|
125
|
+
if view.is_default:
|
|
126
|
+
db.execute("UPDATE saved_views SET is_default = 0 WHERE is_default = 1")
|
|
127
|
+
|
|
128
|
+
config_json = json.dumps(view.view_config.model_dump())
|
|
129
|
+
cursor = db.execute(
|
|
130
|
+
"""
|
|
131
|
+
INSERT INTO saved_views (name, description, view_config, is_default, is_favorite, created_at)
|
|
132
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
133
|
+
""",
|
|
134
|
+
(
|
|
135
|
+
view.name,
|
|
136
|
+
view.description,
|
|
137
|
+
config_json,
|
|
138
|
+
view.is_default,
|
|
139
|
+
view.is_favorite,
|
|
140
|
+
datetime.utcnow().isoformat(),
|
|
141
|
+
),
|
|
142
|
+
)
|
|
143
|
+
db._conn.commit() # type: ignore
|
|
144
|
+
return {"id": cursor.lastrowid, "message": "View saved"}
|
|
145
|
+
except Exception as e:
|
|
146
|
+
if "UNIQUE constraint" in str(e):
|
|
147
|
+
raise HTTPException(status_code=400, detail=f"View with name '{view.name}' already exists")
|
|
148
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
@router.get("/{view_id}")
|
|
152
|
+
async def get_saved_view(view_id: int):
|
|
153
|
+
"""Get a saved view by ID."""
|
|
154
|
+
db = get_database()
|
|
155
|
+
row = db.fetchone("SELECT * FROM saved_views WHERE id = ?", (view_id,))
|
|
156
|
+
|
|
157
|
+
if not row:
|
|
158
|
+
raise HTTPException(status_code=404, detail="View not found")
|
|
159
|
+
|
|
160
|
+
row_dict = dict(row)
|
|
161
|
+
if row_dict.get("view_config"):
|
|
162
|
+
row_dict["view_config"] = json.loads(row_dict["view_config"])
|
|
163
|
+
return row_dict
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@router.put("/{view_id}")
|
|
167
|
+
async def update_saved_view(view_id: int, view: SavedView):
|
|
168
|
+
"""Update a saved view."""
|
|
169
|
+
db = get_database()
|
|
170
|
+
|
|
171
|
+
existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
|
|
172
|
+
if not existing:
|
|
173
|
+
raise HTTPException(status_code=404, detail="View not found")
|
|
174
|
+
|
|
175
|
+
# If this is set as default, unset other defaults
|
|
176
|
+
if view.is_default:
|
|
177
|
+
db.execute("UPDATE saved_views SET is_default = 0 WHERE is_default = 1 AND id != ?", (view_id,))
|
|
178
|
+
|
|
179
|
+
config_json = json.dumps(view.view_config.model_dump())
|
|
180
|
+
db.execute(
|
|
181
|
+
"""
|
|
182
|
+
UPDATE saved_views
|
|
183
|
+
SET name = ?, description = ?, view_config = ?, is_default = ?, is_favorite = ?
|
|
184
|
+
WHERE id = ?
|
|
185
|
+
""",
|
|
186
|
+
(view.name, view.description, config_json, view.is_default, view.is_favorite, view_id),
|
|
187
|
+
)
|
|
188
|
+
db._conn.commit() # type: ignore
|
|
189
|
+
return {"message": "View updated"}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@router.delete("/{view_id}")
|
|
193
|
+
async def delete_saved_view(view_id: int):
|
|
194
|
+
"""Delete a saved view."""
|
|
195
|
+
db = get_database()
|
|
196
|
+
|
|
197
|
+
existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
|
|
198
|
+
if not existing:
|
|
199
|
+
raise HTTPException(status_code=404, detail="View not found")
|
|
200
|
+
|
|
201
|
+
db.execute("DELETE FROM saved_views WHERE id = ?", (view_id,))
|
|
202
|
+
db._conn.commit() # type: ignore
|
|
203
|
+
return {"message": "View deleted"}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@router.post("/{view_id}/use")
|
|
207
|
+
async def mark_view_used(view_id: int):
|
|
208
|
+
"""Mark a view as used (updates last_used_at and use_count)."""
|
|
209
|
+
db = get_database()
|
|
210
|
+
|
|
211
|
+
existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
|
|
212
|
+
if not existing:
|
|
213
|
+
raise HTTPException(status_code=404, detail="View not found")
|
|
214
|
+
|
|
215
|
+
db.execute(
|
|
216
|
+
"""
|
|
217
|
+
UPDATE saved_views
|
|
218
|
+
SET last_used_at = ?, use_count = use_count + 1
|
|
219
|
+
WHERE id = ?
|
|
220
|
+
""",
|
|
221
|
+
(datetime.utcnow().isoformat(), view_id),
|
|
222
|
+
)
|
|
223
|
+
db._conn.commit() # type: ignore
|
|
224
|
+
return {"message": "View marked as used"}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@router.post("/{view_id}/set-default")
|
|
228
|
+
async def set_default_view(view_id: int):
|
|
229
|
+
"""Set a view as the default."""
|
|
230
|
+
db = get_database()
|
|
231
|
+
|
|
232
|
+
existing = db.fetchone("SELECT id FROM saved_views WHERE id = ?", (view_id,))
|
|
233
|
+
if not existing:
|
|
234
|
+
raise HTTPException(status_code=404, detail="View not found")
|
|
235
|
+
|
|
236
|
+
# Unset current default
|
|
237
|
+
db.execute("UPDATE saved_views SET is_default = 0 WHERE is_default = 1")
|
|
238
|
+
|
|
239
|
+
# Set new default
|
|
240
|
+
db.execute("UPDATE saved_views SET is_default = 1 WHERE id = ?", (view_id,))
|
|
241
|
+
db._conn.commit() # type: ignore
|
|
242
|
+
return {"message": "View set as default"}
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@router.post("/{view_id}/favorite")
|
|
246
|
+
async def toggle_view_favorite(view_id: int):
|
|
247
|
+
"""Toggle the favorite status of a view."""
|
|
248
|
+
db = get_database()
|
|
249
|
+
|
|
250
|
+
existing = db.fetchone("SELECT id, is_favorite FROM saved_views WHERE id = ?", (view_id,))
|
|
251
|
+
if not existing:
|
|
252
|
+
raise HTTPException(status_code=404, detail="View not found")
|
|
253
|
+
|
|
254
|
+
new_favorite = not existing["is_favorite"]
|
|
255
|
+
db.execute(
|
|
256
|
+
"UPDATE saved_views SET is_favorite = ? WHERE id = ?",
|
|
257
|
+
(new_favorite, view_id),
|
|
258
|
+
)
|
|
259
|
+
db._conn.commit() # type: ignore
|
|
260
|
+
return {"message": "Favorite toggled", "is_favorite": new_favorite}
|
src/web/routes/pages.py
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""HTML page routes for the web UI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, Request
|
|
6
|
+
from fastapi.responses import HTMLResponse
|
|
7
|
+
|
|
8
|
+
from ..dependencies import get_audit_store, get_group_store, get_resource_store, get_snapshot_store
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/", response_class=HTMLResponse)
|
|
14
|
+
async def dashboard(request: Request):
|
|
15
|
+
"""Render the main dashboard page."""
|
|
16
|
+
templates = request.app.state.templates
|
|
17
|
+
|
|
18
|
+
# Get dashboard data
|
|
19
|
+
snapshot_store = get_snapshot_store()
|
|
20
|
+
resource_store = get_resource_store()
|
|
21
|
+
|
|
22
|
+
snapshots = snapshot_store.list_all()
|
|
23
|
+
total_snapshots = len(snapshots)
|
|
24
|
+
total_resources = snapshot_store.get_resource_count()
|
|
25
|
+
active_snapshot = snapshot_store.get_active()
|
|
26
|
+
|
|
27
|
+
# Get stats for charts
|
|
28
|
+
stats_by_type = resource_store.get_stats(group_by="type")[:10]
|
|
29
|
+
stats_by_region = resource_store.get_stats(group_by="region")
|
|
30
|
+
|
|
31
|
+
return templates.TemplateResponse(
|
|
32
|
+
"pages/dashboard.html",
|
|
33
|
+
{
|
|
34
|
+
"request": request,
|
|
35
|
+
"total_snapshots": total_snapshots,
|
|
36
|
+
"total_resources": total_resources,
|
|
37
|
+
"active_snapshot": active_snapshot,
|
|
38
|
+
"recent_snapshots": snapshots[:5],
|
|
39
|
+
"stats_by_type": stats_by_type,
|
|
40
|
+
"stats_by_region": stats_by_region,
|
|
41
|
+
},
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@router.get("/snapshots", response_class=HTMLResponse)
|
|
46
|
+
async def snapshots_list(request: Request):
|
|
47
|
+
"""Render the snapshots list page."""
|
|
48
|
+
templates = request.app.state.templates
|
|
49
|
+
snapshot_store = get_snapshot_store()
|
|
50
|
+
|
|
51
|
+
snapshots = snapshot_store.list_all()
|
|
52
|
+
active_snapshot = snapshot_store.get_active()
|
|
53
|
+
|
|
54
|
+
return templates.TemplateResponse(
|
|
55
|
+
"pages/snapshots.html",
|
|
56
|
+
{
|
|
57
|
+
"request": request,
|
|
58
|
+
"snapshots": snapshots,
|
|
59
|
+
"active_snapshot": active_snapshot,
|
|
60
|
+
},
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@router.get("/snapshots/{name}", response_class=HTMLResponse)
|
|
65
|
+
async def snapshot_detail(request: Request, name: str):
|
|
66
|
+
"""Render snapshot detail page."""
|
|
67
|
+
templates = request.app.state.templates
|
|
68
|
+
snapshot_store = get_snapshot_store()
|
|
69
|
+
resource_store = get_resource_store()
|
|
70
|
+
|
|
71
|
+
snapshot = snapshot_store.load(name)
|
|
72
|
+
if not snapshot:
|
|
73
|
+
return templates.TemplateResponse(
|
|
74
|
+
"pages/error.html",
|
|
75
|
+
{"request": request, "error": f"Snapshot '{name}' not found"},
|
|
76
|
+
status_code=404,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Get stats for this snapshot
|
|
80
|
+
stats_by_type = resource_store.get_stats(snapshot_name=name, group_by="type")
|
|
81
|
+
stats_by_region = resource_store.get_stats(snapshot_name=name, group_by="region")
|
|
82
|
+
|
|
83
|
+
return templates.TemplateResponse(
|
|
84
|
+
"pages/snapshot_detail.html",
|
|
85
|
+
{
|
|
86
|
+
"request": request,
|
|
87
|
+
"snapshot": snapshot,
|
|
88
|
+
"stats_by_type": stats_by_type,
|
|
89
|
+
"stats_by_region": stats_by_region,
|
|
90
|
+
},
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@router.get("/resources", response_class=HTMLResponse)
|
|
95
|
+
async def resources_list(request: Request):
|
|
96
|
+
"""Render the resource explorer page."""
|
|
97
|
+
templates = request.app.state.templates
|
|
98
|
+
resource_store = get_resource_store()
|
|
99
|
+
snapshot_store = get_snapshot_store()
|
|
100
|
+
|
|
101
|
+
# Get filter options
|
|
102
|
+
resource_types = resource_store.get_unique_resource_types()
|
|
103
|
+
regions = resource_store.get_unique_regions()
|
|
104
|
+
snapshots = snapshot_store.list_all()
|
|
105
|
+
|
|
106
|
+
return templates.TemplateResponse(
|
|
107
|
+
"pages/resources.html",
|
|
108
|
+
{
|
|
109
|
+
"request": request,
|
|
110
|
+
"resource_types": resource_types,
|
|
111
|
+
"regions": regions,
|
|
112
|
+
"snapshots": snapshots,
|
|
113
|
+
},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@router.get("/diff", response_class=HTMLResponse)
|
|
118
|
+
async def diff_page(request: Request):
|
|
119
|
+
"""Render the diff viewer page."""
|
|
120
|
+
templates = request.app.state.templates
|
|
121
|
+
snapshot_store = get_snapshot_store()
|
|
122
|
+
|
|
123
|
+
snapshots = snapshot_store.list_all()
|
|
124
|
+
|
|
125
|
+
return templates.TemplateResponse(
|
|
126
|
+
"pages/diff.html",
|
|
127
|
+
{
|
|
128
|
+
"request": request,
|
|
129
|
+
"snapshots": snapshots,
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@router.get("/queries", response_class=HTMLResponse)
|
|
135
|
+
async def queries_page(request: Request):
|
|
136
|
+
"""Render the SQL query editor page."""
|
|
137
|
+
templates = request.app.state.templates
|
|
138
|
+
|
|
139
|
+
return templates.TemplateResponse(
|
|
140
|
+
"pages/queries.html",
|
|
141
|
+
{
|
|
142
|
+
"request": request,
|
|
143
|
+
},
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@router.get("/cleanup", response_class=HTMLResponse)
|
|
148
|
+
async def cleanup_page(request: Request):
|
|
149
|
+
"""Render the cleanup operations page."""
|
|
150
|
+
templates = request.app.state.templates
|
|
151
|
+
snapshot_store = get_snapshot_store()
|
|
152
|
+
|
|
153
|
+
snapshots = snapshot_store.list_all()
|
|
154
|
+
|
|
155
|
+
return templates.TemplateResponse(
|
|
156
|
+
"pages/cleanup.html",
|
|
157
|
+
{
|
|
158
|
+
"request": request,
|
|
159
|
+
"snapshots": snapshots,
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
@router.get("/audit", response_class=HTMLResponse)
|
|
165
|
+
async def audit_logs_page(request: Request):
|
|
166
|
+
"""Render the audit logs page."""
|
|
167
|
+
templates = request.app.state.templates
|
|
168
|
+
audit_store = get_audit_store()
|
|
169
|
+
|
|
170
|
+
operations = audit_store.list_operations(limit=50)
|
|
171
|
+
|
|
172
|
+
return templates.TemplateResponse(
|
|
173
|
+
"pages/audit_logs.html",
|
|
174
|
+
{
|
|
175
|
+
"request": request,
|
|
176
|
+
"operations": operations,
|
|
177
|
+
},
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
@router.get("/groups", response_class=HTMLResponse)
|
|
182
|
+
async def groups_page(request: Request):
|
|
183
|
+
"""Render the resource groups page."""
|
|
184
|
+
templates = request.app.state.templates
|
|
185
|
+
group_store = get_group_store()
|
|
186
|
+
snapshot_store = get_snapshot_store()
|
|
187
|
+
|
|
188
|
+
groups = group_store.list_all()
|
|
189
|
+
snapshots = snapshot_store.list_all()
|
|
190
|
+
|
|
191
|
+
return templates.TemplateResponse(
|
|
192
|
+
"pages/groups.html",
|
|
193
|
+
{
|
|
194
|
+
"request": request,
|
|
195
|
+
"groups": groups,
|
|
196
|
+
"snapshots": snapshots,
|
|
197
|
+
},
|
|
198
|
+
)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Web services package."""
|