keepup-admin 0.1.0__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 (86) hide show
  1. keepup/THIRD-PARTY.md +44 -0
  2. keepup/__init__.py +41 -0
  3. keepup/api_versions.py +100 -0
  4. keepup/audit.py +499 -0
  5. keepup/auth/__init__.py +7 -0
  6. keepup/auth/config.py +128 -0
  7. keepup/auth/dependencies.py +485 -0
  8. keepup/auth/dto/__init__.py +0 -0
  9. keepup/auth/dto/token.py +31 -0
  10. keepup/auth/factory.py +28 -0
  11. keepup/auth/login_throttle.py +151 -0
  12. keepup/auth/oidc.py +245 -0
  13. keepup/auth/oidc_policy.py +111 -0
  14. keepup/auth/oidc_routes.py +288 -0
  15. keepup/auth/panel_session.py +220 -0
  16. keepup/auth/permissions.py +59 -0
  17. keepup/auth/providers/__init__.py +0 -0
  18. keepup/auth/providers/base.py +168 -0
  19. keepup/auth/providers/local.py +180 -0
  20. keepup/auth/routes.py +660 -0
  21. keepup/auth/seed_accounts.py +322 -0
  22. keepup/auth/session_lifetime.py +104 -0
  23. keepup/auth/signing_key.py +138 -0
  24. keepup/auth/websocket.py +86 -0
  25. keepup/cluster.py +634 -0
  26. keepup/db.py +663 -0
  27. keepup/events.py +764 -0
  28. keepup/factory.py +484 -0
  29. keepup/instance.py +46 -0
  30. keepup/integrations.py +260 -0
  31. keepup/locks.py +412 -0
  32. keepup/logging_setup.py +690 -0
  33. keepup/metrics.py +818 -0
  34. keepup/metrics_retention.py +376 -0
  35. keepup/modules.py +572 -0
  36. keepup/notification_bus.py +355 -0
  37. keepup/plugins/__init__.py +9 -0
  38. keepup/plugins/admin.py +246 -0
  39. keepup/plugins/base.py +234 -0
  40. keepup/plugins/enablement.py +184 -0
  41. keepup/plugins/registry.py +171 -0
  42. keepup/plugins/route_mask.py +338 -0
  43. keepup/plugins/routes.py +376 -0
  44. keepup/roles.py +17 -0
  45. keepup/scheduler.py +93 -0
  46. keepup/schema.py +295 -0
  47. keepup/sections.json +104 -0
  48. keepup/settings.py +184 -0
  49. keepup/static/css/aos.css +1 -0
  50. keepup/static/css/main_nebula.css +232 -0
  51. keepup/static/css/main_new.css +852 -0
  52. keepup/static/css/tailwind.css +1 -0
  53. keepup/static/index_nebula.html +293 -0
  54. keepup/static/index_new.html +286 -0
  55. keepup/static/js/aos.js +1 -0
  56. keepup/static/js/feather-icons.js +13 -0
  57. keepup/static/js/main_new.js +1861 -0
  58. keepup/static/js/tailwind.js +83 -0
  59. keepup/static/modules/css/background_tasks.css +233 -0
  60. keepup/static/modules/css/cluster.css +16 -0
  61. keepup/static/modules/css/event_manager.css +386 -0
  62. keepup/static/modules/css/integration_logs.css +33 -0
  63. keepup/static/modules/css/metrics.css +115 -0
  64. keepup/static/modules/css/modules.css +189 -0
  65. keepup/static/modules/css/themes.css +563 -0
  66. keepup/static/modules/css/users.css +278 -0
  67. keepup/static/modules/js/background_tasks.js +657 -0
  68. keepup/static/modules/js/chart.js +14 -0
  69. keepup/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
  70. keepup/static/modules/js/cluster.js +363 -0
  71. keepup/static/modules/js/event_manager.js +979 -0
  72. keepup/static/modules/js/integration_logs.js +767 -0
  73. keepup/static/modules/js/metrics.js +908 -0
  74. keepup/static/modules/js/modules.js +1086 -0
  75. keepup/static/modules/js/themes.js +653 -0
  76. keepup/static/modules/js/users.js +784 -0
  77. keepup/tables.py +302 -0
  78. keepup/themes.py +496 -0
  79. keepup/web.py +182 -0
  80. keepup_admin-0.1.0.dist-info/METADATA +117 -0
  81. keepup_admin-0.1.0.dist-info/RECORD +86 -0
  82. keepup_admin-0.1.0.dist-info/WHEEL +5 -0
  83. keepup_admin-0.1.0.dist-info/licenses/LICENSE +202 -0
  84. keepup_admin-0.1.0.dist-info/licenses/NOTICE +22 -0
  85. keepup_admin-0.1.0.dist-info/licenses/THIRD-PARTY.md +44 -0
  86. keepup_admin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,657 @@
1
+ // plugins/background_tasks.js
2
+
3
+ class BackgroundTasksPlugin {
4
+ constructor(config) {
5
+ this.plugin_id = "background_tasks";
6
+ this.name = "Background Tasks";
7
+ this.config = config || {};
8
+ this.initialized = false;
9
+ }
10
+
11
+ async initialize() {
12
+ console.log('Initializing background tasks plugin...');
13
+
14
+ // Add the nav item
15
+ this.addNavigation();
16
+
17
+ // Create the content section
18
+ this.createContentSection();
19
+
20
+ this.initialized = true;
21
+ return true;
22
+ }
23
+
24
+ createContentSection() {
25
+ // Skip if the section already exists
26
+ if (document.getElementById('background_tasks-section')) {
27
+ return;
28
+ }
29
+
30
+ const mainContent = document.querySelector('main');
31
+ if (!mainContent) {
32
+ console.error('Main content not found');
33
+ return;
34
+ }
35
+
36
+ // Create the content section
37
+ const contentSection = document.createElement('div');
38
+ contentSection.id = 'background_tasks-section';
39
+ contentSection.className = 'content-section';
40
+ contentSection.innerHTML = this.getContentHTML();
41
+
42
+ mainContent.appendChild(contentSection);
43
+ console.log('Section "Background Tasks" created');
44
+
45
+ // Wire up automatic data loading once the section is activated
46
+ this.setupSectionObserver();
47
+ }
48
+
49
+ setupSectionObserver() {
50
+ // Watch for attribute changes on the section
51
+ const observer = new MutationObserver((mutations) => {
52
+ mutations.forEach((mutation) => {
53
+ if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
54
+ const section = mutation.target;
55
+ if (section.classList.contains('active')) {
56
+ // Section became active -- load the data
57
+ this.loadData();
58
+ }
59
+ }
60
+ });
61
+ });
62
+
63
+ const section = document.getElementById('background_tasks-section');
64
+ if (section) {
65
+ observer.observe(section, {
66
+ attributes: true,
67
+ attributeFilter: ['class']
68
+ });
69
+ }
70
+ }
71
+
72
+ addNavigation() {
73
+ if (document.querySelector('[data-target="background_tasks-section"]')) {
74
+ return;
75
+ }
76
+
77
+ const mainNav = document.getElementById('main-nav');
78
+ if (!mainNav) {
79
+ console.error('Main navigation not found');
80
+ return;
81
+ }
82
+
83
+ // Create the nav item
84
+ const navItem = document.createElement('a');
85
+ navItem.href = '#';
86
+ navItem.className = 'nav-item flex items-center px-6 py-3 text-blue-100 hover:bg-blue-600';
87
+ navItem.setAttribute('data-target', 'background_tasks-section');
88
+ navItem.innerHTML = `
89
+ <i data-feather="clock" class="w-5 h-5 mr-3"></i>
90
+ <span class="nav-text">Background Tasks</span>
91
+ `;
92
+
93
+ // Do NOT attach our own handler -- rely on the existing navigation system
94
+ // Just add the element; the main navigation system picks it up on its own
95
+
96
+ // Add to the modules container
97
+ const modulesNavContainer = document.getElementById('modules-nav-container');
98
+ if (modulesNavContainer) {
99
+ modulesNavContainer.appendChild(navItem);
100
+ } else {
101
+ // No modules container -- fall back to the main nav
102
+ const uploadNav = mainNav.querySelector('[data-target="upload-section"]');
103
+ if (uploadNav) {
104
+ uploadNav.parentNode.insertBefore(navItem, uploadNav.nextSibling);
105
+ }
106
+ }
107
+
108
+ // Sync collapsed-state styling
109
+ const sidebar = document.getElementById('sidebar');
110
+ if (sidebar && sidebar.classList.contains('sidebar-collapsed')) {
111
+ this.updateNavItemForCollapsedState(navItem, true);
112
+ }
113
+
114
+ console.log('Nav item "Background Tasks" added');
115
+
116
+ // Re-run navigation setup for the new element
117
+ if (typeof setupNavigation === 'function') {
118
+ setupNavigation();
119
+ }
120
+ }
121
+
122
+ updateNavItemForCollapsedState(navItem, isCollapsed) {
123
+ const navText = navItem.querySelector('.nav-text');
124
+ const icon = navItem.querySelector('i');
125
+
126
+ if (isCollapsed) {
127
+ if (navText) {
128
+ navText.style.display = 'none';
129
+ }
130
+ navItem.classList.add('justify-center', 'px-3');
131
+ if (icon) {
132
+ icon.classList.remove('mr-3');
133
+ }
134
+ } else {
135
+ if (navText) {
136
+ navText.style.display = 'inline';
137
+ }
138
+ navItem.classList.remove('justify-center', 'px-3');
139
+ if (icon) {
140
+ icon.classList.add('mr-3');
141
+ }
142
+ }
143
+ }
144
+
145
+ getContentHTML() {
146
+ return `
147
+ <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
148
+ <div class="flex justify-between items-center mb-6">
149
+ <h2 class="text-2xl font-bold text-gray-800">Background Tasks and Locks</h2>
150
+ <div class="flex space-x-2">
151
+ <button onclick="backgroundTasksPlugin.refreshAllData()"
152
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
153
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
154
+ Refresh
155
+ </button>
156
+ </div>
157
+ </div>
158
+
159
+ <!-- Scheduler statistics -->
160
+ <div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
161
+ <div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
162
+ <div class="flex items-center">
163
+ <i data-feather="play-circle" class="w-8 h-8 text-blue-600 mr-3"></i>
164
+ <div>
165
+ <p class="text-sm text-blue-600">Scheduler status</p>
166
+ <p id="scheduler-status" class="text-lg font-semibold text-blue-800">Loading...</p>
167
+ </div>
168
+ </div>
169
+ </div>
170
+ <div class="bg-green-50 border border-green-200 rounded-lg p-4">
171
+ <div class="flex items-center">
172
+ <i data-feather="list" class="w-8 h-8 text-green-600 mr-3"></i>
173
+ <div>
174
+ <p class="text-sm text-green-600">Total jobs</p>
175
+ <p id="total-jobs" class="text-lg font-semibold text-green-800">0</p>
176
+ </div>
177
+ </div>
178
+ </div>
179
+ <div class="bg-purple-50 border border-purple-200 rounded-lg p-4">
180
+ <div class="flex items-center">
181
+ <i data-feather="clock" class="w-8 h-8 text-purple-600 mr-3"></i>
182
+ <div>
183
+ <p class="text-sm text-purple-600">Next run</p>
184
+ <p id="next-wakeup" class="text-lg font-semibold text-purple-800">—</p>
185
+ </div>
186
+ </div>
187
+ </div>
188
+ <div class="bg-orange-50 border border-orange-200 rounded-lg p-4">
189
+ <div class="flex items-center">
190
+ <i data-feather="cpu" class="w-8 h-8 text-orange-600 mr-3"></i>
191
+ <div>
192
+ <p class="text-sm text-orange-600">Pending tasks</p>
193
+ <p id="pending-tasks" class="text-lg font-semibold text-orange-800">0</p>
194
+ </div>
195
+ </div>
196
+ </div>
197
+ </div>
198
+
199
+ <!-- Active locks -->
200
+ <div class="mb-6">
201
+ <h3 class="text-lg font-semibold text-gray-800 mb-4">Active Locks</h3>
202
+ <div id="active-locks-container" class="bg-gray-50 rounded-lg p-4">
203
+ <div class="text-center py-4">
204
+ <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
205
+ <p class="text-gray-500">Loading locks...</p>
206
+ </div>
207
+ </div>
208
+ </div>
209
+
210
+ <!-- Scheduler jobs -->
211
+ <div class="mb-6">
212
+ <div class="flex justify-between items-center mb-4">
213
+ <h3 class="text-lg font-semibold text-gray-800">Scheduler Jobs</h3>
214
+ <button onclick="backgroundTasksPlugin.triggerAllJobs()"
215
+ class="px-3 py-1 bg-green-600 text-white rounded text-sm hover:bg-green-700 flex items-center">
216
+ <i data-feather="play" class="w-3 h-3 mr-1"></i>
217
+ Run all
218
+ </button>
219
+ </div>
220
+ <div id="scheduler-jobs-container" class="bg-white border border-gray-200 rounded-lg overflow-hidden">
221
+ <div class="text-center py-4">
222
+ <div class="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-600 mx-auto mb-2"></div>
223
+ <p class="text-gray-500">Loading jobs...</p>
224
+ </div>
225
+ </div>
226
+ </div>
227
+
228
+ <!-- Admin actions -->
229
+ <div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
230
+ <h3 class="text-lg font-semibold text-yellow-800 mb-2">Admin Actions</h3>
231
+ <p class="text-yellow-700 text-sm mb-3">
232
+ Warning: these actions are available only to system administrators
233
+ </p>
234
+ <div class="flex space-x-2">
235
+ <button onclick="backgroundTasksPlugin.forceCleanLocks()"
236
+ class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 flex items-center">
237
+ <i data-feather="trash-2" class="w-4 h-4 mr-2"></i>
238
+ Clear all locks
239
+ </button>
240
+ <button onclick="backgroundTasksPlugin.restartScheduler()"
241
+ class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center">
242
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
243
+ Restart scheduler
244
+ </button>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ `;
249
+ }
250
+
251
+ async loadData() {
252
+ await this.loadSchedulerStats();
253
+ await this.loadActiveLocks();
254
+ await this.loadSchedulerJobs();
255
+ }
256
+
257
+ async loadSchedulerStats() {
258
+ try {
259
+ const token = localStorage.getItem('authToken');
260
+ const response = await fetch('/api/admin/scheduler/stats', {
261
+ headers: {
262
+ 'Authorization': `Bearer ${token}`
263
+ }
264
+ });
265
+
266
+ if (response.ok) {
267
+ const stats = await response.json();
268
+ this.displaySchedulerStats(stats);
269
+ } else {
270
+ this.displayError('Error loading statistics');
271
+ }
272
+ } catch (error) {
273
+ console.error('Error loading statistics:', error);
274
+ this.displayError('Network error');
275
+ }
276
+ }
277
+
278
+ async loadActiveLocks() {
279
+ try {
280
+ const token = localStorage.getItem('authToken');
281
+ const response = await fetch('/api/admin/locks', {
282
+ headers: {
283
+ 'Authorization': `Bearer ${token}`
284
+ }
285
+ });
286
+
287
+ if (response.ok) {
288
+ const data = await response.json();
289
+ this.displayActiveLocks(data.locks || []);
290
+ } else {
291
+ this.displayLocksError();
292
+ }
293
+ } catch (error) {
294
+ console.error('Error loading locks:', error);
295
+ this.displayLocksError();
296
+ }
297
+ }
298
+
299
+ async loadSchedulerJobs() {
300
+ try {
301
+ const token = localStorage.getItem('authToken');
302
+ const response = await fetch('/api/admin/scheduler/jobs', {
303
+ headers: {
304
+ 'Authorization': `Bearer ${token}`
305
+ }
306
+ });
307
+
308
+ if (response.ok) {
309
+ const data = await response.json();
310
+ this.displaySchedulerJobs(data.jobs || []);
311
+ } else {
312
+ this.displayJobsError();
313
+ }
314
+ } catch (error) {
315
+ console.error('Error loading jobs:', error);
316
+ this.displayJobsError();
317
+ }
318
+ }
319
+
320
+ displaySchedulerStats(stats) {
321
+ if (stats.error) {
322
+ document.getElementById('scheduler-status').textContent = 'Unavailable';
323
+ document.getElementById('scheduler-status').className = 'text-lg font-semibold text-red-600';
324
+ return;
325
+ }
326
+
327
+ document.getElementById('scheduler-status').textContent =
328
+ stats.scheduler_running ? 'Running' : 'Stopped';
329
+ document.getElementById('scheduler-status').className =
330
+ `text-lg font-semibold ${stats.scheduler_running ? 'text-green-600' : 'text-red-600'}`;
331
+
332
+ document.getElementById('total-jobs').textContent = stats.total_jobs || 0;
333
+ document.getElementById('pending-tasks').textContent = stats.pending_tasks || 0;
334
+
335
+ const nextWakeup = document.getElementById('next-wakeup');
336
+ if (stats.next_wakeup) {
337
+ nextWakeup.textContent = new Date(stats.next_wakeup).toLocaleString();
338
+ } else {
339
+ nextWakeup.textContent = 'No scheduled jobs';
340
+ }
341
+ }
342
+
343
+ displayActiveLocks(locks) {
344
+ const container = document.getElementById('active-locks-container');
345
+
346
+ if (locks.length === 0) {
347
+ container.innerHTML = `
348
+ <div class="text-center py-4 text-gray-500">
349
+ <i data-feather="lock" class="w-12 h-12 mx-auto mb-2"></i>
350
+ <p>No active locks</p>
351
+ </div>
352
+ `;
353
+ feather.replace();
354
+ return;
355
+ }
356
+
357
+ container.innerHTML = locks.map(lock => `
358
+ <div class="border border-gray-200 rounded-lg p-4 mb-3 last:mb-0">
359
+ <div class="flex justify-between items-start mb-2">
360
+ <div>
361
+ <h4 class="font-semibold text-gray-800">${lock.lock_name}</h4>
362
+ <p class="text-sm text-gray-600">Instance: ${lock.instance_id}</p>
363
+ </div>
364
+ <div class="flex space-x-2">
365
+ <button onclick="backgroundTasksPlugin.forceReleaseLock('${lock.lock_name}')"
366
+ class="px-3 py-1 bg-red-100 text-red-700 rounded text-sm hover:bg-red-200 flex items-center">
367
+ <i data-feather="unlock" class="w-3 h-3 mr-1"></i>
368
+ Release
369
+ </button>
370
+ </div>
371
+ </div>
372
+ <div class="text-xs text-gray-500">
373
+ Acquired: ${new Date(lock.acquired_at).toLocaleString()}
374
+ </div>
375
+ </div>
376
+ `).join('');
377
+
378
+ feather.replace();
379
+ }
380
+
381
+ displaySchedulerJobs(jobs) {
382
+ const container = document.getElementById('scheduler-jobs-container');
383
+
384
+ if (jobs.length === 0) {
385
+ container.innerHTML = `
386
+ <div class="text-center py-4 text-gray-500">
387
+ <i data-feather="clock" class="w-12 h-12 mx-auto mb-2"></i>
388
+ <p>No jobs found</p>
389
+ </div>
390
+ `;
391
+ feather.replace();
392
+ return;
393
+ }
394
+
395
+ container.innerHTML = `
396
+ <table class="responsive-table min-w-full divide-y divide-gray-200">
397
+ <thead class="bg-gray-50">
398
+ <tr>
399
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">ID</th>
400
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Name</th>
401
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Next run</th>
402
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Trigger</th>
403
+ <th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
404
+ </tr>
405
+ </thead>
406
+ <tbody class="bg-white divide-y divide-gray-200">
407
+ ${jobs.map(job => `
408
+ <tr class="hover:bg-gray-50">
409
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${job.id}</td>
410
+ <td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">${job.name}</td>
411
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
412
+ ${job.next_run_time ? new Date(job.next_run_time).toLocaleString() : '—'}
413
+ </td>
414
+ <td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">${job.trigger}</td>
415
+ <td class="px-6 py-4 whitespace-nowrap text-sm">
416
+ <button onclick="backgroundTasksPlugin.triggerJob('${job.id}')"
417
+ class="text-blue-600 hover:text-blue-900 bg-blue-100 px-3 py-1 rounded text-sm flex items-center">
418
+ <i data-feather="play" class="w-3 h-3 mr-1"></i>
419
+ Run
420
+ </button>
421
+ </td>
422
+ </tr>
423
+ `).join('')}
424
+ </tbody>
425
+ </table>
426
+ `;
427
+
428
+ feather.replace();
429
+ }
430
+
431
+ displayError(message) {
432
+ document.getElementById('scheduler-status').textContent = message;
433
+ document.getElementById('scheduler-status').className = 'text-lg font-semibold text-red-600';
434
+ }
435
+
436
+ displayLocksError() {
437
+ const container = document.getElementById('active-locks-container');
438
+ container.innerHTML = `
439
+ <div class="text-center py-4 text-red-500">
440
+ <i data-feather="alert-triangle" class="w-12 h-12 mx-auto mb-2"></i>
441
+ <p>Error loading locks</p>
442
+ </div>
443
+ `;
444
+ feather.replace();
445
+ }
446
+
447
+ displayJobsError() {
448
+ const container = document.getElementById('scheduler-jobs-container');
449
+ container.innerHTML = `
450
+ <div class="text-center py-4 text-red-500">
451
+ <i data-feather="alert-triangle" class="w-12 h-12 mx-auto mb-2"></i>
452
+ <p>Error loading jobs</p>
453
+ </div>
454
+ `;
455
+ feather.replace();
456
+ }
457
+
458
+ // Action methods
459
+ async refreshAllData() {
460
+ await this.loadData();
461
+ this.showNotification('Data refreshed', 'success');
462
+ }
463
+
464
+ async triggerJob(jobId) {
465
+ try {
466
+ const token = localStorage.getItem('authToken');
467
+ const response = await fetch(`/api/admin/scheduler/jobs/${jobId}/trigger`, {
468
+ method: 'POST',
469
+ headers: {
470
+ 'Authorization': `Bearer ${token}`
471
+ }
472
+ });
473
+
474
+ if (response.ok) {
475
+ this.showNotification(`Job ${jobId} started`, 'success');
476
+ // Refresh the job list a second later
477
+ setTimeout(() => this.loadSchedulerJobs(), 1000);
478
+ } else {
479
+ this.showNotification('Error starting the job', 'error');
480
+ }
481
+ } catch (error) {
482
+ console.error('Error starting the job:', error);
483
+ this.showNotification('Network error', 'error');
484
+ }
485
+ }
486
+
487
+ async triggerAllJobs() {
488
+ try {
489
+ const token = localStorage.getItem('authToken');
490
+ const jobsResponse = await fetch('/api/admin/scheduler/jobs', {
491
+ headers: {
492
+ 'Authorization': `Bearer ${token}`
493
+ }
494
+ });
495
+
496
+ if (jobsResponse.ok) {
497
+ const data = await jobsResponse.json();
498
+ const jobs = data.jobs || [];
499
+
500
+ let successCount = 0;
501
+ for (const job of jobs) {
502
+ try {
503
+ const triggerResponse = await fetch(`/api/admin/scheduler/jobs/${job.id}/trigger`, {
504
+ method: 'POST',
505
+ headers: {
506
+ 'Authorization': `Bearer ${token}`
507
+ }
508
+ });
509
+ if (triggerResponse.ok) {
510
+ successCount++;
511
+ }
512
+ } catch (error) {
513
+ console.error(`Error starting job ${job.id}:`, error);
514
+ }
515
+ }
516
+
517
+ this.showNotification(`Started ${successCount} of ${jobs.length} jobs`, 'success');
518
+ setTimeout(() => this.loadSchedulerJobs(), 1000);
519
+ }
520
+ } catch (error) {
521
+ console.error('Error bulk-starting jobs:', error);
522
+ this.showNotification('Error starting jobs', 'error');
523
+ }
524
+ }
525
+
526
+ async forceReleaseLock(lockName) {
527
+ if (!confirm(`Are you sure you want to force-release the lock "${lockName}"?`)) {
528
+ return;
529
+ }
530
+
531
+ try {
532
+ const token = localStorage.getItem('authToken');
533
+ const response = await fetch(`/api/admin/locks/${lockName}?confirm=true`, {
534
+ method: 'DELETE',
535
+ headers: {
536
+ 'Authorization': `Bearer ${token}`
537
+ }
538
+ });
539
+
540
+ if (response.ok) {
541
+ this.showNotification(`Lock "${lockName}" released`, 'success');
542
+ setTimeout(() => this.loadActiveLocks(), 1000);
543
+ } else {
544
+ this.showNotification('Error releasing the lock', 'error');
545
+ }
546
+ } catch (error) {
547
+ console.error('Error releasing the lock:', error);
548
+ this.showNotification('Network error', 'error');
549
+ }
550
+ }
551
+
552
+ async forceCleanLocks() {
553
+ if (!confirm('Are you sure you want to clear ALL locks? This may disrupt background tasks.')) {
554
+ return;
555
+ }
556
+
557
+ try {
558
+ const token = localStorage.getItem('authToken');
559
+ const locksResponse = await fetch('/api/admin/locks', {
560
+ headers: {
561
+ 'Authorization': `Bearer ${token}`
562
+ }
563
+ });
564
+
565
+ if (locksResponse.ok) {
566
+ const data = await locksResponse.json();
567
+ const locks = data.locks || [];
568
+
569
+ let releasedCount = 0;
570
+ for (const lock of locks) {
571
+ try {
572
+ await fetch(`/api/admin/locks/${lock.lock_name}?confirm=true`, {
573
+ method: 'DELETE',
574
+ headers: {
575
+ 'Authorization': `Bearer ${token}`
576
+ }
577
+ });
578
+ releasedCount++;
579
+ } catch (error) {
580
+ console.error(`Error releasing lock ${lock.lock_name}:`, error);
581
+ }
582
+ }
583
+
584
+ this.showNotification(`Released ${releasedCount} locks`, 'success');
585
+ setTimeout(() => this.loadActiveLocks(), 1000);
586
+ }
587
+ } catch (error) {
588
+ console.error('Error clearing locks:', error);
589
+ this.showNotification('Error clearing locks', 'error');
590
+ }
591
+ }
592
+
593
+ async restartScheduler() {
594
+ if (!confirm('Restart the job scheduler? All current jobs will be restarted.')) {
595
+ return;
596
+ }
597
+
598
+ this.showNotification('Scheduler restart is not implemented yet', 'info');
599
+ // An API call to restart the scheduler could be added here
600
+ }
601
+
602
+ showNotification(message, type = 'info') {
603
+ // Use the existing showNotification function, or fall back to our own
604
+ if (typeof showNotification === 'function') {
605
+ showNotification(message, type);
606
+ } else {
607
+ // Simple fallback when the global function is not available
608
+ const notification = document.createElement('div');
609
+ notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 ${
610
+ type === 'success' ? 'bg-green-100 text-green-800 border border-green-200' :
611
+ type === 'error' ? 'bg-red-100 text-red-800 border border-red-200' :
612
+ 'bg-blue-100 text-blue-800 border border-blue-200'
613
+ }`;
614
+ notification.innerHTML = `
615
+ <div class="flex items-center">
616
+ <i data-feather="${type === 'success' ? 'check-circle' : type === 'error' ? 'alert-circle' : 'info'}"
617
+ class="w-5 h-5 mr-2"></i>
618
+ <span>${message}</span>
619
+ <button onclick="this.parentElement.parentElement.remove()"
620
+ class="ml-4 text-gray-500 hover:text-gray-700">
621
+ <i data-feather="x" class="w-4 h-4"></i>
622
+ </button>
623
+ </div>
624
+ `;
625
+ document.body.appendChild(notification);
626
+
627
+ if (typeof feather !== 'undefined') {
628
+ feather.replace();
629
+ }
630
+
631
+ setTimeout(() => {
632
+ if (notification.parentElement) {
633
+ notification.remove();
634
+ }
635
+ }, 5000);
636
+ }
637
+ }
638
+
639
+ getApiRoutes() {
640
+ return [];
641
+ }
642
+
643
+ getHandlers() {
644
+ return {};
645
+ }
646
+
647
+ async cleanup() {
648
+ this.initialized = false;
649
+ }
650
+ }
651
+
652
+ window.backgroundTasksPlugin = new BackgroundTasksPlugin();
653
+
654
+ function initBackgroundTasks() {
655
+ console.log('Starting background tasks plugin initialization...');
656
+ return window.backgroundTasksPlugin.initialize();
657
+ }