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.
Files changed (152) hide show
  1. aws_inventory_manager-0.17.12.dist-info/LICENSE +21 -0
  2. aws_inventory_manager-0.17.12.dist-info/METADATA +1292 -0
  3. aws_inventory_manager-0.17.12.dist-info/RECORD +152 -0
  4. aws_inventory_manager-0.17.12.dist-info/WHEEL +5 -0
  5. aws_inventory_manager-0.17.12.dist-info/entry_points.txt +2 -0
  6. aws_inventory_manager-0.17.12.dist-info/top_level.txt +1 -0
  7. src/__init__.py +3 -0
  8. src/aws/__init__.py +11 -0
  9. src/aws/client.py +128 -0
  10. src/aws/credentials.py +191 -0
  11. src/aws/rate_limiter.py +177 -0
  12. src/cli/__init__.py +12 -0
  13. src/cli/config.py +130 -0
  14. src/cli/main.py +4046 -0
  15. src/cloudtrail/__init__.py +5 -0
  16. src/cloudtrail/query.py +642 -0
  17. src/config_service/__init__.py +21 -0
  18. src/config_service/collector.py +346 -0
  19. src/config_service/detector.py +256 -0
  20. src/config_service/resource_type_mapping.py +328 -0
  21. src/cost/__init__.py +5 -0
  22. src/cost/analyzer.py +226 -0
  23. src/cost/explorer.py +209 -0
  24. src/cost/reporter.py +237 -0
  25. src/delta/__init__.py +5 -0
  26. src/delta/calculator.py +206 -0
  27. src/delta/differ.py +185 -0
  28. src/delta/formatters.py +272 -0
  29. src/delta/models.py +154 -0
  30. src/delta/reporter.py +234 -0
  31. src/matching/__init__.py +6 -0
  32. src/matching/config.py +52 -0
  33. src/matching/normalizer.py +450 -0
  34. src/matching/prompts.py +33 -0
  35. src/models/__init__.py +21 -0
  36. src/models/config_diff.py +135 -0
  37. src/models/cost_report.py +87 -0
  38. src/models/deletion_operation.py +104 -0
  39. src/models/deletion_record.py +97 -0
  40. src/models/delta_report.py +122 -0
  41. src/models/efs_resource.py +80 -0
  42. src/models/elasticache_resource.py +90 -0
  43. src/models/group.py +318 -0
  44. src/models/inventory.py +133 -0
  45. src/models/protection_rule.py +123 -0
  46. src/models/report.py +288 -0
  47. src/models/resource.py +111 -0
  48. src/models/security_finding.py +102 -0
  49. src/models/snapshot.py +122 -0
  50. src/restore/__init__.py +20 -0
  51. src/restore/audit.py +175 -0
  52. src/restore/cleaner.py +461 -0
  53. src/restore/config.py +209 -0
  54. src/restore/deleter.py +976 -0
  55. src/restore/dependency.py +254 -0
  56. src/restore/safety.py +115 -0
  57. src/security/__init__.py +0 -0
  58. src/security/checks/__init__.py +0 -0
  59. src/security/checks/base.py +56 -0
  60. src/security/checks/ec2_checks.py +88 -0
  61. src/security/checks/elasticache_checks.py +149 -0
  62. src/security/checks/iam_checks.py +102 -0
  63. src/security/checks/rds_checks.py +140 -0
  64. src/security/checks/s3_checks.py +95 -0
  65. src/security/checks/secrets_checks.py +96 -0
  66. src/security/checks/sg_checks.py +142 -0
  67. src/security/cis_mapper.py +97 -0
  68. src/security/models.py +53 -0
  69. src/security/reporter.py +174 -0
  70. src/security/scanner.py +87 -0
  71. src/snapshot/__init__.py +6 -0
  72. src/snapshot/capturer.py +453 -0
  73. src/snapshot/filter.py +259 -0
  74. src/snapshot/inventory_storage.py +236 -0
  75. src/snapshot/report_formatter.py +250 -0
  76. src/snapshot/reporter.py +189 -0
  77. src/snapshot/resource_collectors/__init__.py +5 -0
  78. src/snapshot/resource_collectors/apigateway.py +140 -0
  79. src/snapshot/resource_collectors/backup.py +136 -0
  80. src/snapshot/resource_collectors/base.py +81 -0
  81. src/snapshot/resource_collectors/cloudformation.py +55 -0
  82. src/snapshot/resource_collectors/cloudwatch.py +109 -0
  83. src/snapshot/resource_collectors/codebuild.py +69 -0
  84. src/snapshot/resource_collectors/codepipeline.py +82 -0
  85. src/snapshot/resource_collectors/dynamodb.py +65 -0
  86. src/snapshot/resource_collectors/ec2.py +240 -0
  87. src/snapshot/resource_collectors/ecs.py +215 -0
  88. src/snapshot/resource_collectors/efs_collector.py +102 -0
  89. src/snapshot/resource_collectors/eks.py +200 -0
  90. src/snapshot/resource_collectors/elasticache_collector.py +79 -0
  91. src/snapshot/resource_collectors/elb.py +126 -0
  92. src/snapshot/resource_collectors/eventbridge.py +156 -0
  93. src/snapshot/resource_collectors/glue.py +199 -0
  94. src/snapshot/resource_collectors/iam.py +188 -0
  95. src/snapshot/resource_collectors/kms.py +111 -0
  96. src/snapshot/resource_collectors/lambda_func.py +139 -0
  97. src/snapshot/resource_collectors/rds.py +109 -0
  98. src/snapshot/resource_collectors/route53.py +86 -0
  99. src/snapshot/resource_collectors/s3.py +105 -0
  100. src/snapshot/resource_collectors/secretsmanager.py +70 -0
  101. src/snapshot/resource_collectors/sns.py +68 -0
  102. src/snapshot/resource_collectors/sqs.py +82 -0
  103. src/snapshot/resource_collectors/ssm.py +160 -0
  104. src/snapshot/resource_collectors/stepfunctions.py +74 -0
  105. src/snapshot/resource_collectors/vpcendpoints.py +79 -0
  106. src/snapshot/resource_collectors/waf.py +159 -0
  107. src/snapshot/storage.py +351 -0
  108. src/storage/__init__.py +21 -0
  109. src/storage/audit_store.py +419 -0
  110. src/storage/database.py +294 -0
  111. src/storage/group_store.py +763 -0
  112. src/storage/inventory_store.py +320 -0
  113. src/storage/resource_store.py +416 -0
  114. src/storage/schema.py +339 -0
  115. src/storage/snapshot_store.py +363 -0
  116. src/utils/__init__.py +12 -0
  117. src/utils/export.py +305 -0
  118. src/utils/hash.py +60 -0
  119. src/utils/logging.py +63 -0
  120. src/utils/pagination.py +41 -0
  121. src/utils/paths.py +51 -0
  122. src/utils/progress.py +41 -0
  123. src/utils/unsupported_resources.py +306 -0
  124. src/web/__init__.py +5 -0
  125. src/web/app.py +97 -0
  126. src/web/dependencies.py +69 -0
  127. src/web/routes/__init__.py +1 -0
  128. src/web/routes/api/__init__.py +18 -0
  129. src/web/routes/api/charts.py +156 -0
  130. src/web/routes/api/cleanup.py +186 -0
  131. src/web/routes/api/filters.py +253 -0
  132. src/web/routes/api/groups.py +305 -0
  133. src/web/routes/api/inventories.py +80 -0
  134. src/web/routes/api/queries.py +202 -0
  135. src/web/routes/api/resources.py +393 -0
  136. src/web/routes/api/snapshots.py +314 -0
  137. src/web/routes/api/views.py +260 -0
  138. src/web/routes/pages.py +198 -0
  139. src/web/services/__init__.py +1 -0
  140. src/web/templates/base.html +955 -0
  141. src/web/templates/components/navbar.html +31 -0
  142. src/web/templates/components/sidebar.html +104 -0
  143. src/web/templates/pages/audit_logs.html +86 -0
  144. src/web/templates/pages/cleanup.html +279 -0
  145. src/web/templates/pages/dashboard.html +227 -0
  146. src/web/templates/pages/diff.html +175 -0
  147. src/web/templates/pages/error.html +30 -0
  148. src/web/templates/pages/groups.html +721 -0
  149. src/web/templates/pages/queries.html +246 -0
  150. src/web/templates/pages/resources.html +2429 -0
  151. src/web/templates/pages/snapshot_detail.html +271 -0
  152. src/web/templates/pages/snapshots.html +429 -0
@@ -0,0 +1,429 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Snapshots - AWS Inventory Browser{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="space-y-6" x-data="snapshotsPage()" x-init="loadInventories()">
7
+ <div class="md:flex md:items-center md:justify-between">
8
+ <div class="min-w-0 flex-1">
9
+ <h1 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl">Snapshots</h1>
10
+ </div>
11
+ <div class="mt-4 flex md:ml-4 md:mt-0">
12
+ <button @click="showCreateModal = true"
13
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-lg shadow-sm text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all">
14
+ <svg class="-ml-1 mr-2 h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
15
+ <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4"/>
16
+ </svg>
17
+ Create Snapshot
18
+ </button>
19
+ </div>
20
+ </div>
21
+
22
+ <!-- Snapshots Grid -->
23
+ <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
24
+ {% for snapshot in snapshots %}
25
+ <div class="relative bg-white rounded-lg shadow hover:shadow-md transition-shadow">
26
+ <div class="p-5">
27
+ <div class="flex items-center justify-between">
28
+ <h3 class="text-lg font-medium text-gray-900 truncate">
29
+ <a href="/snapshots/{{ snapshot.name }}" class="hover:text-blue-600">
30
+ {{ snapshot.name }}
31
+ </a>
32
+ </h3>
33
+ {% if snapshot.name == active_snapshot %}
34
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
35
+ Active
36
+ </span>
37
+ {% endif %}
38
+ </div>
39
+
40
+ <div class="mt-4 space-y-2">
41
+ <div class="flex items-center text-sm text-gray-500">
42
+ <svg class="flex-shrink-0 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
43
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
44
+ </svg>
45
+ {{ snapshot.resource_count | format_number }} resources
46
+ </div>
47
+ <div class="flex items-center text-sm text-gray-500">
48
+ <svg class="flex-shrink-0 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
50
+ </svg>
51
+ {{ snapshot.created_at }}
52
+ </div>
53
+ <div class="flex items-center text-sm text-gray-500">
54
+ <svg class="flex-shrink-0 mr-2 h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
55
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"/>
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"/>
57
+ </svg>
58
+ {{ snapshot.regions | join(', ') if snapshot.regions else 'N/A' }}
59
+ </div>
60
+ </div>
61
+
62
+ <div class="mt-4 flex space-x-2">
63
+ <a href="/snapshots/{{ snapshot.name }}"
64
+ class="flex-1 text-center px-3 py-2 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50">
65
+ View
66
+ </a>
67
+ {% if snapshot.name != active_snapshot %}
68
+ <button hx-post="/api/snapshots/{{ snapshot.name }}/activate"
69
+ hx-swap="none"
70
+ hx-on::after-request="window.location.reload()"
71
+ class="flex-1 text-center px-3 py-2 border border-transparent rounded-md text-sm font-medium text-white bg-blue-600 hover:bg-blue-700">
72
+ Set Active
73
+ </button>
74
+ {% endif %}
75
+ </div>
76
+
77
+ <!-- Action buttons -->
78
+ <div class="mt-3 pt-3 border-t border-gray-100 flex space-x-2">
79
+ <button @click="openRenameModal('{{ snapshot.name }}')"
80
+ class="flex-1 text-center px-3 py-1.5 text-xs font-medium text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded transition-colors">
81
+ <svg class="inline w-3.5 h-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
82
+ <path stroke-linecap="round" stroke-linejoin="round" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
83
+ </svg>
84
+ Rename
85
+ </button>
86
+ {% if snapshot.name != active_snapshot %}
87
+ <button @click="confirmDelete('{{ snapshot.name }}')"
88
+ class="flex-1 text-center px-3 py-1.5 text-xs font-medium text-red-600 hover:text-red-800 hover:bg-red-50 rounded transition-colors">
89
+ <svg class="inline w-3.5 h-3.5 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
90
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
91
+ </svg>
92
+ Delete
93
+ </button>
94
+ {% endif %}
95
+ </div>
96
+ </div>
97
+ </div>
98
+ {% else %}
99
+ <div class="col-span-full text-center py-12">
100
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
101
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z"/>
102
+ </svg>
103
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No snapshots</h3>
104
+ <p class="mt-1 text-sm text-gray-500">Create your first snapshot with the CLI:</p>
105
+ <code class="mt-2 inline-block bg-gray-100 px-4 py-2 rounded text-sm">
106
+ awsinv snapshot create my-baseline --regions us-east-1
107
+ </code>
108
+ </div>
109
+ {% endfor %}
110
+ </div>
111
+
112
+ <!-- Create Snapshot Modal -->
113
+ <div x-show="showCreateModal" class="fixed z-50 inset-0 overflow-y-auto" x-cloak>
114
+ <div class="flex items-center justify-center min-h-screen px-4">
115
+ <div class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity" @click="showCreateModal = false"></div>
116
+
117
+ <div class="relative bg-white rounded-xl shadow-2xl max-w-lg w-full overflow-hidden"
118
+ x-transition:enter="transition ease-out duration-300"
119
+ x-transition:enter-start="opacity-0 scale-95"
120
+ x-transition:enter-end="opacity-100 scale-100">
121
+ <div class="bg-gradient-to-r from-blue-600 to-indigo-600 px-6 py-5">
122
+ <h3 class="text-xl font-bold text-white">Create New Snapshot</h3>
123
+ <p class="text-blue-100 text-sm mt-1">Capture current state of AWS resources</p>
124
+ </div>
125
+
126
+ <form @submit.prevent="createSnapshot" class="p-6 space-y-5">
127
+ <div>
128
+ <label class="block text-sm font-semibold text-gray-700 mb-2">Snapshot Name</label>
129
+ <input type="text" x-model="newSnapshot.name"
130
+ class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
131
+ placeholder="Auto-generated if empty">
132
+ <p class="text-xs text-gray-500 mt-1">Leave empty to auto-generate (snapshot-YYYYMMDD-HHMMSS)</p>
133
+ </div>
134
+
135
+ <div>
136
+ <label class="block text-sm font-semibold text-gray-700 mb-2">Inventory</label>
137
+ <select x-model="newSnapshot.inventory"
138
+ class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
139
+ <option value="">-- No inventory (collect all) --</option>
140
+ <template x-for="inv in inventories" :key="inv.name">
141
+ <option :value="inv.name" x-text="inv.name + (inv.description ? ' - ' + inv.description : '')"></option>
142
+ </template>
143
+ </select>
144
+ <p class="text-xs text-gray-500 mt-1">Use inventory filters (include/exclude tags)</p>
145
+ </div>
146
+
147
+ <div>
148
+ <label class="block text-sm font-semibold text-gray-700 mb-2">Regions</label>
149
+ <input type="text" x-model="newSnapshot.regions"
150
+ class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
151
+ placeholder="us-east-1">
152
+ <p class="text-xs text-gray-500 mt-1">Comma-separated list (default: us-east-1)</p>
153
+ </div>
154
+
155
+ <div class="flex items-center space-x-4">
156
+ <label class="flex items-center">
157
+ <input type="checkbox" x-model="newSnapshot.setActive"
158
+ class="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
159
+ <span class="ml-2 text-sm text-gray-700">Set as active snapshot</span>
160
+ </label>
161
+ <label class="flex items-center">
162
+ <input type="checkbox" x-model="newSnapshot.useConfig"
163
+ class="h-4 w-4 text-blue-600 rounded border-gray-300 focus:ring-blue-500">
164
+ <span class="ml-2 text-sm text-gray-700">Use AWS Config</span>
165
+ </label>
166
+ </div>
167
+
168
+ <!-- Job Status -->
169
+ <div x-show="createJobId" class="p-4 bg-blue-50 rounded-lg border border-blue-200">
170
+ <div class="flex items-center justify-between mb-2">
171
+ <span class="text-sm font-medium text-blue-700">Status:</span>
172
+ <span class="text-sm font-semibold"
173
+ :class="{
174
+ 'text-yellow-600': createJobStatus === 'pending' || createJobStatus === 'running',
175
+ 'text-green-600': createJobStatus === 'completed',
176
+ 'text-red-600': createJobStatus === 'failed'
177
+ }"
178
+ x-text="createJobStatus"></span>
179
+ </div>
180
+ <p class="text-sm text-gray-600" x-text="createJobMessage"></p>
181
+ <div x-show="createJobProgress > 0" class="mt-2">
182
+ <div class="w-full bg-blue-100 rounded-full h-2">
183
+ <div class="bg-blue-600 h-2 rounded-full transition-all duration-300"
184
+ :style="'width: ' + createJobProgress + '%'"></div>
185
+ </div>
186
+ </div>
187
+ </div>
188
+
189
+ <div class="flex gap-3 pt-2">
190
+ <button type="button" @click="showCreateModal = false; resetCreateForm()"
191
+ class="flex-1 px-4 py-2.5 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors"
192
+ :disabled="creating">
193
+ <span x-text="createJobStatus === 'completed' ? 'Close' : 'Cancel'"></span>
194
+ </button>
195
+ <button type="submit" class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 rounded-lg transition-colors"
196
+ :disabled="creating || createJobId"
197
+ x-show="!createJobId">
198
+ <span x-show="!creating">Create Snapshot</span>
199
+ <span x-show="creating" class="flex items-center justify-center">
200
+ <svg class="animate-spin h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24">
201
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
202
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
203
+ </svg>
204
+ Starting...
205
+ </span>
206
+ </button>
207
+ <button type="button" @click="window.location.reload()"
208
+ class="flex-1 px-4 py-2.5 text-sm font-medium text-white bg-green-600 hover:bg-green-700 rounded-lg transition-colors"
209
+ x-show="createJobStatus === 'completed'">
210
+ View Snapshot
211
+ </button>
212
+ </div>
213
+ </form>
214
+ </div>
215
+ </div>
216
+ </div>
217
+
218
+ <!-- Rename Modal -->
219
+ <div x-show="showRenameModal" class="fixed z-50 inset-0 overflow-y-auto" x-cloak>
220
+ <div class="flex items-center justify-center min-h-screen px-4">
221
+ <div class="fixed inset-0 bg-gray-900/60 backdrop-blur-sm transition-opacity" @click="showRenameModal = false"></div>
222
+
223
+ <div class="relative bg-white rounded-xl shadow-2xl max-w-md w-full overflow-hidden"
224
+ x-transition:enter="transition ease-out duration-300"
225
+ x-transition:enter-start="opacity-0 scale-95"
226
+ x-transition:enter-end="opacity-100 scale-100">
227
+ <div class="bg-gradient-to-r from-blue-500 to-indigo-600 px-6 py-4">
228
+ <h3 class="text-lg font-bold text-white">Rename Snapshot</h3>
229
+ </div>
230
+
231
+ <form @submit.prevent="renameSnapshot" class="p-6 space-y-4">
232
+ <div>
233
+ <label class="block text-sm font-medium text-gray-700 mb-1">Current Name</label>
234
+ <input type="text" :value="renameOldName" disabled
235
+ class="w-full bg-gray-100 border-gray-300 rounded-lg text-gray-500">
236
+ </div>
237
+ <div>
238
+ <label class="block text-sm font-medium text-gray-700 mb-1">New Name</label>
239
+ <input type="text" x-model="renameNewName" required
240
+ class="w-full border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500"
241
+ placeholder="Enter new name">
242
+ </div>
243
+ <div class="flex gap-3 pt-2">
244
+ <button type="button" @click="showRenameModal = false"
245
+ class="flex-1 px-4 py-2 text-sm font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-lg transition-colors">
246
+ Cancel
247
+ </button>
248
+ <button type="submit" class="flex-1 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
249
+ :disabled="renaming">
250
+ <span x-show="!renaming">Rename</span>
251
+ <span x-show="renaming">Renaming...</span>
252
+ </button>
253
+ </div>
254
+ </form>
255
+ </div>
256
+ </div>
257
+ </div>
258
+ </div>
259
+ {% endblock %}
260
+
261
+ {% block scripts %}
262
+ <script>
263
+ function snapshotsPage() {
264
+ return {
265
+ // Rename modal
266
+ showRenameModal: false,
267
+ renameOldName: '',
268
+ renameNewName: '',
269
+ renaming: false,
270
+
271
+ // Create modal
272
+ showCreateModal: false,
273
+ creating: false,
274
+ inventories: [],
275
+ newSnapshot: {
276
+ name: '',
277
+ inventory: '',
278
+ regions: 'us-east-1',
279
+ setActive: true,
280
+ useConfig: true
281
+ },
282
+ createJobId: null,
283
+ createJobStatus: null,
284
+ createJobMessage: '',
285
+ createJobProgress: 0,
286
+ pollInterval: null,
287
+
288
+ async loadInventories() {
289
+ try {
290
+ const response = await fetch('/api/inventories');
291
+ if (response.ok) {
292
+ const data = await response.json();
293
+ this.inventories = data.inventories || [];
294
+ }
295
+ } catch (error) {
296
+ console.error('Failed to load inventories:', error);
297
+ }
298
+ },
299
+
300
+ resetCreateForm() {
301
+ this.newSnapshot = {
302
+ name: '',
303
+ inventory: '',
304
+ regions: 'us-east-1',
305
+ setActive: true,
306
+ useConfig: true
307
+ };
308
+ this.createJobId = null;
309
+ this.createJobStatus = null;
310
+ this.createJobMessage = '';
311
+ this.createJobProgress = 0;
312
+ if (this.pollInterval) {
313
+ clearInterval(this.pollInterval);
314
+ this.pollInterval = null;
315
+ }
316
+ },
317
+
318
+ async createSnapshot() {
319
+ this.creating = true;
320
+ try {
321
+ const regions = this.newSnapshot.regions
322
+ ? this.newSnapshot.regions.split(',').map(r => r.trim()).filter(r => r)
323
+ : ['us-east-1'];
324
+
325
+ const response = await fetch('/api/snapshots', {
326
+ method: 'POST',
327
+ headers: { 'Content-Type': 'application/json' },
328
+ body: JSON.stringify({
329
+ name: this.newSnapshot.name || null,
330
+ inventory: this.newSnapshot.inventory || null,
331
+ regions: regions,
332
+ set_active: this.newSnapshot.setActive,
333
+ use_config: this.newSnapshot.useConfig
334
+ })
335
+ });
336
+
337
+ if (!response.ok) {
338
+ const error = await response.json();
339
+ throw new Error(error.detail || 'Failed to create snapshot');
340
+ }
341
+
342
+ const result = await response.json();
343
+ this.createJobId = result.job_id;
344
+ this.createJobStatus = 'pending';
345
+ this.createJobMessage = result.message;
346
+
347
+ // Start polling for status
348
+ this.pollJobStatus();
349
+
350
+ } catch (error) {
351
+ alert('Error: ' + error.message);
352
+ } finally {
353
+ this.creating = false;
354
+ }
355
+ },
356
+
357
+ pollJobStatus() {
358
+ this.pollInterval = setInterval(async () => {
359
+ try {
360
+ const response = await fetch('/api/snapshots/jobs/' + this.createJobId);
361
+ if (response.ok) {
362
+ const status = await response.json();
363
+ this.createJobStatus = status.status;
364
+ this.createJobMessage = status.message;
365
+ this.createJobProgress = status.progress || 0;
366
+
367
+ if (status.status === 'completed' || status.status === 'failed') {
368
+ clearInterval(this.pollInterval);
369
+ this.pollInterval = null;
370
+ }
371
+ }
372
+ } catch (error) {
373
+ console.error('Failed to poll job status:', error);
374
+ }
375
+ }, 2000);
376
+ },
377
+
378
+ openRenameModal(name) {
379
+ this.renameOldName = name;
380
+ this.renameNewName = name;
381
+ this.showRenameModal = true;
382
+ },
383
+
384
+ async renameSnapshot() {
385
+ if (!this.renameNewName || this.renameNewName === this.renameOldName) return;
386
+
387
+ this.renaming = true;
388
+ try {
389
+ const response = await fetch('/api/snapshots/' + encodeURIComponent(this.renameOldName) + '/rename', {
390
+ method: 'POST',
391
+ headers: { 'Content-Type': 'application/json' },
392
+ body: JSON.stringify({ new_name: this.renameNewName })
393
+ });
394
+
395
+ if (!response.ok) {
396
+ const error = await response.json();
397
+ throw new Error(error.detail || 'Failed to rename snapshot');
398
+ }
399
+
400
+ window.location.reload();
401
+ } catch (error) {
402
+ alert('Error: ' + error.message);
403
+ } finally {
404
+ this.renaming = false;
405
+ }
406
+ },
407
+
408
+ async confirmDelete(name) {
409
+ if (!confirm('Are you sure you want to delete snapshot "' + name + '"? This cannot be undone.')) return;
410
+
411
+ try {
412
+ const response = await fetch('/api/snapshots/' + encodeURIComponent(name), {
413
+ method: 'DELETE'
414
+ });
415
+
416
+ if (!response.ok) {
417
+ const error = await response.json();
418
+ throw new Error(error.detail || 'Failed to delete snapshot');
419
+ }
420
+
421
+ window.location.reload();
422
+ } catch (error) {
423
+ alert('Error: ' + error.message);
424
+ }
425
+ }
426
+ };
427
+ }
428
+ </script>
429
+ {% endblock %}