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,389 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Console - 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
+ Console
12
+ </h2>
13
+ <p class="mt-1 text-sm text-gray-500">Interactive command interface and system logs</p>
14
+ </div>
15
+ <div class="mt-4 flex md:mt-0 space-x-3">
16
+ <button
17
+ onclick="clearConsole()"
18
+ 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"
19
+ >
20
+ Clear
21
+ </button>
22
+ <button
23
+ onclick="toggleAutoScroll()"
24
+ id="autoscroll-btn"
25
+ class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700"
26
+ >
27
+ Auto-scroll: ON
28
+ </button>
29
+ </div>
30
+ </div>
31
+
32
+ <!-- Console Interface -->
33
+ <div class="mt-6 space-y-6">
34
+ <!-- Command Input -->
35
+ <div class="bg-white shadow rounded-lg">
36
+ <div class="px-6 py-4 border-b border-gray-200">
37
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Command Interface</h3>
38
+ </div>
39
+ <div class="px-6 py-4">
40
+ <div class="flex space-x-4">
41
+ <input
42
+ type="text"
43
+ id="command-input"
44
+ placeholder="Enter command..."
45
+ class="flex-1 block w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500 sm:text-sm font-mono"
46
+ onkeypress="handleCommandKeypress(event)"
47
+ >
48
+ <button
49
+ onclick="executeCommand()"
50
+ class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700"
51
+ >
52
+ Execute
53
+ </button>
54
+ </div>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Live Log Stream -->
59
+ <div class="bg-white shadow rounded-lg">
60
+ <div class="px-6 py-4 border-b border-gray-200">
61
+ <h3 class="text-lg leading-6 font-medium text-gray-900">Live Server Logs</h3>
62
+ <div class="mt-1 flex items-center space-x-4 text-sm text-gray-500">
63
+ <span id="connection-status" class="flex items-center">
64
+ <span class="w-2 h-2 bg-gray-400 rounded-full mr-2"></span>
65
+ Connecting...
66
+ </span>
67
+ <span>Auto-scroll: <span id="autoscroll-status">ON</span></span>
68
+ </div>
69
+ </div>
70
+ <div class="px-6 py-4">
71
+ <div
72
+ id="console-output"
73
+ class="bg-gray-900 rounded-lg p-4 font-mono text-sm text-green-300 h-96 overflow-y-auto"
74
+ >
75
+ <div id="console-lines" class="space-y-1">
76
+ <div class="flex space-x-2">
77
+ <span class="text-gray-500">[--:--:--]</span>
78
+ <span class="text-blue-400 font-semibold">SYSTEM</span>
79
+ <span>Connecting to live log stream...</span>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+
88
+ <script>
89
+ let autoScroll = true;
90
+ let eventSource = null;
91
+ let reconnectTimeout = null;
92
+ let lineCount = 0;
93
+ let maxLines = 1000; // Limit console lines for performance
94
+
95
+ // Initialize console on page load
96
+ document.addEventListener('DOMContentLoaded', function() {
97
+ connectToLogStream();
98
+ });
99
+
100
+ function connectToLogStream() {
101
+ if (eventSource) {
102
+ eventSource.close();
103
+ }
104
+
105
+ updateConnectionStatus('connecting');
106
+
107
+ // Connect to the log stream endpoint with console token
108
+ const consoleToken = '{{ console_token }}';
109
+ const cacheBuster = Date.now();
110
+
111
+ // Better validation and error handling
112
+ if (!consoleToken || consoleToken === 'undefined' || consoleToken === '') {
113
+ addLogMessage('ERROR', 'No console token available. Please refresh the page.', 'red');
114
+ updateConnectionStatus('error');
115
+ return;
116
+ }
117
+
118
+ const streamUrl = `/admin/log-stream?token=${encodeURIComponent(consoleToken)}&_t=${cacheBuster}`;
119
+
120
+ try {
121
+ eventSource = new EventSource(streamUrl);
122
+
123
+ eventSource.onopen = function(event) {
124
+ updateConnectionStatus('connected');
125
+ addLogMessage('SYSTEM', 'Connected to live log stream', 'blue');
126
+ };
127
+
128
+ eventSource.onmessage = function(event) {
129
+ try {
130
+ // Handle different event data formats
131
+ let logData;
132
+ if (event.data.startsWith('data: ')) {
133
+ // Remove "data: " prefix if present
134
+ logData = JSON.parse(event.data.substring(6));
135
+ } else {
136
+ logData = JSON.parse(event.data);
137
+ }
138
+
139
+ if (logData && logData.level && logData.message) {
140
+ addLogMessage(logData.level, logData.message, getLevelColor(logData.level), logData.timestamp);
141
+ } else {
142
+ addLogMessage('WARNING', 'Received invalid log data format', 'yellow');
143
+ }
144
+ } catch (e) {
145
+ addLogMessage('ERROR', `Parse error: ${e.message}`, 'red');
146
+ }
147
+ };
148
+
149
+ eventSource.onerror = function(event) {
150
+ updateConnectionStatus('error');
151
+
152
+ // More specific error handling
153
+ if (eventSource.readyState === EventSource.CLOSED) {
154
+ addLogMessage('ERROR', 'Connection to log stream closed by server', 'red');
155
+ } else if (eventSource.readyState === EventSource.CONNECTING) {
156
+ addLogMessage('WARNING', 'Attempting to reconnect to log stream...', 'yellow');
157
+ } else {
158
+ addLogMessage('ERROR', 'Unknown log stream error occurred', 'red');
159
+ }
160
+
161
+ // Reconnect after 5 seconds
162
+ clearTimeout(reconnectTimeout);
163
+ reconnectTimeout = setTimeout(() => {
164
+ addLogMessage('INFO', 'Attempting to reconnect...', 'blue');
165
+ connectToLogStream();
166
+ }, 5000);
167
+ };
168
+ } catch (e) {
169
+ addLogMessage('ERROR', `Failed to create connection: ${e.message}`, 'red');
170
+ updateConnectionStatus('error');
171
+ }
172
+ }
173
+
174
+ function updateConnectionStatus(status) {
175
+ const statusElement = document.getElementById('connection-status');
176
+ if (!statusElement) return;
177
+
178
+ const statusIndicator = statusElement.querySelector('.w-2.h-2');
179
+ const statusText = statusElement.querySelector('span:last-child');
180
+
181
+ if (!statusIndicator || !statusText) return;
182
+
183
+ switch (status) {
184
+ case 'connecting':
185
+ statusIndicator.className = 'w-2 h-2 bg-yellow-400 rounded-full mr-2 animate-pulse';
186
+ statusText.textContent = 'Connecting...';
187
+ break;
188
+ case 'connected':
189
+ statusIndicator.className = 'w-2 h-2 bg-green-400 rounded-full mr-2';
190
+ statusText.textContent = 'Connected';
191
+ break;
192
+ case 'error':
193
+ statusIndicator.className = 'w-2 h-2 bg-red-400 rounded-full mr-2 animate-pulse';
194
+ statusText.textContent = 'Disconnected';
195
+ break;
196
+ }
197
+ }
198
+
199
+ function getLevelColor(level) {
200
+ const levelColors = {
201
+ 'TRACE': 'purple',
202
+ 'DEBUG': 'gray',
203
+ 'INFO': 'blue',
204
+ 'SUCCESS': 'green',
205
+ 'WARNING': 'yellow',
206
+ 'ERROR': 'red',
207
+ 'CRITICAL': 'red'
208
+ };
209
+ return levelColors[level] || 'green';
210
+ }
211
+
212
+ function addLogMessage(level, message, color = 'green', timestamp = null) {
213
+ const lines = document.getElementById('console-lines');
214
+ if (!lines) return;
215
+
216
+ // Parse timestamp or use current time
217
+ let timeStr;
218
+ if (timestamp) {
219
+ try {
220
+ const date = new Date(timestamp);
221
+ timeStr = date.toLocaleTimeString();
222
+ } catch (e) {
223
+ timeStr = new Date().toLocaleTimeString();
224
+ }
225
+ } else {
226
+ timeStr = new Date().toLocaleTimeString();
227
+ }
228
+
229
+ const colorMap = {
230
+ 'red': 'text-red-400',
231
+ 'green': 'text-green-300',
232
+ 'blue': 'text-blue-400',
233
+ 'yellow': 'text-yellow-400',
234
+ 'cyan': 'text-cyan-400',
235
+ 'purple': 'text-purple-400',
236
+ 'gray': 'text-gray-400'
237
+ };
238
+
239
+ const div = document.createElement('div');
240
+ div.className = 'flex space-x-2 text-xs leading-relaxed';
241
+ div.innerHTML = `
242
+ <span class="text-gray-500 flex-shrink-0">[${timeStr}]</span>
243
+ <span class="font-semibold min-w-[60px] flex-shrink-0 ${colorMap[color] || 'text-green-300'}">${level}</span>
244
+ <span class="${colorMap[color] || 'text-green-300'} break-words">${escapeHtml(message)}</span>
245
+ `;
246
+
247
+ lines.appendChild(div);
248
+ lineCount++;
249
+
250
+ // Limit lines for performance
251
+ if (lineCount > maxLines) {
252
+ const firstChild = lines.firstElementChild;
253
+ if (firstChild) {
254
+ lines.removeChild(firstChild);
255
+ lineCount--;
256
+ }
257
+ }
258
+
259
+ if (autoScroll) {
260
+ const output = document.getElementById('console-output');
261
+ if (output) {
262
+ output.scrollTop = output.scrollHeight;
263
+ }
264
+ }
265
+ }
266
+
267
+ function escapeHtml(text) {
268
+ const div = document.createElement('div');
269
+ div.textContent = text;
270
+ return div.innerHTML;
271
+ }
272
+
273
+ function handleCommandKeypress(event) {
274
+ if (event.key === 'Enter') {
275
+ executeCommand();
276
+ }
277
+ }
278
+
279
+ function executeCommand() {
280
+ const input = document.getElementById('command-input');
281
+ if (!input) return;
282
+
283
+ const command = input.value.trim();
284
+
285
+ if (!command) return;
286
+
287
+ // Show command in console
288
+ addLogMessage('USER', `$ ${command}`, 'cyan');
289
+
290
+ // Process command
291
+ processCommand(command);
292
+
293
+ // Clear input
294
+ input.value = '';
295
+ }
296
+
297
+ function processCommand(command) {
298
+ const cmd = command.toLowerCase();
299
+
300
+ // Handle local-only commands
301
+ if (cmd === 'clear') {
302
+ clearConsole();
303
+ return;
304
+ }
305
+
306
+ if (cmd === 'reconnect') {
307
+ addLogMessage('INFO', 'Reconnecting to log stream...', 'blue');
308
+ connectToLogStream();
309
+ return;
310
+ }
311
+
312
+ // Send command to server for processing and live console display
313
+ sendCommandToServer(command);
314
+ }
315
+
316
+ async function sendCommandToServer(command) {
317
+ const consoleToken = '{{ console_token }}';
318
+
319
+ if (!consoleToken || consoleToken === 'undefined' || consoleToken === '') {
320
+ addLogMessage('ERROR', 'No console token available. Cannot execute command.', 'red');
321
+ return;
322
+ }
323
+
324
+ try {
325
+ const response = await fetch(`/admin/api/console/execute?token=${encodeURIComponent(consoleToken)}`, {
326
+ method: 'POST',
327
+ headers: {
328
+ 'Content-Type': 'application/json',
329
+ },
330
+ body: JSON.stringify({
331
+ command: command
332
+ })
333
+ });
334
+
335
+ if (!response.ok) {
336
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
337
+ }
338
+
339
+ const result = await response.json();
340
+
341
+ if (result.status === 'error') {
342
+ addLogMessage('ERROR', `Command failed: ${result.message}`, 'red');
343
+ }
344
+ // Success messages are handled by the log stream
345
+
346
+ } catch (error) {
347
+ addLogMessage('ERROR', `Failed to execute command: ${error.message}`, 'red');
348
+ }
349
+ }
350
+
351
+ function clearConsole() {
352
+ const lines = document.getElementById('console-lines');
353
+ if (lines) {
354
+ lines.innerHTML = '';
355
+ lineCount = 0;
356
+ addLogMessage('SYSTEM', 'Console cleared.', 'blue');
357
+ }
358
+ }
359
+
360
+ function toggleAutoScroll() {
361
+ autoScroll = !autoScroll;
362
+ const btn = document.getElementById('autoscroll-btn');
363
+ const status = document.getElementById('autoscroll-status');
364
+
365
+ if (btn && status) {
366
+ const newText = autoScroll ? 'Auto-scroll: ON' : 'Auto-scroll: OFF';
367
+ btn.textContent = newText;
368
+ status.textContent = autoScroll ? 'ON' : 'OFF';
369
+ }
370
+
371
+ if (autoScroll) {
372
+ const output = document.getElementById('console-output');
373
+ if (output) {
374
+ output.scrollTop = output.scrollHeight;
375
+ }
376
+ }
377
+ }
378
+
379
+ // Clean up on page unload
380
+ window.addEventListener('beforeunload', function() {
381
+ if (eventSource) {
382
+ eventSource.close();
383
+ }
384
+ if (reconnectTimeout) {
385
+ clearTimeout(reconnectTimeout);
386
+ }
387
+ });
388
+ </script>
389
+ {% endblock %}
@@ -0,0 +1,153 @@
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Dashboard - 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
+ Dashboard
12
+ </h2>
13
+ </div>
14
+ </div>
15
+
16
+ <!-- Stats Cards -->
17
+ <div class="mt-8 grid grid-cols-1 gap-5 sm:grid-cols-3">
18
+ <!-- Jobs Stats -->
19
+ <div class="bg-white overflow-hidden shadow rounded-lg">
20
+ <div class="p-5">
21
+ <div class="flex items-center">
22
+ <div class="flex-shrink-0">
23
+ <div class="w-8 h-8 bg-blue-500 rounded-md flex items-center justify-center">
24
+ <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
25
+ <path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z" clip-rule="evenodd"></path>
26
+ </svg>
27
+ </div>
28
+ </div>
29
+ <div class="ml-5 w-0 flex-1">
30
+ <dl>
31
+ <dt class="text-sm font-medium text-gray-500 truncate">Total Jobs</dt>
32
+ <dd class="text-lg font-medium text-gray-900">{{ stats.jobs.total }}</dd>
33
+ </dl>
34
+ </div>
35
+ </div>
36
+ </div>
37
+ <div class="bg-gray-50 px-5 py-3">
38
+ <div class="text-sm">
39
+ <a href="/admin/jobs" class="font-medium text-blue-700 hover:text-blue-900">
40
+ View all jobs
41
+ </a>
42
+ </div>
43
+ <div class="mt-1 text-xs text-gray-500">
44
+ Running: {{ stats.jobs.running }} | Completed: {{ stats.jobs.completed }} | Failed: {{ stats.jobs.failed }}
45
+ </div>
46
+ </div>
47
+ </div>
48
+
49
+ <!-- Cases Stats -->
50
+ <div class="bg-white overflow-hidden shadow rounded-lg">
51
+ <div class="p-5">
52
+ <div class="flex items-center">
53
+ <div class="flex-shrink-0">
54
+ <div class="w-8 h-8 bg-green-500 rounded-md flex items-center justify-center">
55
+ <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
56
+ <path fill-rule="evenodd" d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path>
57
+ </svg>
58
+ </div>
59
+ </div>
60
+ <div class="ml-5 w-0 flex-1">
61
+ <dl>
62
+ <dt class="text-sm font-medium text-gray-500 truncate">Total Cases</dt>
63
+ <dd class="text-lg font-medium text-gray-900">{{ stats.cases.total }}</dd>
64
+ </dl>
65
+ </div>
66
+ </div>
67
+ </div>
68
+ <div class="bg-gray-50 px-5 py-3">
69
+ <div class="text-sm">
70
+ <a href="/admin/cases" class="font-medium text-green-700 hover:text-green-900">
71
+ View all cases
72
+ </a>
73
+ </div>
74
+ <div class="mt-1 text-xs text-gray-500">
75
+ Running: {{ stats.cases.running }} | Completed: {{ stats.cases.completed }} | Failed: {{ stats.cases.failed }}
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ <!-- System Stats -->
81
+ <div class="bg-white overflow-hidden shadow rounded-lg">
82
+ <div class="p-5">
83
+ <div class="flex items-center">
84
+ <div class="flex-shrink-0">
85
+ <div class="w-8 h-8 bg-purple-500 rounded-md flex items-center justify-center">
86
+ <svg class="w-5 h-5 text-white" fill="currentColor" viewBox="0 0 20 20">
87
+ <path fill-rule="evenodd" d="M2 5a2 2 0 012-2h8a2 2 0 012 2v10a2 2 0 002 2H4a2 2 0 01-2-2V5zm3 1h6v4H5V6zm6 6H5v2h6v-2z" clip-rule="evenodd"></path>
88
+ <path d="M15 7h1a2 2 0 012 2v5.5a1.5 1.5 0 01-3 0V9a1 1 0 00-1-1h-1v-1z"></path>
89
+ </svg>
90
+ </div>
91
+ </div>
92
+ <div class="ml-5 w-0 flex-1">
93
+ <dl>
94
+ <dt class="text-sm font-medium text-gray-500 truncate">System Status</dt>
95
+ <dd class="text-lg font-medium text-gray-900">{{ system_status }}</dd>
96
+ </dl>
97
+ </div>
98
+ </div>
99
+ </div>
100
+ <div class="bg-gray-50 px-5 py-3">
101
+ <div class="text-sm">
102
+ <span class="font-medium text-purple-700">Persistent Storage: {{ data_storage_path }}</span>
103
+ </div>
104
+ <div class="mt-1 text-xs text-gray-500">
105
+ Database: {{ db_name }} | Collections: {{ stats.collections }}
106
+ </div>
107
+ </div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Recent Activity -->
112
+ <div class="mt-8">
113
+ <div class="md:flex md:items-center md:justify-between">
114
+ <div class="min-w-0 flex-1">
115
+ <h3 class="text-lg font-medium leading-6 text-gray-900">Recent Activity</h3>
116
+ </div>
117
+ <div class="mt-4 flex md:mt-0">
118
+ <button
119
+ hx-get="/admin/api/recent-activity"
120
+ hx-target="#recent-activity-content"
121
+ hx-indicator="#refresh-indicator"
122
+ 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"
123
+ >
124
+ <svg id="refresh-indicator" class="htmx-indicator -ml-1 mr-2 h-4 w-4 animate-spin" fill="none" viewBox="0 0 24 24">
125
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
126
+ <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>
127
+ </svg>
128
+ Refresh
129
+ </button>
130
+ </div>
131
+ </div>
132
+
133
+ <div id="recent-activity-content" class="mt-6 bg-white shadow overflow-hidden rounded-md">
134
+ <div class="px-4 py-5 sm:p-6">
135
+ <div class="text-center">
136
+ <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
137
+ <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" />
138
+ </svg>
139
+ <h3 class="mt-2 text-sm font-medium text-gray-900">Loading recent activity...</h3>
140
+ <p class="mt-1 text-sm text-gray-500">This will show the latest entity updates.</p>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+ </div>
146
+
147
+ <script>
148
+ // Auto-load recent activity on page load
149
+ document.addEventListener('DOMContentLoaded', function() {
150
+ htmx.trigger('#recent-activity-content', 'refresh');
151
+ });
152
+ </script>
153
+ {% endblock %}