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