supervaizer 0.10.5__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 (76) hide show
  1. supervaizer/__init__.py +97 -0
  2. supervaizer/__version__.py +10 -0
  3. supervaizer/account.py +308 -0
  4. supervaizer/account_service.py +93 -0
  5. supervaizer/admin/routes.py +1293 -0
  6. supervaizer/admin/static/js/job-start-form.js +373 -0
  7. supervaizer/admin/templates/agent_detail.html +145 -0
  8. supervaizer/admin/templates/agents.html +249 -0
  9. supervaizer/admin/templates/agents_grid.html +82 -0
  10. supervaizer/admin/templates/base.html +233 -0
  11. supervaizer/admin/templates/case_detail.html +230 -0
  12. supervaizer/admin/templates/cases_list.html +182 -0
  13. supervaizer/admin/templates/cases_table.html +134 -0
  14. supervaizer/admin/templates/console.html +389 -0
  15. supervaizer/admin/templates/dashboard.html +153 -0
  16. supervaizer/admin/templates/job_detail.html +192 -0
  17. supervaizer/admin/templates/job_start_test.html +109 -0
  18. supervaizer/admin/templates/jobs_list.html +180 -0
  19. supervaizer/admin/templates/jobs_table.html +122 -0
  20. supervaizer/admin/templates/navigation.html +163 -0
  21. supervaizer/admin/templates/recent_activity.html +81 -0
  22. supervaizer/admin/templates/server.html +105 -0
  23. supervaizer/admin/templates/server_status_cards.html +121 -0
  24. supervaizer/admin/templates/supervaize_instructions.html +212 -0
  25. supervaizer/agent.py +956 -0
  26. supervaizer/case.py +432 -0
  27. supervaizer/cli.py +395 -0
  28. supervaizer/common.py +324 -0
  29. supervaizer/deploy/__init__.py +16 -0
  30. supervaizer/deploy/cli.py +305 -0
  31. supervaizer/deploy/commands/__init__.py +9 -0
  32. supervaizer/deploy/commands/clean.py +294 -0
  33. supervaizer/deploy/commands/down.py +119 -0
  34. supervaizer/deploy/commands/local.py +460 -0
  35. supervaizer/deploy/commands/plan.py +167 -0
  36. supervaizer/deploy/commands/status.py +169 -0
  37. supervaizer/deploy/commands/up.py +281 -0
  38. supervaizer/deploy/docker.py +377 -0
  39. supervaizer/deploy/driver_factory.py +42 -0
  40. supervaizer/deploy/drivers/__init__.py +39 -0
  41. supervaizer/deploy/drivers/aws_app_runner.py +607 -0
  42. supervaizer/deploy/drivers/base.py +196 -0
  43. supervaizer/deploy/drivers/cloud_run.py +570 -0
  44. supervaizer/deploy/drivers/do_app_platform.py +504 -0
  45. supervaizer/deploy/health.py +404 -0
  46. supervaizer/deploy/state.py +210 -0
  47. supervaizer/deploy/templates/Dockerfile.template +44 -0
  48. supervaizer/deploy/templates/debug_env.py +69 -0
  49. supervaizer/deploy/templates/docker-compose.yml.template +37 -0
  50. supervaizer/deploy/templates/dockerignore.template +66 -0
  51. supervaizer/deploy/templates/entrypoint.sh +20 -0
  52. supervaizer/deploy/utils.py +52 -0
  53. supervaizer/event.py +181 -0
  54. supervaizer/examples/controller_template.py +196 -0
  55. supervaizer/instructions.py +145 -0
  56. supervaizer/job.py +392 -0
  57. supervaizer/job_service.py +156 -0
  58. supervaizer/lifecycle.py +417 -0
  59. supervaizer/parameter.py +233 -0
  60. supervaizer/protocol/__init__.py +11 -0
  61. supervaizer/protocol/a2a/__init__.py +21 -0
  62. supervaizer/protocol/a2a/model.py +227 -0
  63. supervaizer/protocol/a2a/routes.py +99 -0
  64. supervaizer/py.typed +1 -0
  65. supervaizer/routes.py +917 -0
  66. supervaizer/server.py +553 -0
  67. supervaizer/server_utils.py +54 -0
  68. supervaizer/storage.py +462 -0
  69. supervaizer/telemetry.py +81 -0
  70. supervaizer/utils/__init__.py +16 -0
  71. supervaizer/utils/version_check.py +56 -0
  72. supervaizer-0.10.5.dist-info/METADATA +317 -0
  73. supervaizer-0.10.5.dist-info/RECORD +76 -0
  74. supervaizer-0.10.5.dist-info/WHEEL +4 -0
  75. supervaizer-0.10.5.dist-info/entry_points.txt +2 -0
  76. supervaizer-0.10.5.dist-info/licenses/LICENSE.md +346 -0
@@ -0,0 +1,230 @@
1
+ <div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4 max-h-[80vh] overflow-y-auto">
2
+ <div class="sm:flex sm:items-start">
3
+ <div class="w-full">
4
+ <!-- Header -->
5
+ <div class="flex items-center justify-between mb-4">
6
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Case Details</h3>
7
+ <button
8
+ @click="open = false"
9
+ class="bg-white rounded-md text-gray-400 hover:text-gray-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
10
+ >
11
+ <span class="sr-only">Close</span>
12
+ <svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
14
+ </svg>
15
+ </button>
16
+ </div>
17
+
18
+ <!-- Case Information -->
19
+ <div class="grid grid-cols-1 gap-6 sm:grid-cols-2">
20
+ <div>
21
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Basic Information</h4>
22
+ <dl class="space-y-2">
23
+ <div>
24
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Name</dt>
25
+ <dd class="text-sm text-gray-900">{{ case.name or "Unnamed Case" }}</dd>
26
+ </div>
27
+ <div>
28
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">ID</dt>
29
+ <dd class="text-sm text-gray-900 font-mono">{{ case.id }}</dd>
30
+ </div>
31
+ <div>
32
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Status</dt>
33
+ <dd>
34
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
35
+ {% if case.status == 'completed' %}bg-green-100 text-green-800
36
+ {% elif case.status == 'in_progress' %}bg-blue-100 text-blue-800
37
+ {% elif case.status == 'failed' %}bg-red-100 text-red-800
38
+ {% elif case.status == 'cancelled' %}bg-gray-100 text-gray-800
39
+ {% else %}bg-yellow-100 text-yellow-800{% endif %}">
40
+ {{ case.status.replace('_', ' ').title() }}
41
+ </span>
42
+ </dd>
43
+ </div>
44
+ <div>
45
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Total Cost</dt>
46
+ <dd class="text-sm text-gray-900">${{ "%.2f"|format(case.total_cost or 0) }}</dd>
47
+ </div>
48
+ {% if case.agent_name %}
49
+ <div>
50
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</dt>
51
+ <dd>
52
+ <a href="/admin/agents" class="text-sm text-blue-600 hover:text-blue-900">{{ case.agent_name }}</a>
53
+ </dd>
54
+ </div>
55
+ {% endif %}
56
+ </dl>
57
+ </div>
58
+
59
+ <div>
60
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Timestamps</h4>
61
+ <dl class="space-y-2">
62
+ <div>
63
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Created</dt>
64
+ <dd class="text-sm text-gray-900">{{ case.created_at or "-" }}</dd>
65
+ </div>
66
+ <div>
67
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Finished</dt>
68
+ <dd class="text-sm text-gray-900">{{ case.finished_at or "-" }}</dd>
69
+ </div>
70
+ {% if case.job_id %}
71
+ <div>
72
+ <dt class="text-xs font-medium text-gray-500 uppercase tracking-wider">Parent Job</dt>
73
+ <dd>
74
+ <a href="/admin/jobs" class="text-sm text-blue-600 hover:text-blue-900 font-mono">{{ case.job_id }}</a>
75
+ </dd>
76
+ </div>
77
+ {% endif %}
78
+ </dl>
79
+ </div>
80
+ </div>
81
+
82
+ <!-- Description -->
83
+ {% if case.description %}
84
+ <div class="mt-6">
85
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Description</h4>
86
+ <div class="bg-gray-50 p-4 rounded-lg">
87
+ <p class="text-sm text-gray-700">{{ case.description }}</p>
88
+ </div>
89
+ </div>
90
+ {% endif %}
91
+
92
+ <!-- Parent Job Information -->
93
+ {% if job %}
94
+ <div class="mt-6">
95
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Parent Job Information</h4>
96
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
97
+ <div class="flex items-center justify-between">
98
+ <div>
99
+ <p class="text-sm font-medium text-blue-900">{{ job.name or "Unnamed Job" }}</p>
100
+ {% if job.agent_name %}
101
+ <p class="text-sm text-blue-700">
102
+ Agent: <a href="/admin/agents" class="text-blue-600 hover:text-blue-800 underline">{{ job.agent_name }}</a>
103
+ </p>
104
+ {% endif %}
105
+ </div>
106
+ <div class="flex items-center space-x-3">
107
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
108
+ {% if job.status == 'completed' %}bg-green-100 text-green-800
109
+ {% elif job.status == 'in_progress' %}bg-blue-100 text-blue-800
110
+ {% elif job.status == 'failed' %}bg-red-100 text-red-800
111
+ {% elif job.status == 'cancelled' %}bg-gray-100 text-gray-800
112
+ {% else %}bg-yellow-100 text-yellow-800{% endif %}">
113
+ {{ job.status.replace('_', ' ').title() }}
114
+ </span>
115
+ <a href="/admin/jobs" class="text-blue-600 hover:text-blue-900 text-sm font-medium underline">View Job</a>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ {% endif %}
121
+
122
+
123
+
124
+ <!-- Case Updates -->
125
+ {% if case.updates %}
126
+ <div class="mt-6">
127
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Case Updates ({{ case.updates | length }} total, showing recent 10)</h4>
128
+ <div class="bg-white overflow-hidden shadow rounded-lg">
129
+ <div class="divide-y divide-gray-200 max-h-96 overflow-y-auto">
130
+ {% for update in case.updates[-10:] %}
131
+ <div class="px-4 py-4">
132
+ <div class="flex items-start space-x-3">
133
+ <!-- Update Type Icon -->
134
+ <div class="flex-shrink-0">
135
+ {% if update.type == 'status_change' %}
136
+ <div class="w-2 h-2 bg-blue-400 rounded-full mt-2"></div>
137
+ {% elif update.type == 'error' %}
138
+ <div class="w-2 h-2 bg-red-400 rounded-full mt-2"></div>
139
+ {% elif update.type == 'progress' %}
140
+ <div class="w-2 h-2 bg-green-400 rounded-full mt-2"></div>
141
+ {% else %}
142
+ <div class="w-2 h-2 bg-gray-400 rounded-full mt-2"></div>
143
+ {% endif %}
144
+ </div>
145
+
146
+ <div class="flex-1 min-w-0">
147
+ <!-- Update Header -->
148
+ <div class="flex items-center justify-between">
149
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
150
+ {% if update.type == 'status_change' %}bg-blue-100 text-blue-800
151
+ {% elif update.type == 'error' %}bg-red-100 text-red-800
152
+ {% elif update.type == 'progress' %}bg-green-100 text-green-800
153
+ {% else %}bg-gray-100 text-gray-800{% endif %}">
154
+ {{ update.type or "Update" }}
155
+ </span>
156
+ <time class="text-xs text-gray-500">
157
+ {{ update.timestamp or "Unknown time" }}
158
+ </time>
159
+ </div>
160
+
161
+ <!-- Update Message -->
162
+ <div class="mt-2">
163
+ <p class="text-sm text-gray-900">{{ update.message or "No message" }}</p>
164
+
165
+ <!-- Additional Details -->
166
+ {% if update.details %}
167
+ <div class="mt-2 bg-gray-50 rounded-md p-3">
168
+ {% if update.details is string %}
169
+ <p class="text-xs text-gray-600">{{ update.details }}</p>
170
+ {% else %}
171
+ <pre class="text-xs text-gray-600 whitespace-pre-wrap">{{ update.details | tojson(indent=2) }}</pre>
172
+ {% endif %}
173
+ </div>
174
+ {% endif %}
175
+
176
+ <!-- Cost Information -->
177
+ {% if update.cost %}
178
+ <div class="mt-2 flex items-center text-xs text-gray-500">
179
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
180
+ <path d="M8.433 7.418c.155-.103.346-.196.567-.267v1.698a2.305 2.305 0 01-.567-.267C8.07 8.34 8 8.114 8 8c0-.114.07-.34.433-.582zM11 12.849v-1.698c.22.071.412.164.567.267.364.243.433.468.433.582 0 .114-.07.34-.433.582a2.305 2.305 0 01-.567.267z"></path>
181
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-13a1 1 0 10-2 0v.092a4.535 4.535 0 00-1.676.662C6.602 6.234 6 7.009 6 8c0 .99.602 1.765 1.324 2.246.48.32 1.054.545 1.676.662v1.941c-.391-.127-.68-.317-.843-.504a1 1 0 10-1.51 1.31c.562.649 1.413 1.076 2.353 1.253V15a1 1 0 102 0v-.092a4.535 4.535 0 001.676-.662C13.398 13.766 14 12.991 14 12c0-.99-.602-1.765-1.324-2.246A4.535 4.535 0 0011 9.092V7.151c.391.127.68.317.843.504a1 1 0 101.511-1.31c-.563-.649-1.413-1.076-2.354-1.253V5z" clip-rule="evenodd"></path>
182
+ </svg>
183
+ Cost: ${{ "%.4f"|format(update.cost) }}
184
+ </div>
185
+ {% endif %}
186
+
187
+ <!-- Agent Information -->
188
+ {% if update.agent %}
189
+ <div class="mt-2 flex items-center text-xs text-gray-500">
190
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
191
+ <path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z" clip-rule="evenodd"></path>
192
+ </svg>
193
+ Agent: {{ update.agent }}
194
+ </div>
195
+ {% endif %}
196
+ </div>
197
+ </div>
198
+ </div>
199
+ </div>
200
+ {% endfor %}
201
+ </div>
202
+ </div>
203
+ </div>
204
+ {% endif %}
205
+
206
+ <!-- Final Delivery -->
207
+ {% if case.final_delivery %}
208
+ <div class="mt-6">
209
+ <h4 class="text-sm font-medium text-gray-900 mb-3">Final Delivery</h4>
210
+ <div class="bg-green-50 border border-green-200 rounded-lg p-4">
211
+ <pre class="text-sm text-green-800 whitespace-pre-wrap">{{ case.final_delivery | tojson(indent=2) }}</pre>
212
+ </div>
213
+ </div>
214
+ {% endif %}
215
+ </div>
216
+ </div>
217
+ </div>
218
+
219
+ <!-- Modal Footer -->
220
+ <div class="bg-gray-50 px-4 py-3 sm:px-6 sm:flex sm:flex-row-reverse">
221
+ <button
222
+ @click="open = false"
223
+ type="button"
224
+ class="mt-3 w-full inline-flex justify-center rounded-md border border-gray-300 shadow-sm px-4 py-2 bg-white text-base font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:mt-0 sm:ml-3 sm:w-auto sm:text-sm"
225
+ >
226
+ Close
227
+ </button>
228
+
229
+
230
+ </div>
@@ -0,0 +1,182 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Cases - Supervaizer Admin{% endblock %}
4
+
5
+ {% block content %}
6
+ <div class="px-4 py-6 sm:px-0">
7
+ <!-- Header -->
8
+ <div class="md:flex md:items-center md:justify-between">
9
+ <div class="min-w-0 flex-1">
10
+ <h2 class="text-2xl font-bold leading-7 text-gray-900 sm:truncate sm:text-3xl sm:tracking-tight">
11
+ Cases
12
+ </h2>
13
+ <p class="mt-1 text-sm text-gray-500">Manage all cases in the system</p>
14
+ </div>
15
+ <div class="mt-4 flex md:mt-0">
16
+ <button
17
+ hx-get="/admin/api/cases"
18
+ hx-target="#cases-table-container"
19
+ hx-indicator="#refresh-indicator"
20
+ class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
21
+ >
22
+ <svg id="refresh-indicator" class="htmx-indicator -ml-1 mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
23
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
24
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
25
+ </svg>
26
+ Refresh
27
+ </button>
28
+ </div>
29
+ </div>
30
+
31
+ <!-- Filters -->
32
+ <div class="mt-6 bg-white shadow rounded-lg" x-data="{ filtersOpen: false }">
33
+ <div class="px-6 py-4 border-b border-gray-200">
34
+ <button
35
+ @click="filtersOpen = !filtersOpen"
36
+ class="flex items-center text-sm font-medium text-gray-700 hover:text-gray-900"
37
+ >
38
+ <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
39
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.414A1 1 0 013 6.707V4z"></path>
40
+ </svg>
41
+ Filters
42
+ <svg class="w-4 h-4 ml-2 transition-transform" :class="filtersOpen ? 'rotate-180' : 'rotate-0'" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
44
+ </svg>
45
+ </button>
46
+ </div>
47
+
48
+ <div x-show="filtersOpen" x-cloak class="px-6 py-4 border-b border-gray-200">
49
+ <form
50
+ hx-get="/admin/api/cases"
51
+ hx-target="#cases-table-container"
52
+ hx-trigger="change, submit"
53
+ class="grid grid-cols-1 gap-4 sm:grid-cols-4"
54
+ >
55
+ <!-- Status Filter -->
56
+ <div>
57
+ <label for="status" class="block text-sm font-medium text-gray-700">Status</label>
58
+ <select name="status" id="status" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
59
+ <option value="">All statuses</option>
60
+ <option value="stopped">Stopped</option>
61
+ <option value="in_progress">In Progress</option>
62
+ <option value="completed">Completed</option>
63
+ <option value="failed">Failed</option>
64
+ <option value="cancelled">Cancelled</option>
65
+ <option value="awaiting">Awaiting</option>
66
+ </select>
67
+ </div>
68
+
69
+ <!-- Job ID Filter -->
70
+ <div>
71
+ <label for="job_id" class="block text-sm font-medium text-gray-700">Job ID</label>
72
+ <input type="text" name="job_id" id="job_id" placeholder="Filter by job ID..." class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
73
+ </div>
74
+
75
+ <!-- Search -->
76
+ <div>
77
+ <label for="search" class="block text-sm font-medium text-gray-700">Search</label>
78
+ <input type="text" name="search" id="search" placeholder="Case name, ID, or description..." class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
79
+ </div>
80
+
81
+ <!-- Sort -->
82
+ <div>
83
+ <label for="sort" class="block text-sm font-medium text-gray-700">Sort by</label>
84
+ <select name="sort" id="sort" class="mt-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm">
85
+ <option value="-created_at">Newest first</option>
86
+ <option value="created_at">Oldest first</option>
87
+ <option value="name">Name A-Z</option>
88
+ <option value="-name">Name Z-A</option>
89
+ <option value="status">Status</option>
90
+ <option value="-total_cost">Cost (High to Low)</option>
91
+ <option value="total_cost">Cost (Low to High)</option>
92
+ </select>
93
+ </div>
94
+ </form>
95
+ </div>
96
+ </div>
97
+
98
+ <!-- Cases Table -->
99
+ <div id="cases-table-container" class="mt-6">
100
+ <div class="bg-white shadow overflow-hidden rounded-md">
101
+ <div class="px-4 py-5 sm:p-6">
102
+ <div class="text-center">
103
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
104
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
105
+ </svg>
106
+ <h3 class="mt-2 text-sm font-medium text-gray-900">Loading cases...</h3>
107
+ <p class="mt-1 text-sm text-gray-500">Please wait while we fetch the case data.</p>
108
+ </div>
109
+ </div>
110
+ </div>
111
+ </div>
112
+ </div>
113
+
114
+ <!-- Case Detail Modal -->
115
+ <div id="case-modal" class="fixed inset-0 z-50" x-data="{ open: false }" x-show="open" x-cloak>
116
+ <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
117
+ <div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" @click="open = false"></div>
118
+
119
+ <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
120
+ <div id="case-modal-content">
121
+ <!-- Content will be loaded here via HTMX -->
122
+ </div>
123
+ </div>
124
+ </div>
125
+ </div>
126
+
127
+ <!-- Job Detail Modal -->
128
+ <div id="job-modal" class="fixed inset-0 z-50" x-data="{ open: false }" x-show="open" x-cloak>
129
+ <div class="flex items-end justify-center min-h-screen pt-4 px-4 pb-20 text-center sm:block sm:p-0">
130
+ <div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75" @click="open = false"></div>
131
+
132
+ <div class="inline-block align-bottom bg-white rounded-lg text-left overflow-hidden shadow-xl transform transition-all sm:my-8 sm:align-middle sm:max-w-4xl sm:w-full">
133
+ <div id="job-modal-content">
134
+ <!-- Content will be loaded here via HTMX -->
135
+ </div>
136
+ </div>
137
+ </div>
138
+ </div>
139
+
140
+ <script>
141
+ // Auto-load cases on page load
142
+ document.addEventListener('DOMContentLoaded', function() {
143
+ // Load cases immediately
144
+ htmx.ajax('GET', '/admin/api/cases', {target: '#cases-table-container'});
145
+
146
+ // Set up auto-refresh interval for cases every 30 seconds
147
+ setInterval(function() {
148
+ htmx.ajax('GET', '/admin/api/cases', {target: '#cases-table-container'});
149
+ }, 30000); // Refresh every 30 seconds
150
+ });
151
+
152
+ // Handle case modal
153
+ document.body.addEventListener('htmx:afterRequest', function(e) {
154
+ if (e.detail.target.id === 'case-modal-content') {
155
+ // Show modal after content loads
156
+ const modal = document.getElementById('case-modal');
157
+ if (modal && modal._x_dataStack && modal._x_dataStack[0]) {
158
+ modal._x_dataStack[0].open = true;
159
+ }
160
+ }
161
+
162
+ // Handle job modal
163
+ if (e.detail.target.id === 'job-modal-content') {
164
+ // Show modal after content loads
165
+ const modal = document.getElementById('job-modal');
166
+ if (modal && modal._x_dataStack && modal._x_dataStack[0]) {
167
+ modal._x_dataStack[0].open = true;
168
+ }
169
+ }
170
+ });
171
+
172
+ // Global function to show case details
173
+ window.showCaseDetails = function(caseId) {
174
+ htmx.ajax('GET', `/admin/api/cases/${caseId}`, {target: '#case-modal-content'});
175
+ };
176
+
177
+ // Global function to show job details
178
+ window.showJobDetails = function(jobId) {
179
+ htmx.ajax('GET', `/admin/api/jobs/${jobId}`, {target: '#job-modal-content'});
180
+ };
181
+ </script>
182
+ {% endblock %}
@@ -0,0 +1,134 @@
1
+ <div class="bg-white shadow overflow-hidden rounded-md">
2
+ <table class="min-w-full divide-y divide-gray-200">
3
+ <thead class="bg-gray-50">
4
+ <tr>
5
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
6
+ Case
7
+ </th>
8
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
9
+ Description
10
+ </th>
11
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
12
+ Status
13
+ </th>
14
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
15
+ Job ID
16
+ </th>
17
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
18
+ Cost
19
+ </th>
20
+ <th scope="col" class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
21
+ Created
22
+ </th>
23
+ <th scope="col" class="relative px-6 py-3">
24
+ <span class="sr-only">Actions</span>
25
+ </th>
26
+ </tr>
27
+ </thead>
28
+ <tbody class="bg-white divide-y divide-gray-200">
29
+ {% for case in cases %}
30
+ <tr class="hover:bg-gray-50">
31
+ <td class="px-6 py-4 whitespace-nowrap">
32
+ <div>
33
+ <div class="text-sm font-medium text-gray-900">{{ case.name }}</div>
34
+ <div class="text-sm text-gray-500">{{ case.id }}</div>
35
+ </div>
36
+ </td>
37
+ <td class="px-6 py-4">
38
+ <div class="text-sm text-gray-900 max-w-xs truncate">{{ case.description or "-" }}</div>
39
+ </td>
40
+ <td class="px-6 py-4 whitespace-nowrap">
41
+ <span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
42
+ {% if case.status == 'completed' %}bg-green-100 text-green-800
43
+ {% elif case.status == 'in_progress' %}bg-blue-100 text-blue-800
44
+ {% elif case.status == 'failed' %}bg-red-100 text-red-800
45
+ {% elif case.status == 'cancelled' %}bg-gray-100 text-gray-800
46
+ {% else %}bg-yellow-100 text-yellow-800{% endif %}">
47
+ {{ case.status.replace('_', ' ').title() }}
48
+ </span>
49
+ </td>
50
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
51
+ {% if case.job_id %}
52
+ <a href="#" onclick="showJobDetails('{{ case.job_id }}')" class="text-blue-600 hover:text-blue-900">
53
+ {{ case.job_id[:8] }}...
54
+ </a>
55
+ {% else %}
56
+ -
57
+ {% endif %}
58
+ </td>
59
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
60
+ ${{ "%.2f"|format(case.total_cost) }}
61
+ </td>
62
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
63
+ {% if case.created_at %}
64
+ {{ case.created_at[:10] }}
65
+ {% else %}
66
+ -
67
+ {% endif %}
68
+ </td>
69
+ <td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
70
+ <button
71
+ onclick="showCaseDetails('{{ case.id }}')"
72
+ class="text-blue-600 hover:text-blue-900 mr-4"
73
+ >
74
+ View
75
+ </button>
76
+ <button
77
+ hx-delete="/admin/api/cases/{{ case.id }}"
78
+ hx-confirm="Are you sure you want to delete this case?"
79
+ hx-trigger="click"
80
+ hx-on::after-request="if(event.detail.successful) { window.showToast('Case deleted successfully', 'success'); htmx.trigger('#cases-table-container', 'refresh'); }"
81
+ class="text-red-600 hover:text-red-900"
82
+ >
83
+ Delete
84
+ </button>
85
+ </td>
86
+ </tr>
87
+ {% else %}
88
+ <tr>
89
+ <td colspan="7" class="px-6 py-12 text-center">
90
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
91
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
92
+ </svg>
93
+ <h3 class="mt-2 text-sm font-medium text-gray-900">No cases found</h3>
94
+ <p class="mt-1 text-sm text-gray-500">Try adjusting your filters or search criteria.</p>
95
+ </td>
96
+ </tr>
97
+ {% endfor %}
98
+ </tbody>
99
+ </table>
100
+
101
+ <!-- Pagination -->
102
+ {% if total > 0 %}
103
+ <div class="bg-white px-4 py-3 border-t border-gray-200 sm:px-6">
104
+ <div class="flex items-center justify-between">
105
+ <div class="flex items-center">
106
+ <p class="text-sm text-gray-700">
107
+ Showing {{ skip + 1 }} to {{ skip + cases|length }} of {{ total }} results
108
+ </p>
109
+ </div>
110
+ <div class="flex space-x-2">
111
+ {% if has_prev %}
112
+ <button
113
+ hx-get="/admin/api/cases?skip={{ skip - limit }}&limit={{ limit }}"
114
+ hx-target="#cases-table-container"
115
+ class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
116
+ >
117
+ Previous
118
+ </button>
119
+ {% endif %}
120
+
121
+ {% if has_next %}
122
+ <button
123
+ hx-get="/admin/api/cases?skip={{ skip + limit }}&limit={{ limit }}"
124
+ hx-target="#cases-table-container"
125
+ class="px-3 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50"
126
+ >
127
+ Next
128
+ </button>
129
+ {% endif %}
130
+ </div>
131
+ </div>
132
+ </div>
133
+ {% endif %}
134
+ </div>