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.
- keepup/THIRD-PARTY.md +44 -0
- keepup/__init__.py +41 -0
- keepup/api_versions.py +100 -0
- keepup/audit.py +499 -0
- keepup/auth/__init__.py +7 -0
- keepup/auth/config.py +128 -0
- keepup/auth/dependencies.py +485 -0
- keepup/auth/dto/__init__.py +0 -0
- keepup/auth/dto/token.py +31 -0
- keepup/auth/factory.py +28 -0
- keepup/auth/login_throttle.py +151 -0
- keepup/auth/oidc.py +245 -0
- keepup/auth/oidc_policy.py +111 -0
- keepup/auth/oidc_routes.py +288 -0
- keepup/auth/panel_session.py +220 -0
- keepup/auth/permissions.py +59 -0
- keepup/auth/providers/__init__.py +0 -0
- keepup/auth/providers/base.py +168 -0
- keepup/auth/providers/local.py +180 -0
- keepup/auth/routes.py +660 -0
- keepup/auth/seed_accounts.py +322 -0
- keepup/auth/session_lifetime.py +104 -0
- keepup/auth/signing_key.py +138 -0
- keepup/auth/websocket.py +86 -0
- keepup/cluster.py +634 -0
- keepup/db.py +663 -0
- keepup/events.py +764 -0
- keepup/factory.py +484 -0
- keepup/instance.py +46 -0
- keepup/integrations.py +260 -0
- keepup/locks.py +412 -0
- keepup/logging_setup.py +690 -0
- keepup/metrics.py +818 -0
- keepup/metrics_retention.py +376 -0
- keepup/modules.py +572 -0
- keepup/notification_bus.py +355 -0
- keepup/plugins/__init__.py +9 -0
- keepup/plugins/admin.py +246 -0
- keepup/plugins/base.py +234 -0
- keepup/plugins/enablement.py +184 -0
- keepup/plugins/registry.py +171 -0
- keepup/plugins/route_mask.py +338 -0
- keepup/plugins/routes.py +376 -0
- keepup/roles.py +17 -0
- keepup/scheduler.py +93 -0
- keepup/schema.py +295 -0
- keepup/sections.json +104 -0
- keepup/settings.py +184 -0
- keepup/static/css/aos.css +1 -0
- keepup/static/css/main_nebula.css +232 -0
- keepup/static/css/main_new.css +852 -0
- keepup/static/css/tailwind.css +1 -0
- keepup/static/index_nebula.html +293 -0
- keepup/static/index_new.html +286 -0
- keepup/static/js/aos.js +1 -0
- keepup/static/js/feather-icons.js +13 -0
- keepup/static/js/main_new.js +1861 -0
- keepup/static/js/tailwind.js +83 -0
- keepup/static/modules/css/background_tasks.css +233 -0
- keepup/static/modules/css/cluster.css +16 -0
- keepup/static/modules/css/event_manager.css +386 -0
- keepup/static/modules/css/integration_logs.css +33 -0
- keepup/static/modules/css/metrics.css +115 -0
- keepup/static/modules/css/modules.css +189 -0
- keepup/static/modules/css/themes.css +563 -0
- keepup/static/modules/css/users.css +278 -0
- keepup/static/modules/js/background_tasks.js +657 -0
- keepup/static/modules/js/chart.js +14 -0
- keepup/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
- keepup/static/modules/js/cluster.js +363 -0
- keepup/static/modules/js/event_manager.js +979 -0
- keepup/static/modules/js/integration_logs.js +767 -0
- keepup/static/modules/js/metrics.js +908 -0
- keepup/static/modules/js/modules.js +1086 -0
- keepup/static/modules/js/themes.js +653 -0
- keepup/static/modules/js/users.js +784 -0
- keepup/tables.py +302 -0
- keepup/themes.py +496 -0
- keepup/web.py +182 -0
- keepup_admin-0.1.0.dist-info/METADATA +117 -0
- keepup_admin-0.1.0.dist-info/RECORD +86 -0
- keepup_admin-0.1.0.dist-info/WHEEL +5 -0
- keepup_admin-0.1.0.dist-info/licenses/LICENSE +202 -0
- keepup_admin-0.1.0.dist-info/licenses/NOTICE +22 -0
- keepup_admin-0.1.0.dist-info/licenses/THIRD-PARTY.md +44 -0
- 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
|
+
}
|