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,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, '<')
|
|
50
|
+
.replace(/>/g, '>')
|
|
51
|
+
.replace(/"/g, '"')
|
|
52
|
+
.replace(/'/g, ''')
|
|
53
|
+
.replace(/\//g, '/');
|
|
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
|
+
});
|