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,979 @@
1
+ /**
2
+ * Admin audit event management module
3
+ *
4
+ * Provides an interface for viewing, filtering, and analyzing system events
5
+ */
6
+
7
+ // ==================== GLOBAL VARIABLES ====================
8
+
9
+ // Event data
10
+ var auditEvents = window.auditEvents || [];
11
+ var currentEventId = null;
12
+ let isAuditRouteActive = false;
13
+
14
+ // Pagination
15
+ var auditCurrentPage = 1;
16
+ var auditTotalPages = 0;
17
+ var auditTotalEvents = 0;
18
+ var auditPageSize = 50;
19
+ var auditIsLoading = false;
20
+
21
+ // Filters
22
+ var auditFilters = {
23
+ event_type: null,
24
+ instance_id: null,
25
+ instance_name: null,
26
+ start_date: null,
27
+ end_date: null
28
+ };
29
+
30
+ // Available event types and instances
31
+ var auditEventTypes = [];
32
+ var auditInstances = [];
33
+
34
+ // Statistics
35
+ var auditStats = null;
36
+
37
+ // Initialization flag
38
+ var auditInitialized = false;
39
+
40
+ // ==================== HELPER FUNCTIONS ====================
41
+
42
+ /**
43
+ * Safe escaping of strings for use in HTML/JavaScript
44
+ */
45
+ function escapeHtml(str) {
46
+ if (typeof str !== 'string') return str || '';
47
+ return str
48
+ .replace(/&/g, '&')
49
+ .replace(/</g, '&lt;')
50
+ .replace(/>/g, '&gt;')
51
+ .replace(/"/g, '&quot;')
52
+ .replace(/'/g, '&#039;')
53
+ .replace(/\//g, '&#x2F;');
54
+ }
55
+
56
+ /**
57
+ * Date formatting
58
+ */
59
+ function formatDate(date, format = 'datetime') {
60
+ if (!date) return '—';
61
+ const d = new Date(date);
62
+ if (isNaN(d.getTime())) return '—';
63
+
64
+ const options = {
65
+ date: { year: 'numeric', month: 'long', day: 'numeric' },
66
+ datetime: { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false },
67
+ time: { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }
68
+ };
69
+
70
+ return d.toLocaleString('en-GB', options[format] || options.datetime);
71
+ }
72
+
73
+ /**
74
+ * JSON formatting for display
75
+ */
76
+ function formatJSON(data) {
77
+ if (!data) return '—';
78
+ try {
79
+ if (typeof data === 'string') {
80
+ data = JSON.parse(data);
81
+ }
82
+ return JSON.stringify(data, null, 2);
83
+ } catch (e) {
84
+ return String(data);
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Get color for event type
90
+ */
91
+ function getEventTypeColor(eventType) {
92
+ const colorMap = {
93
+ 'user_login': 'bg-green-100 text-green-800',
94
+ 'user_logout': 'bg-gray-100 text-gray-800',
95
+ 'payment_success': 'bg-blue-100 text-blue-800',
96
+ 'payment_failed': 'bg-red-100 text-red-800',
97
+ 'error': 'bg-red-100 text-red-800',
98
+ 'warning': 'bg-yellow-100 text-yellow-800',
99
+ 'info': 'bg-blue-100 text-blue-800',
100
+ 'config_change': 'bg-purple-100 text-purple-800',
101
+ 'api_call': 'bg-indigo-100 text-indigo-800'
102
+ };
103
+ return colorMap[eventType] || 'bg-gray-100 text-gray-800';
104
+ }
105
+
106
+ /**
107
+ * Show notification
108
+ */
109
+ function showNotification(message, type = 'info') {
110
+ // Check whether a global notification function exists and isn't this same function
111
+ if (window.showNotification && window.showNotification !== showNotification) {
112
+ // Use the existing notification function from main_new.js
113
+ window.showNotification(message, type);
114
+ } else if (window.app && window.app.showNotification) {
115
+ // Fallback option
116
+ window.app.showNotification(message, type);
117
+ } else {
118
+ // Create a temporary notification if no global function exists
119
+ console.log(`[${type.toUpperCase()}] ${message}`);
120
+
121
+ // Create a simple notification on the page
122
+ const notification = document.createElement('div');
123
+ notification.className = `fixed top-4 right-4 p-4 rounded-lg shadow-lg z-50 ${
124
+ type === 'success' ? 'bg-green-100 text-green-800 border border-green-200' :
125
+ type === 'error' ? 'bg-red-100 text-red-800 border border-red-200' :
126
+ 'bg-blue-100 text-blue-800 border border-blue-200'
127
+ }`;
128
+ notification.innerHTML = `
129
+ <div class="flex items-center">
130
+ <span>${escapeHtml(message)}</span>
131
+ <button onclick="this.parentElement.parentElement.remove()"
132
+ class="ml-4 text-gray-500 hover:text-gray-700">
133
+ ✕
134
+ </button>
135
+ </div>
136
+ `;
137
+ document.body.appendChild(notification);
138
+
139
+ // Auto-hide after 5 seconds
140
+ setTimeout(() => {
141
+ if (notification.parentElement) {
142
+ notification.remove();
143
+ }
144
+ }, 5000);
145
+ }
146
+ }
147
+
148
+ // ==================== DATA LOADING ====================
149
+
150
+ /**
151
+ * Load the event list with filtering
152
+ */
153
+ async function loadAuditEvents(page = 1, pageSize = 50, append = false) {
154
+ if (auditIsLoading) return;
155
+
156
+ try {
157
+ auditIsLoading = true;
158
+ const token = localStorage.getItem('authToken');
159
+
160
+ // Build the URL with filter parameters
161
+ let url = `/api/events?page=${page}&page_size=${pageSize}`;
162
+
163
+ if (auditFilters.event_type) {
164
+ url += `&event_type=${encodeURIComponent(auditFilters.event_type)}`;
165
+ }
166
+ if (auditFilters.instance_id) {
167
+ url += `&instance_id=${encodeURIComponent(auditFilters.instance_id)}`;
168
+ }
169
+ if (auditFilters.instance_name) {
170
+ url += `&instance_name=${encodeURIComponent(auditFilters.instance_name)}`;
171
+ }
172
+ if (auditFilters.start_date) {
173
+ url += `&start_date=${encodeURIComponent(auditFilters.start_date)}`;
174
+ }
175
+ if (auditFilters.end_date) {
176
+ url += `&end_date=${encodeURIComponent(auditFilters.end_date)}`;
177
+ }
178
+
179
+ const response = await fetch(url, {
180
+ headers: {
181
+ 'Authorization': `Bearer ${token}`
182
+ }
183
+ });
184
+
185
+ if (response.ok) {
186
+ const result = await response.json();
187
+ auditCurrentPage = result.page;
188
+ auditTotalPages = result.total_pages;
189
+ auditTotalEvents = result.total;
190
+
191
+ if (page === 1 && !append) {
192
+ auditEvents = result.events;
193
+ } else {
194
+ auditEvents = [...auditEvents, ...result.events];
195
+ }
196
+
197
+ displayAuditEvents(auditEvents);
198
+ updateAuditPagination();
199
+ updateFilterInfo();
200
+
201
+ if (result.events.length > 0) {
202
+ showNotification(`Loaded ${result.events.length} events of ${result.total}`, 'success');
203
+ }
204
+ } else {
205
+ const error = await response.json();
206
+ throw new Error(error.detail || 'Error loading events');
207
+ }
208
+
209
+ } catch (error) {
210
+ console.error('Load error:', error);
211
+ if (error.message !== 'Failed to fetch') {
212
+ showNotification('Load error: ' + error.message, 'error');
213
+ }
214
+ displayAuditError(error.message);
215
+ } finally {
216
+ auditIsLoading = false;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Load event types
222
+ */
223
+ async function loadEventTypes() {
224
+ try {
225
+ const token = localStorage.getItem('authToken');
226
+ const response = await fetch('/api/events/types', {
227
+ headers: {
228
+ 'Authorization': `Bearer ${token}`
229
+ }
230
+ });
231
+
232
+ if (response.ok) {
233
+ const result = await response.json();
234
+ auditEventTypes = result.event_types || [];
235
+ updateEventTypeFilter();
236
+ }
237
+ } catch (error) {
238
+ console.error('Error loading event types:', error);
239
+ }
240
+ }
241
+
242
+ /**
243
+ * Load the instance list
244
+ */
245
+ async function loadInstances() {
246
+ try {
247
+ const token = localStorage.getItem('authToken');
248
+ const response = await fetch('/api/events/instances', {
249
+ headers: {
250
+ 'Authorization': `Bearer ${token}`
251
+ }
252
+ });
253
+
254
+ if (response.ok) {
255
+ const result = await response.json();
256
+ auditInstances = result.instances || [];
257
+ updateInstanceFilter();
258
+ }
259
+ } catch (error) {
260
+ console.error('Error loading instances:', error);
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Load statistics
266
+ */
267
+ async function loadAuditStats() {
268
+ try {
269
+ const token = localStorage.getItem('authToken');
270
+ const response = await fetch('/api/events/stats', {
271
+ headers: {
272
+ 'Authorization': `Bearer ${token}`
273
+ }
274
+ });
275
+
276
+ if (response.ok) {
277
+ auditStats = await response.json();
278
+ displayAuditStats();
279
+ }
280
+ } catch (error) {
281
+ console.error('Error loading stats:', error);
282
+ }
283
+ }
284
+
285
+ // ==================== DATA DISPLAY ====================
286
+
287
+ /**
288
+ * Display the event list
289
+ */
290
+ function displayAuditEvents(events) {
291
+ const container = document.getElementById('audit-events-container');
292
+ if (!container) return;
293
+
294
+ if (!events || events.length === 0) {
295
+ container.innerHTML = `
296
+ <div class="text-center py-12">
297
+ <i data-feather="inbox" class="w-12 h-12 text-gray-400 mx-auto mb-3"></i>
298
+ <p class="text-gray-500">No events to display</p>
299
+ <button onclick="resetAuditFilters()" class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
300
+ Reset filters
301
+ </button>
302
+ </div>
303
+ `;
304
+ if (typeof feather !== 'undefined') feather.replace();
305
+ return;
306
+ }
307
+
308
+ container.innerHTML = '';
309
+
310
+ events.forEach(event => {
311
+ const eventElement = createAuditEventElement(event);
312
+ container.appendChild(eventElement);
313
+ });
314
+
315
+ if (typeof feather !== 'undefined') feather.replace();
316
+ }
317
+
318
+ /**
319
+ * Create an event element
320
+ */
321
+ function createAuditEventElement(event) {
322
+ const div = document.createElement('div');
323
+ div.className = 'audit-event bg-white border rounded-lg p-4 hover:shadow-md transition-shadow mb-3';
324
+ div.setAttribute('data-event-id', event.id);
325
+
326
+ const eventTypeColor = getEventTypeColor(event.event_type);
327
+ const eventData = event.event_data ? formatJSON(event.event_data) : null;
328
+
329
+ div.innerHTML = `
330
+ <div class="flex justify-between items-start mb-3">
331
+ <div class="flex items-center space-x-2">
332
+ <span class="px-2 py-1 text-xs rounded-full ${eventTypeColor}">
333
+ ${escapeHtml(event.event_type)}
334
+ </span>
335
+ <span class="text-xs text-gray-500">ID: ${event.id}</span>
336
+ </div>
337
+ <span class="text-xs text-gray-500">${formatDate(event.created_at, 'datetime')}</span>
338
+ </div>
339
+
340
+ <div class="mb-3">
341
+ <p class="text-gray-800">${escapeHtml(event.event_text)}</p>
342
+ </div>
343
+
344
+ <div class="text-sm text-gray-600 mb-3">
345
+ <div class="flex items-center space-x-4">
346
+ <span><strong>Instance:</strong> ${escapeHtml(event.instance_name || event.instance_id)}</span>
347
+ ${event.instance_id ? `<span class="text-xs text-gray-500">(${escapeHtml(event.instance_id)})</span>` : ''}
348
+ </div>
349
+ </div>
350
+
351
+ ${eventData && eventData !== '—' ? `
352
+ <div class="mt-3">
353
+ <button onclick="toggleEventData(${event.id})"
354
+ class="text-sm text-blue-600 hover:text-blue-800 flex items-center">
355
+ <i data-feather="chevron-down" class="w-4 h-4 mr-1" id="chevron-${event.id}"></i>
356
+ Show data
357
+ </button>
358
+ <div id="event-data-${event.id}" class="hidden mt-2">
359
+ <pre class="bg-gray-50 p-3 rounded text-xs overflow-x-auto">${escapeHtml(eventData)}</pre>
360
+ </div>
361
+ </div>
362
+ ` : ''}
363
+ `;
364
+
365
+ return div;
366
+ }
367
+
368
+ /**
369
+ * Toggle event data display
370
+ */
371
+ function toggleEventData(eventId) {
372
+ const dataDiv = document.getElementById(`event-data-${eventId}`);
373
+ const chevron = document.getElementById(`chevron-${eventId}`);
374
+
375
+ if (dataDiv) {
376
+ if (dataDiv.classList.contains('hidden')) {
377
+ dataDiv.classList.remove('hidden');
378
+ if (chevron) chevron.setAttribute('data-feather', 'chevron-up');
379
+ } else {
380
+ dataDiv.classList.add('hidden');
381
+ if (chevron) chevron.setAttribute('data-feather', 'chevron-down');
382
+ }
383
+ if (typeof feather !== 'undefined') feather.replace();
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Display an error
389
+ */
390
+ function displayAuditError(message) {
391
+ const container = document.getElementById('audit-events-container');
392
+ if (!container) return;
393
+
394
+ container.innerHTML = `
395
+ <div class="text-center py-12">
396
+ <i data-feather="alert-triangle" class="w-12 h-12 text-red-400 mx-auto mb-3"></i>
397
+ <p class="text-red-500 mb-4">${escapeHtml(message)}</p>
398
+ <button onclick="refreshAuditEvents()"
399
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
400
+ Try again
401
+ </button>
402
+ </div>
403
+ `;
404
+ if (typeof feather !== 'undefined') feather.replace();
405
+ }
406
+
407
+ /**
408
+ * Display statistics
409
+ */
410
+ function displayAuditStats() {
411
+ const container = document.getElementById('audit-stats-container');
412
+ if (!container || !auditStats) return;
413
+
414
+ container.innerHTML = `
415
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
416
+ <div class="bg-blue-50 rounded-lg p-4">
417
+ <div class="text-sm text-blue-600 mb-1">Total events</div>
418
+ <div class="text-2xl font-bold text-blue-900">${auditStats.total_events || 0}</div>
419
+ </div>
420
+ <div class="bg-green-50 rounded-lg p-4">
421
+ <div class="text-sm text-green-600 mb-1">Event types</div>
422
+ <div class="text-2xl font-bold text-green-900">${auditStats.unique_event_types || 0}</div>
423
+ </div>
424
+ <div class="bg-purple-50 rounded-lg p-4">
425
+ <div class="text-sm text-purple-600 mb-1">Instances</div>
426
+ <div class="text-2xl font-bold text-purple-900">${auditStats.unique_instances || 0}</div>
427
+ </div>
428
+ </div>
429
+
430
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
431
+ <div class="bg-white rounded-lg p-4 border">
432
+ <h4 class="font-semibold text-gray-800 mb-3">Top event types</h4>
433
+ <div class="space-y-2">
434
+ ${auditStats.event_types?.slice(0, 5).map(type => `
435
+ <div class="flex justify-between items-center">
436
+ <span class="text-sm">${escapeHtml(type.event_type)}</span>
437
+ <span class="text-sm font-semibold">${type.count}</span>
438
+ </div>
439
+ <div class="w-full bg-gray-200 rounded-full h-2">
440
+ <div class="bg-blue-600 rounded-full h-2" style="width: ${(type.count / auditStats.total_events) * 100}%"></div>
441
+ </div>
442
+ `).join('') || '<p class="text-gray-500">No data</p>'}
443
+ </div>
444
+ </div>
445
+
446
+ <div class="bg-white rounded-lg p-4 border">
447
+ <h4 class="font-semibold text-gray-800 mb-3">Active instances</h4>
448
+ <div class="space-y-2">
449
+ ${auditStats.instances?.slice(0, 5).map(instance => `
450
+ <div class="flex justify-between items-center">
451
+ <span class="text-sm">${escapeHtml(instance.instance_name || instance.instance_id)}</span>
452
+ <span class="text-sm font-semibold">${instance.event_count}</span>
453
+ </div>
454
+ `).join('') || '<p class="text-gray-500">No data</p>'}
455
+ </div>
456
+ </div>
457
+ </div>
458
+
459
+ <div class="mt-6 bg-yellow-50 rounded-lg p-4">
460
+ <div class="flex justify-between items-center">
461
+ <div>
462
+ <p class="text-sm text-yellow-800">Current instance</p>
463
+ <p class="font-semibold">${escapeHtml(auditStats.current_instance?.name)}</p>
464
+ <p class="text-xs text-yellow-600">ID: ${escapeHtml(auditStats.current_instance?.id)}</p>
465
+ </div>
466
+ <button onclick="refreshAuditStats()" class="text-yellow-800 hover:text-yellow-900">
467
+ <i data-feather="refresh-cw" class="w-5 h-5"></i>
468
+ </button>
469
+ </div>
470
+ </div>
471
+ `;
472
+ if (typeof feather !== 'undefined') feather.replace();
473
+ }
474
+
475
+ // ==================== FILTERING ====================
476
+
477
+ /**
478
+ * Apply filters
479
+ */
480
+ function applyAuditFilters() {
481
+ const eventType = document.getElementById('filter-event-type')?.value;
482
+ const instanceName = document.getElementById('filter-instance')?.value;
483
+ const startDate = document.getElementById('filter-start-date')?.value;
484
+ const endDate = document.getElementById('filter-end-date')?.value;
485
+
486
+ auditFilters.event_type = eventType || null;
487
+ auditFilters.instance_name = instanceName || null;
488
+ auditFilters.start_date = startDate || null;
489
+ auditFilters.end_date = endDate || null;
490
+
491
+ refreshAuditEvents();
492
+ }
493
+
494
+ /**
495
+ * Reset filters
496
+ */
497
+ function resetAuditFilters() {
498
+ auditFilters = {
499
+ event_type: null,
500
+ instance_id: null,
501
+ instance_name: null,
502
+ start_date: null,
503
+ end_date: null
504
+ };
505
+
506
+ // Reset form fields
507
+ const filterForm = document.getElementById('audit-filters-form');
508
+ if (filterForm) filterForm.reset();
509
+
510
+ refreshAuditEvents();
511
+ }
512
+
513
+ /**
514
+ * Update filter info
515
+ */
516
+ function updateFilterInfo() {
517
+ const filterInfo = document.getElementById('filter-info');
518
+ if (!filterInfo) return;
519
+
520
+ const activeFilters = [];
521
+ if (auditFilters.event_type) activeFilters.push(`Type: ${auditFilters.event_type}`);
522
+ if (auditFilters.instance_name) activeFilters.push(`Instance: ${auditFilters.instance_name}`);
523
+ if (auditFilters.start_date) activeFilters.push(`From: ${formatDate(auditFilters.start_date, 'date')}`);
524
+ if (auditFilters.end_date) activeFilters.push(`To: ${formatDate(auditFilters.end_date, 'date')}`);
525
+
526
+ if (activeFilters.length > 0) {
527
+ filterInfo.innerHTML = `
528
+ <div class="flex items-center space-x-2 text-sm text-gray-600">
529
+ <span>Active filters:</span>
530
+ ${activeFilters.map(f => `<span class="bg-gray-100 px-2 py-1 rounded">${escapeHtml(f)}</span>`).join('')}
531
+ <button onclick="resetAuditFilters()" class="text-red-600 hover:text-red-800 text-sm">Reset all</button>
532
+ </div>
533
+ `;
534
+ } else {
535
+ filterInfo.innerHTML = '<span class="text-sm text-gray-500">No active filters</span>';
536
+ }
537
+ }
538
+
539
+ /**
540
+ * Update the event type dropdown
541
+ */
542
+ function updateEventTypeFilter() {
543
+ const select = document.getElementById('filter-event-type');
544
+ if (!select) return;
545
+
546
+ select.innerHTML = '<option value="">All types</option>' +
547
+ auditEventTypes.map(type => `<option value="${escapeHtml(type)}">${escapeHtml(type)}</option>`).join('');
548
+ }
549
+
550
+ /**
551
+ * Update the instance dropdown
552
+ */
553
+ function updateInstanceFilter() {
554
+ const select = document.getElementById('filter-instance');
555
+ if (!select) return;
556
+
557
+ select.innerHTML = '<option value="">All instances</option>' +
558
+ auditInstances.map(inst => `<option value="${escapeHtml(inst.instance_name)}">${escapeHtml(inst.instance_name)} (${escapeHtml(inst.instance_id)})</option>`).join('');
559
+ }
560
+
561
+ // ==================== PAGINATION ====================
562
+
563
+ /**
564
+ * Update pagination
565
+ */
566
+ function updateAuditPagination() {
567
+ const paginationContainer = document.getElementById('audit-pagination');
568
+ if (!paginationContainer) return;
569
+
570
+ if (auditTotalPages <= 1) {
571
+ paginationContainer.innerHTML = '';
572
+ return;
573
+ }
574
+
575
+ let paginationHtml = '<div class="flex justify-center items-center space-x-2 mt-6">';
576
+
577
+ // "Back" button
578
+ paginationHtml += `
579
+ <button onclick="goToAuditPage(${auditCurrentPage - 1})"
580
+ ${auditCurrentPage === 1 ? 'disabled' : ''}
581
+ class="px-3 py-2 border rounded-lg ${auditCurrentPage === 1 ? 'bg-gray-100 text-gray-400' : 'hover:bg-gray-50'}">
582
+ ← Back
583
+ </button>
584
+ `;
585
+
586
+ // Page numbers
587
+ const startPage = Math.max(1, auditCurrentPage - 2);
588
+ const endPage = Math.min(auditTotalPages, auditCurrentPage + 2);
589
+
590
+ for (let i = startPage; i <= endPage; i++) {
591
+ paginationHtml += `
592
+ <button onclick="goToAuditPage(${i})"
593
+ class="px-3 py-2 border rounded-lg ${i === auditCurrentPage ? 'bg-blue-600 text-white' : 'hover:bg-gray-50'}">
594
+ ${i}
595
+ </button>
596
+ `;
597
+ }
598
+
599
+ // "Next" button
600
+ paginationHtml += `
601
+ <button onclick="goToAuditPage(${auditCurrentPage + 1})"
602
+ ${auditCurrentPage === auditTotalPages ? 'disabled' : ''}
603
+ class="px-3 py-2 border rounded-lg ${auditCurrentPage === auditTotalPages ? 'bg-gray-100 text-gray-400' : 'hover:bg-gray-50'}">
604
+ Next →
605
+ </button>
606
+ `;
607
+
608
+ paginationHtml += `<span class="ml-4 text-sm text-gray-500">Total: ${auditTotalEvents} events</span>`;
609
+ paginationHtml += '</div>';
610
+
611
+ paginationContainer.innerHTML = paginationHtml;
612
+ }
613
+
614
+ /**
615
+ * Go to page
616
+ */
617
+ function goToAuditPage(page) {
618
+ if (page < 1 || page > auditTotalPages || page === auditCurrentPage) return;
619
+ loadAuditEvents(page, auditPageSize, false);
620
+ }
621
+
622
+ // ==================== DATA MANAGEMENT ====================
623
+
624
+ /**
625
+ * Clean up old events
626
+ */
627
+ async function cleanupOldEvents() {
628
+ const days = prompt('Delete events older than (days):', '30');
629
+ if (!days) return;
630
+
631
+ if (!confirm(`Are you sure you want to delete all events older than ${days} days? This action cannot be undone.`)) return;
632
+
633
+ try {
634
+ const token = localStorage.getItem('authToken');
635
+ const response = await fetch(`/api/events/cleanup?days=${days}`, {
636
+ method: 'DELETE',
637
+ headers: {
638
+ 'Authorization': `Bearer ${token}`
639
+ }
640
+ });
641
+
642
+ if (response.ok) {
643
+ const result = await response.json();
644
+ showNotification(result.message, 'success');
645
+ refreshAuditEvents();
646
+ loadAuditStats();
647
+ } else {
648
+ const error = await response.json();
649
+ throw new Error(error.detail || 'Cleanup error');
650
+ }
651
+ } catch (error) {
652
+ console.error('Cleanup error:', error);
653
+ showNotification('Cleanup error: ' + error.message, 'error');
654
+ }
655
+ }
656
+
657
+ /**
658
+ * Refresh the event list
659
+ */
660
+ function refreshAuditEvents() {
661
+ loadAuditEvents(1, auditPageSize, false);
662
+ }
663
+
664
+ /**
665
+ * Refresh statistics
666
+ */
667
+ function refreshAuditStats() {
668
+ loadAuditStats();
669
+ }
670
+
671
+ // ==================== DATA EXPORT ====================
672
+
673
+ /**
674
+ * Export events to CSV
675
+ */
676
+ function exportEventsToCSV() {
677
+ if (!auditEvents || auditEvents.length === 0) {
678
+ showNotification('No data to export', 'warning');
679
+ return;
680
+ }
681
+
682
+ const headers = ['ID', 'Event type', 'Text', 'Instance ID', 'Instance', 'Created at', 'Data'];
683
+ const rows = auditEvents.map(event => [
684
+ event.id,
685
+ event.event_type,
686
+ event.event_text,
687
+ event.instance_id,
688
+ event.instance_name || '',
689
+ formatDate(event.created_at, 'datetime'),
690
+ event.event_data ? JSON.stringify(event.event_data) : ''
691
+ ]);
692
+
693
+ const csvContent = [headers, ...rows].map(row => row.map(cell => `"${String(cell).replace(/"/g, '""')}"`).join(',')).join('\n');
694
+ const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' });
695
+ const link = document.createElement('a');
696
+ const url = URL.createObjectURL(blob);
697
+ link.setAttribute('href', url);
698
+ link.setAttribute('download', `audit_events_${new Date().toISOString().slice(0,19)}.csv`);
699
+ link.style.display = 'none';
700
+ document.body.appendChild(link);
701
+ link.click();
702
+ document.body.removeChild(link);
703
+ URL.revokeObjectURL(url);
704
+
705
+ showNotification('Export completed', 'success');
706
+ }
707
+
708
+ // ==================== UI SECTION ====================
709
+
710
+ /**
711
+ * Create the audit section
712
+ */
713
+ function createAuditSection() {
714
+ // Check whether the section already exists
715
+ if (document.getElementById('audit-section')) return;
716
+
717
+ const mainContent = document.querySelector('main');
718
+ if (!mainContent) return;
719
+
720
+ const section = document.createElement('div');
721
+ section.id = 'audit-section';
722
+ section.className = 'content-section';
723
+ section.innerHTML = `
724
+ <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
725
+ <div class="flex justify-between items-center mb-6">
726
+ <h2 class="text-2xl font-bold text-gray-800">Event audit</h2>
727
+ <div class="flex space-x-2">
728
+ <button onclick="exportEventsToCSV()"
729
+ class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
730
+ <i data-feather="download" class="w-4 h-4 mr-2"></i>
731
+ Export CSV
732
+ </button>
733
+ <button onclick="cleanupOldEvents()"
734
+ class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 flex items-center">
735
+ <i data-feather="trash-2" class="w-4 h-4 mr-2"></i>
736
+ Clean up old
737
+ </button>
738
+ <button onclick="refreshAuditEvents()"
739
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
740
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
741
+ Refresh
742
+ </button>
743
+ </div>
744
+ </div>
745
+
746
+ <!-- Statistics -->
747
+ <div id="audit-stats-container" class="mb-6"></div>
748
+
749
+ <!-- Filters -->
750
+ <div class="bg-gray-50 rounded-lg p-4 mb-6">
751
+ <h3 class="font-semibold text-gray-800 mb-3">Filters</h3>
752
+ <form id="audit-filters-form" class="grid grid-cols-1 md:grid-cols-4 gap-4">
753
+ <div>
754
+ <label class="block text-sm text-gray-600 mb-1">Event type</label>
755
+ <select id="filter-event-type" class="w-full border rounded-lg px-3 py-2">
756
+ <option value="">All types</option>
757
+ </select>
758
+ </div>
759
+ <div>
760
+ <label class="block text-sm text-gray-600 mb-1">Instance</label>
761
+ <select id="filter-instance" class="w-full border rounded-lg px-3 py-2">
762
+ <option value="">All instances</option>
763
+ </select>
764
+ </div>
765
+ <div>
766
+ <label class="block text-sm text-gray-600 mb-1">Date from</label>
767
+ <input type="date" id="filter-start-date" class="w-full border rounded-lg px-3 py-2">
768
+ </div>
769
+ <div>
770
+ <label class="block text-sm text-gray-600 mb-1">Date to</label>
771
+ <input type="date" id="filter-end-date" class="w-full border rounded-lg px-3 py-2">
772
+ </div>
773
+ </form>
774
+ <div class="flex justify-between items-center mt-4">
775
+ <div id="filter-info"></div>
776
+ <div class="flex space-x-2">
777
+ <button onclick="applyAuditFilters()"
778
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
779
+ Apply
780
+ </button>
781
+ <button onclick="resetAuditFilters()"
782
+ class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
783
+ Reset
784
+ </button>
785
+ </div>
786
+ </div>
787
+ </div>
788
+
789
+ <!-- Event list -->
790
+ <div id="audit-events-container" class="space-y-3">
791
+ <div class="text-center py-12">
792
+ <i data-feather="loader" class="w-12 h-12 text-gray-400 mx-auto mb-3 animate-spin"></i>
793
+ <p class="text-gray-500">Loading events...</p>
794
+ </div>
795
+ </div>
796
+
797
+ <!-- Pagination -->
798
+ <div id="audit-pagination"></div>
799
+ </div>
800
+ `;
801
+
802
+ mainContent.appendChild(section);
803
+ if (typeof feather !== 'undefined') feather.replace();
804
+ }
805
+
806
+ /**
807
+ * Show the audit section
808
+ */
809
+ function showAuditSection() {
810
+ if (window.appRouter) {
811
+ window.appRouter.navigate('/selfcare/modules/audit');
812
+ }
813
+
814
+ // Hide all sections
815
+ const contentSections = document.querySelectorAll('.content-section');
816
+ contentSections.forEach(section => {
817
+ section.classList.remove('active');
818
+ });
819
+
820
+ // Show the audit section
821
+ const targetSection = document.getElementById('audit-section');
822
+ if (targetSection) {
823
+ targetSection.classList.add('active');
824
+ // Update the title
825
+ const titleEl = document.getElementById('current-section-title');
826
+ if (titleEl) {
827
+ titleEl.textContent = 'Event audit';
828
+ }
829
+ }
830
+
831
+ isAuditRouteActive = true;
832
+ if (typeof feather !== 'undefined') feather.replace();
833
+
834
+ // Load data only if the section was created
835
+ if (document.getElementById('audit-section')) {
836
+ loadAuditStats();
837
+ loadEventTypes();
838
+ loadInstances();
839
+ loadAuditEvents(1, auditPageSize, false);
840
+ }
841
+ }
842
+
843
+ // ==================== NAVIGATION ====================
844
+
845
+ /**
846
+ * Add an item to the sidebar menu
847
+ */
848
+ function addAuditNavigation() {
849
+
850
+ const mainNav = document.getElementById('main-nav');
851
+ if (!mainNav) return;
852
+
853
+ // Check whether the item is already added
854
+ if (document.querySelector('[data-target="audit-section"]')) return;
855
+
856
+ const navItem = document.createElement('a');
857
+ navItem.href = '#';
858
+ navItem.className = 'nav-item flex items-center px-6 py-3 text-blue-100 hover:bg-blue-600';
859
+ navItem.setAttribute('data-target', 'audit-section');
860
+ navItem.innerHTML = `
861
+ <i data-feather="activity" class="w-5 h-5 mr-3"></i>
862
+ <span class="nav-text">Event audit</span>
863
+ `;
864
+
865
+ navItem.addEventListener('click', function(e) {
866
+ e.preventDefault();
867
+ e.stopPropagation();
868
+ showAuditSection();
869
+
870
+ // Update the active menu item
871
+ document.querySelectorAll('.nav-item').forEach(item => {
872
+ item.classList.remove('text-white', 'bg-blue-800');
873
+ item.classList.add('text-blue-100', 'hover:bg-blue-600');
874
+ });
875
+
876
+ this.classList.remove('text-blue-100', 'hover:bg-blue-600');
877
+ this.classList.add('text-white', 'bg-blue-800');
878
+ });
879
+
880
+ // Add to the modules container or the main menu
881
+ const modulesNavContainer = document.getElementById('modules-nav-container');
882
+ if (modulesNavContainer) {
883
+ modulesNavContainer.appendChild(navItem);
884
+ } else {
885
+ mainNav.appendChild(navItem);
886
+ }
887
+
888
+ if (typeof feather !== 'undefined') feather.replace();
889
+ }
890
+
891
+ // ==================== SYSTEM REGISTRATION ====================
892
+
893
+ /**
894
+ * Register routes in the router
895
+ */
896
+ function registerAuditRoutes() {
897
+ if (window.appRouter) {
898
+ window.appRouter.registerRoute('/selfcare/modules/audit', () => {
899
+ console.log('Audit module: Handling route');
900
+ showAuditSection();
901
+ });
902
+ }
903
+ }
904
+
905
+ /**
906
+ * Register sections in the system
907
+ */
908
+ function registerAuditSections() {
909
+ if (window.appSections) {
910
+ window.appSections.registerSection(
911
+ 'audit-section',
912
+ '/selfcare/modules/audit',
913
+ () => {
914
+ console.log('Audit module: Handling section');
915
+ showAuditSection();
916
+ }
917
+ );
918
+ console.log('Audit module: Sections registered');
919
+ }
920
+ }
921
+
922
+ // ==================== INITIALIZATION ====================
923
+
924
+ /**
925
+ * Initialize the module (called from main_new.js)
926
+ */
927
+ function initAuditModule() {
928
+ // Prevent double initialization
929
+ if (window.auditModuleInitialized) {
930
+ console.log('Audit module already initialized');
931
+ return;
932
+ }
933
+
934
+ console.log('Initializing Audit Module');
935
+
936
+ // Create the UI section
937
+ createAuditSection();
938
+
939
+ // Register routes
940
+ registerAuditRoutes();
941
+
942
+ // Register sections
943
+ registerAuditSections();
944
+
945
+ // Add an item to navigation
946
+ addAuditNavigation();
947
+
948
+ window.auditModuleInitialized = true;
949
+ console.log('Audit Module initialized');
950
+ }
951
+
952
+ // ==================== FUNCTION EXPORTS ====================
953
+
954
+ // Make functions globally accessible
955
+ window.auditEvents = auditEvents;
956
+ window.auditIsLoading = auditIsLoading;
957
+ window.auditCurrentPage = auditCurrentPage;
958
+
959
+ window.loadAuditEvents = loadAuditEvents;
960
+ window.refreshAuditEvents = refreshAuditEvents;
961
+ window.loadAuditStats = loadAuditStats;
962
+ window.refreshAuditStats = refreshAuditStats;
963
+ window.applyAuditFilters = applyAuditFilters;
964
+ window.resetAuditFilters = resetAuditFilters;
965
+ window.goToAuditPage = goToAuditPage;
966
+ window.cleanupOldEvents = cleanupOldEvents;
967
+ window.exportEventsToCSV = exportEventsToCSV;
968
+ window.toggleEventData = toggleEventData;
969
+ window.showAuditSection = showAuditSection;
970
+ window.initAuditModule = initAuditModule;
971
+
972
+ document.addEventListener('DOMContentLoaded', function() {
973
+ const checkUserInterval = setInterval(function() {
974
+ if (window.currentUser !== undefined) {
975
+ clearInterval(checkUserInterval);
976
+ initAuditModule();
977
+ }
978
+ }, 100);
979
+ });