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.
- keepup_http/__init__.py +38 -0
- keepup_http/api_docs.py +76 -0
- keepup_http/body_limit.py +156 -0
- keepup_http/events_api.py +237 -0
- keepup_http/factory.py +562 -0
- keepup_http/lock_routes.py +155 -0
- keepup_http/plugin.py +53 -0
- keepup_http/routes.py +549 -0
- keepup_http/static/css/aos.css +1 -0
- keepup_http/static/css/main_nebula.css +232 -0
- keepup_http/static/css/main_new.css +869 -0
- keepup_http/static/css/tailwind.css +1 -0
- keepup_http/static/index_nebula.html +282 -0
- keepup_http/static/index_new.html +275 -0
- keepup_http/static/js/aos.js +1 -0
- keepup_http/static/js/feather-icons.js +13 -0
- keepup_http/static/js/layout_bootstrap.js +29 -0
- keepup_http/static/js/main_new.js +2076 -0
- keepup_http/static/js/tailwind.js +83 -0
- keepup_http/static/modules/css/background_tasks.css +233 -0
- keepup_http/static/modules/css/cluster.css +16 -0
- keepup_http/static/modules/css/event_manager.css +386 -0
- keepup_http/static/modules/css/integration_logs.css +33 -0
- keepup_http/static/modules/css/metrics.css +115 -0
- keepup_http/static/modules/css/modules.css +189 -0
- keepup_http/static/modules/css/themes.css +563 -0
- keepup_http/static/modules/css/users.css +278 -0
- keepup_http/static/modules/js/background_tasks.js +695 -0
- keepup_http/static/modules/js/chart.js +14 -0
- keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
- keepup_http/static/modules/js/cluster.js +374 -0
- keepup_http/static/modules/js/event_manager.js +1007 -0
- keepup_http/static/modules/js/integration_logs.js +795 -0
- keepup_http/static/modules/js/metrics.js +929 -0
- keepup_http/static/modules/js/modules.js +1128 -0
- keepup_http/static/modules/js/themes.js +684 -0
- keepup_http/static/modules/js/users.js +898 -0
- keepup_http/web.py +214 -0
- keepup_http-0.5.0.dist-info/METADATA +29 -0
- keepup_http-0.5.0.dist-info/RECORD +43 -0
- keepup_http-0.5.0.dist-info/WHEEL +5 -0
- keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
- keepup_http-0.5.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
// Every value of a log row is escaped: user names, addresses, endpoints and the
|
|
2
|
+
// bodies sent to and received from somebody else's service are whatever those
|
|
3
|
+
// parties put there, and a row rendered as markup ran in the administrator's
|
|
4
|
+
// session (keepup-62).
|
|
5
|
+
function integrationLogsEscape(value) {
|
|
6
|
+
return String(value === undefined || value === null ? '' : value)
|
|
7
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
8
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// static/modules/js/integration_logs.js
|
|
12
|
+
class IntegrationLogsPlugin {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.name = "Integration Log";
|
|
15
|
+
this.currentPage = 1;
|
|
16
|
+
this.limit = 50;
|
|
17
|
+
this.filters = {
|
|
18
|
+
user_id: '',
|
|
19
|
+
username: '',
|
|
20
|
+
host: '',
|
|
21
|
+
start_date: '',
|
|
22
|
+
end_date: ''
|
|
23
|
+
};
|
|
24
|
+
this.initialized = false;
|
|
25
|
+
this.contentCreated = false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async initialize() {
|
|
29
|
+
// Skip if the plugin is already initialized
|
|
30
|
+
if (this.initialized) {
|
|
31
|
+
console.log('Integration logs plugin already initialized');
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
this.addNavigation();
|
|
37
|
+
this.createContentSection();
|
|
38
|
+
this.loadHostOptions();
|
|
39
|
+
this.initialized = true;
|
|
40
|
+
console.log('Integration logs plugin initialized successfully');
|
|
41
|
+
return true;
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Error initializing integration logs plugin:', error);
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
addNavigation() {
|
|
49
|
+
// Skip if the nav item is already there
|
|
50
|
+
if (document.querySelector('[data-target="integration_logs-section"]')) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const mainNav = document.getElementById('main-nav');
|
|
55
|
+
if (!mainNav) {
|
|
56
|
+
console.error('Main navigation not found');
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const navItem = document.createElement('a');
|
|
61
|
+
navItem.href = '#';
|
|
62
|
+
navItem.className = 'nav-item flex items-center px-6 py-3 text-blue-100 hover:bg-blue-600';
|
|
63
|
+
navItem.setAttribute('data-target', 'integration_logs-section');
|
|
64
|
+
navItem.innerHTML = `
|
|
65
|
+
<i data-feather="activity" class="w-5 h-5 mr-3"></i>
|
|
66
|
+
<span class="nav-text">Integration Log</span>
|
|
67
|
+
`;
|
|
68
|
+
|
|
69
|
+
const modulesNavContainer = document.getElementById('modules-nav-container');
|
|
70
|
+
if (modulesNavContainer) {
|
|
71
|
+
modulesNavContainer.appendChild(navItem);
|
|
72
|
+
} else {
|
|
73
|
+
// No modules container -- fall back to the main nav
|
|
74
|
+
const uploadNav = mainNav.querySelector('[data-target="upload-section"]');
|
|
75
|
+
if (uploadNav) {
|
|
76
|
+
uploadNav.parentNode.insertBefore(navItem, uploadNav.nextSibling);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Sync collapsed-state styling
|
|
81
|
+
this.updateNavItemForCollapsedState(navItem,
|
|
82
|
+
document.getElementById('sidebar').classList.contains('sidebar-collapsed'));
|
|
83
|
+
|
|
84
|
+
console.log('Navigation item "Integration Log" added');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
createContentSection() {
|
|
88
|
+
// Skip if the section already exists
|
|
89
|
+
if (document.getElementById('integration_logs-section')) {
|
|
90
|
+
this.contentCreated = true;
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const mainContent = document.querySelector('main');
|
|
95
|
+
if (!mainContent) {
|
|
96
|
+
console.error('Main content not found');
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const contentSection = document.createElement('div');
|
|
101
|
+
contentSection.id = 'integration_logs-section';
|
|
102
|
+
contentSection.className = 'content-section';
|
|
103
|
+
contentSection.innerHTML = this.getContentHTML();
|
|
104
|
+
|
|
105
|
+
mainContent.appendChild(contentSection);
|
|
106
|
+
this.contentCreated = true;
|
|
107
|
+
|
|
108
|
+
// Wire up listeners only for a section we just created
|
|
109
|
+
this.setupEventListeners();
|
|
110
|
+
this.setupSectionObserver();
|
|
111
|
+
|
|
112
|
+
console.log('Content section "Integration Log" created');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
getContentHTML() {
|
|
116
|
+
return `
|
|
117
|
+
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
|
|
118
|
+
<div class="flex justify-between items-center mb-6">
|
|
119
|
+
<h2 class="text-2xl font-bold text-gray-800">Integration Log</h2>
|
|
120
|
+
<div class="flex space-x-2">
|
|
121
|
+
<button data-action="logs.stats"
|
|
122
|
+
class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
|
|
123
|
+
<i data-feather="bar-chart-2" class="w-4 h-4 mr-2"></i>
|
|
124
|
+
Statistics
|
|
125
|
+
</button>
|
|
126
|
+
<button data-action="logs.export"
|
|
127
|
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
|
|
128
|
+
<i data-feather="download" class="w-4 h-4 mr-2"></i>
|
|
129
|
+
Export
|
|
130
|
+
</button>
|
|
131
|
+
${currentUser && currentUser.role === 'ADMIN' ? `
|
|
132
|
+
<button data-action="logs.cleanup"
|
|
133
|
+
class="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 flex items-center">
|
|
134
|
+
<i data-feather="trash-2" class="w-4 h-4 mr-2"></i>
|
|
135
|
+
Cleanup
|
|
136
|
+
</button>
|
|
137
|
+
` : ''}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Filters -->
|
|
142
|
+
<div class="bg-gray-50 rounded-lg p-4 mb-6">
|
|
143
|
+
<h3 class="text-lg font-semibold text-gray-800 mb-4">Filters</h3>
|
|
144
|
+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-4">
|
|
145
|
+
<div>
|
|
146
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">User ID</label>
|
|
147
|
+
<input type="number" id="filter-user-id"
|
|
148
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
149
|
+
</div>
|
|
150
|
+
<div>
|
|
151
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
|
152
|
+
<input type="text" id="filter-username"
|
|
153
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Host</label>
|
|
157
|
+
<select id="filter-host" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
158
|
+
<option value="">All hosts</option>
|
|
159
|
+
</select>
|
|
160
|
+
</div>
|
|
161
|
+
<div>
|
|
162
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Limit</label>
|
|
163
|
+
<select id="filter-limit" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
164
|
+
<option value="50">50 records</option>
|
|
165
|
+
<option value="100">100 records</option>
|
|
166
|
+
<option value="200">200 records</option>
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
171
|
+
<div>
|
|
172
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Start date</label>
|
|
173
|
+
<input type="datetime-local" id="filter-start-date"
|
|
174
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
175
|
+
</div>
|
|
176
|
+
<div>
|
|
177
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">End date</label>
|
|
178
|
+
<input type="datetime-local" id="filter-end-date"
|
|
179
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
<div class="flex justify-between">
|
|
183
|
+
<button data-action="logs.apply-filters"
|
|
184
|
+
class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
|
|
185
|
+
<i data-feather="filter" class="w-4 h-4 mr-2"></i>
|
|
186
|
+
Apply filters
|
|
187
|
+
</button>
|
|
188
|
+
<button data-action="logs.reset-filters"
|
|
189
|
+
class="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 flex items-center">
|
|
190
|
+
<i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>
|
|
191
|
+
Reset
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
|
|
196
|
+
<!-- Statistics -->
|
|
197
|
+
<div id="stats-container" class="hidden mb-6">
|
|
198
|
+
<!-- Dynamically loaded statistics -->
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<!-- Log table -->
|
|
202
|
+
<div class="bg-white border border-gray-200 rounded-lg overflow-hidden">
|
|
203
|
+
<div id="logs-container">
|
|
204
|
+
<div class="text-center py-8">
|
|
205
|
+
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600 mx-auto mb-2"></div>
|
|
206
|
+
<p class="text-gray-500">Loading log...</p>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<!-- Pagination -->
|
|
212
|
+
<div id="pagination-container" class="mt-4 flex justify-between items-center">
|
|
213
|
+
<!-- Dynamic pagination -->
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<!-- Detail modal -->
|
|
218
|
+
<div id="log-detail-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden p-4">
|
|
219
|
+
<div class="bg-white rounded-lg p-6 w-full max-w-4xl max-h-[90vh] overflow-y-auto">
|
|
220
|
+
<div class="flex justify-between items-center mb-4">
|
|
221
|
+
<h3 class="text-xl font-bold text-gray-800">Request details</h3>
|
|
222
|
+
<button data-action="logs.close-detail"
|
|
223
|
+
class="text-gray-500 hover:text-gray-700">
|
|
224
|
+
<i data-feather="x" class="w-6 h-6"></i>
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
<div id="log-detail-content">
|
|
228
|
+
<!-- Log details -->
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
setupEventListeners() {
|
|
236
|
+
// Set default date range
|
|
237
|
+
const now = new Date();
|
|
238
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
239
|
+
|
|
240
|
+
const startDateElem = document.getElementById('filter-start-date');
|
|
241
|
+
const endDateElem = document.getElementById('filter-end-date');
|
|
242
|
+
const limitElem = document.getElementById('filter-limit');
|
|
243
|
+
|
|
244
|
+
if (startDateElem) startDateElem.value = weekAgo.toISOString().slice(0, 16);
|
|
245
|
+
if (endDateElem) endDateElem.value = now.toISOString().slice(0, 16);
|
|
246
|
+
if (limitElem) {
|
|
247
|
+
limitElem.value = '50';
|
|
248
|
+
this.limit = 50;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
console.log('Integration logs event listeners setup');
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
setupSectionObserver() {
|
|
255
|
+
const section = document.getElementById('integration_logs-section');
|
|
256
|
+
if (!section) {
|
|
257
|
+
console.error('Integration logs section not found for observer');
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const observer = new MutationObserver((mutations) => {
|
|
262
|
+
mutations.forEach((mutation) => {
|
|
263
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
|
264
|
+
if (section.classList.contains('active')) {
|
|
265
|
+
console.log('Integration logs section activated, loading logs...');
|
|
266
|
+
this.loadLogs();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
observer.observe(section, {
|
|
273
|
+
attributes: true,
|
|
274
|
+
attributeFilter: ['class']
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
console.log('Integration logs section observer setup');
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async loadLogs(page = 1) {
|
|
281
|
+
console.log(`Loading integration logs, page: ${page}`);
|
|
282
|
+
this.currentPage = page;
|
|
283
|
+
const offset = (page - 1) * this.limit;
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
const token = localStorage.getItem('authToken');
|
|
287
|
+
const params = new URLSearchParams({
|
|
288
|
+
limit: this.limit.toString(),
|
|
289
|
+
offset: offset.toString(),
|
|
290
|
+
...this.filters
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Drop empty params
|
|
294
|
+
for (const [key, value] of Object.entries(this.filters)) {
|
|
295
|
+
if (value) {
|
|
296
|
+
params.append(key, value);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const response = await fetch(`/api/integration-logs?${params}`, {
|
|
301
|
+
headers: {
|
|
302
|
+
'Authorization': `Bearer ${token}`
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
if (response.ok) {
|
|
307
|
+
const data = await response.json();
|
|
308
|
+
this.displayLogs(data);
|
|
309
|
+
console.log(`Integration logs loaded successfully, total: ${data.total_count}`);
|
|
310
|
+
} else {
|
|
311
|
+
console.error('Error loading integration logs:', response.status);
|
|
312
|
+
this.displayError('Error loading log');
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
console.error('Error loading integration logs:', error);
|
|
316
|
+
this.displayError('Network error');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
displayLogs(data) {
|
|
321
|
+
const container = document.getElementById('logs-container');
|
|
322
|
+
if (!container) {
|
|
323
|
+
console.error('Logs container not found');
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (!data.success || !data.logs || data.logs.length === 0) {
|
|
328
|
+
container.innerHTML = `
|
|
329
|
+
<div class="text-center py-8 text-gray-500">
|
|
330
|
+
<i data-feather="inbox" class="w-12 h-12 mx-auto mb-2"></i>
|
|
331
|
+
<p>No records found</p>
|
|
332
|
+
</div>
|
|
333
|
+
`;
|
|
334
|
+
if (typeof feather !== 'undefined') {
|
|
335
|
+
feather.replace();
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
container.innerHTML = `
|
|
341
|
+
<table class="responsive-table min-w-full divide-y divide-gray-200">
|
|
342
|
+
<thead class="bg-gray-50">
|
|
343
|
+
<tr>
|
|
344
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Date</th>
|
|
345
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">User</th>
|
|
346
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Host</th>
|
|
347
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
|
|
348
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
|
349
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Duration</th>
|
|
350
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Actions</th>
|
|
351
|
+
</tr>
|
|
352
|
+
</thead>
|
|
353
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
354
|
+
${data.logs.map(log => `
|
|
355
|
+
<tr class="hover:bg-gray-50">
|
|
356
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
357
|
+
${new Date(log.created_at).toLocaleString()}
|
|
358
|
+
</td>
|
|
359
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
360
|
+
<div>${integrationLogsEscape(log.username)}</div>
|
|
361
|
+
<div class="text-gray-500 text-xs">ID: ${integrationLogsEscape(log.user_id)}</div>
|
|
362
|
+
</td>
|
|
363
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${integrationLogsEscape(log.host)}</td>
|
|
364
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">${integrationLogsEscape(log.method)}</td>
|
|
365
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
366
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
|
367
|
+
log.status_code >= 400 ? 'bg-red-100 text-red-800' :
|
|
368
|
+
log.status_code >= 200 ? 'bg-green-100 text-green-800' :
|
|
369
|
+
'bg-yellow-100 text-yellow-800'
|
|
370
|
+
}">
|
|
371
|
+
${integrationLogsEscape(log.status_code || 'N/A')}
|
|
372
|
+
</span>
|
|
373
|
+
</td>
|
|
374
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
|
375
|
+
${log.duration_ms ? `${log.duration_ms}ms` : 'N/A'}
|
|
376
|
+
</td>
|
|
377
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm">
|
|
378
|
+
<button data-action="logs.show-detail"
|
|
379
|
+
data-log-id="${keepupEscapeHtml(log.id)}"
|
|
380
|
+
class="text-blue-600 hover:text-blue-900 bg-blue-100 px-3 py-1 rounded text-sm flex items-center">
|
|
381
|
+
<i data-feather="eye" class="w-3 h-3 mr-1"></i>
|
|
382
|
+
Details
|
|
383
|
+
</button>
|
|
384
|
+
</td>
|
|
385
|
+
</tr>
|
|
386
|
+
`).join('')}
|
|
387
|
+
</tbody>
|
|
388
|
+
</table>
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
this.updatePagination(data.total_count);
|
|
392
|
+
if (typeof feather !== 'undefined') {
|
|
393
|
+
feather.replace();
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
updatePagination(totalCount) {
|
|
398
|
+
const container = document.getElementById('pagination-container');
|
|
399
|
+
if (!container) return;
|
|
400
|
+
|
|
401
|
+
const totalPages = Math.ceil(totalCount / this.limit);
|
|
402
|
+
|
|
403
|
+
if (totalPages <= 1) {
|
|
404
|
+
container.innerHTML = `
|
|
405
|
+
<div class="text-sm text-gray-700">
|
|
406
|
+
Showing ${totalCount} records
|
|
407
|
+
</div>
|
|
408
|
+
`;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
container.innerHTML = `
|
|
413
|
+
<div class="text-sm text-gray-700">
|
|
414
|
+
Showing ${((this.currentPage - 1) * this.limit) + 1}-${Math.min(this.currentPage * this.limit, totalCount)} of ${totalCount} records
|
|
415
|
+
</div>
|
|
416
|
+
<div class="flex space-x-2">
|
|
417
|
+
<button data-action="logs.page"
|
|
418
|
+
data-page="${keepupEscapeHtml(this.currentPage - 1)}"
|
|
419
|
+
${this.currentPage <= 1 ? 'disabled' : ''}
|
|
420
|
+
class="px-3 py-1 border border-gray-300 rounded text-sm ${this.currentPage <= 1 ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}">
|
|
421
|
+
Back
|
|
422
|
+
</button>
|
|
423
|
+
<span class="px-3 py-1 text-sm text-gray-700">
|
|
424
|
+
Page ${this.currentPage} of ${totalPages}
|
|
425
|
+
</span>
|
|
426
|
+
<button data-action="logs.page"
|
|
427
|
+
data-page="${keepupEscapeHtml(this.currentPage + 1)}"
|
|
428
|
+
${this.currentPage >= totalPages ? 'disabled' : ''}
|
|
429
|
+
class="px-3 py-1 border border-gray-300 rounded text-sm ${this.currentPage >= totalPages ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50'}">
|
|
430
|
+
Next
|
|
431
|
+
</button>
|
|
432
|
+
</div>
|
|
433
|
+
`;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
async applyFilters() {
|
|
437
|
+
console.log('Applying filters...');
|
|
438
|
+
this.filters = {
|
|
439
|
+
user_id: document.getElementById('filter-user-id')?.value || '',
|
|
440
|
+
username: document.getElementById('filter-username')?.value || '',
|
|
441
|
+
host: document.getElementById('filter-host')?.value || '',
|
|
442
|
+
start_date: document.getElementById('filter-start-date')?.value || '',
|
|
443
|
+
end_date: document.getElementById('filter-end-date')?.value || ''
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
const limitElem = document.getElementById('filter-limit');
|
|
447
|
+
if (limitElem) {
|
|
448
|
+
this.limit = parseInt(limitElem.value) || 50;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
this.currentPage = 1;
|
|
452
|
+
|
|
453
|
+
await this.loadLogs();
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
resetFilters() {
|
|
457
|
+
console.log('Resetting filters...');
|
|
458
|
+
const useridElem = document.getElementById('filter-user-id');
|
|
459
|
+
const usernameElem = document.getElementById('filter-username');
|
|
460
|
+
const hostElem = document.getElementById('filter-host');
|
|
461
|
+
const startDateElem = document.getElementById('filter-start-date');
|
|
462
|
+
const endDateElem = document.getElementById('filter-end-date');
|
|
463
|
+
const limitElem = document.getElementById('filter-limit');
|
|
464
|
+
|
|
465
|
+
if (useridElem) useridElem.value = '';
|
|
466
|
+
if (usernameElem) usernameElem.value = '';
|
|
467
|
+
if (hostElem) hostElem.value = '';
|
|
468
|
+
|
|
469
|
+
const now = new Date();
|
|
470
|
+
const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
471
|
+
|
|
472
|
+
if (startDateElem) startDateElem.value = weekAgo.toISOString().slice(0, 16);
|
|
473
|
+
if (endDateElem) endDateElem.value = now.toISOString().slice(0, 16);
|
|
474
|
+
if (limitElem) limitElem.value = '50';
|
|
475
|
+
|
|
476
|
+
this.filters = {};
|
|
477
|
+
this.limit = 50;
|
|
478
|
+
this.currentPage = 1;
|
|
479
|
+
|
|
480
|
+
this.loadLogs();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async showLogDetail(logId) {
|
|
484
|
+
console.log(`Showing log detail for ID: ${logId}`);
|
|
485
|
+
try {
|
|
486
|
+
const token = localStorage.getItem('authToken');
|
|
487
|
+
const response = await fetch(`/api/integration-logs/${logId}`, {
|
|
488
|
+
headers: {
|
|
489
|
+
'Authorization': `Bearer ${token}`
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
if (response.ok) {
|
|
494
|
+
const data = await response.json();
|
|
495
|
+
if (data.success) {
|
|
496
|
+
this.displayLogDetail(data.log);
|
|
497
|
+
} else {
|
|
498
|
+
this.displayError(data.error);
|
|
499
|
+
}
|
|
500
|
+
} else {
|
|
501
|
+
this.displayError('Error loading details');
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error('Error loading log detail:', error);
|
|
505
|
+
this.displayError('Network error');
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
displayLogDetail(log) {
|
|
510
|
+
const modal = document.getElementById('log-detail-modal');
|
|
511
|
+
const content = document.getElementById('log-detail-content');
|
|
512
|
+
|
|
513
|
+
if (!modal || !content) {
|
|
514
|
+
console.error('Log detail modal elements not found');
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
content.innerHTML = `
|
|
519
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
520
|
+
<div>
|
|
521
|
+
<strong>ID:</strong> ${integrationLogsEscape(log.id)}
|
|
522
|
+
</div>
|
|
523
|
+
<div>
|
|
524
|
+
<strong>Date:</strong> ${new Date(log.created_at).toLocaleString()}
|
|
525
|
+
</div>
|
|
526
|
+
<div>
|
|
527
|
+
<strong>User:</strong> ${integrationLogsEscape(log.username)} (ID: ${integrationLogsEscape(log.user_id)})
|
|
528
|
+
</div>
|
|
529
|
+
<div>
|
|
530
|
+
<strong>Host:</strong> ${integrationLogsEscape(log.host)}
|
|
531
|
+
</div>
|
|
532
|
+
<div>
|
|
533
|
+
<strong>Method:</strong> ${integrationLogsEscape(log.method)}
|
|
534
|
+
</div>
|
|
535
|
+
<div>
|
|
536
|
+
<strong>Endpoint:</strong> ${integrationLogsEscape(log.endpoint)}
|
|
537
|
+
</div>
|
|
538
|
+
<div>
|
|
539
|
+
<strong>Status:</strong>
|
|
540
|
+
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full ${
|
|
541
|
+
log.status_code >= 400 ? 'bg-red-100 text-red-800' :
|
|
542
|
+
log.status_code >= 200 ? 'bg-green-100 text-green-800' :
|
|
543
|
+
'bg-yellow-100 text-yellow-800'
|
|
544
|
+
}">
|
|
545
|
+
${integrationLogsEscape(log.status_code || 'N/A')}
|
|
546
|
+
</span>
|
|
547
|
+
</div>
|
|
548
|
+
<div>
|
|
549
|
+
<strong>Duration:</strong> ${log.duration_ms ? `${log.duration_ms}ms` : 'N/A'}
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
|
|
553
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
554
|
+
<div>
|
|
555
|
+
<h4 class="font-semibold mb-2">Request body:</h4>
|
|
556
|
+
<pre class="bg-gray-100 p-3 rounded text-sm overflow-auto max-h-60">${integrationLogsEscape(this.formatJson(log.request_body))}</pre>
|
|
557
|
+
</div>
|
|
558
|
+
<div>
|
|
559
|
+
<h4 class="font-semibold mb-2">Response body:</h4>
|
|
560
|
+
<pre class="bg-gray-100 p-3 rounded text-sm overflow-auto max-h-60">${integrationLogsEscape(this.formatJson(log.response_body))}</pre>
|
|
561
|
+
</div>
|
|
562
|
+
</div>
|
|
563
|
+
`;
|
|
564
|
+
|
|
565
|
+
modal.classList.remove('hidden');
|
|
566
|
+
if (typeof feather !== 'undefined') {
|
|
567
|
+
feather.replace();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
formatJson(jsonString) {
|
|
572
|
+
if (!jsonString) return 'No data';
|
|
573
|
+
|
|
574
|
+
try {
|
|
575
|
+
const parsed = JSON.parse(jsonString);
|
|
576
|
+
return JSON.stringify(parsed, null, 2);
|
|
577
|
+
} catch {
|
|
578
|
+
return jsonString;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
closeDetailModal() {
|
|
583
|
+
const modal = document.getElementById('log-detail-modal');
|
|
584
|
+
if (modal) {
|
|
585
|
+
modal.classList.add('hidden');
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
async loadHostOptions() {
|
|
590
|
+
// The host list comes from the log, not hardcoded markup. It used to
|
|
591
|
+
// list four integrations of one specific application -- elsewhere the
|
|
592
|
+
// filter offered hosts that never show up in the log, and missed ones
|
|
593
|
+
// that do. The framework does not know other integrations' names, and should not.
|
|
594
|
+
const select = document.getElementById('filter-host');
|
|
595
|
+
if (!select) return;
|
|
596
|
+
try {
|
|
597
|
+
const token = localStorage.getItem('authToken');
|
|
598
|
+
const response = await fetch('/api/integration-logs/stats?days=30', {
|
|
599
|
+
headers: { 'Authorization': `Bearer ${token}` }
|
|
600
|
+
});
|
|
601
|
+
if (!response.ok) return;
|
|
602
|
+
const data = await response.json();
|
|
603
|
+
const hosts = [...new Set((data.host_stats || []).map(stat => stat.host).filter(Boolean))].sort();
|
|
604
|
+
const chosen = select.value;
|
|
605
|
+
for (const host of hosts) {
|
|
606
|
+
const option = document.createElement('option');
|
|
607
|
+
option.value = host;
|
|
608
|
+
option.textContent = host;
|
|
609
|
+
select.appendChild(option);
|
|
610
|
+
}
|
|
611
|
+
select.value = chosen;
|
|
612
|
+
} catch (error) {
|
|
613
|
+
// A filter with no host list still means "all hosts" -- the section still works.
|
|
614
|
+
console.warn('Could not read the hosts for the filter:', error);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
async loadStats() {
|
|
619
|
+
console.log('Loading statistics...');
|
|
620
|
+
try {
|
|
621
|
+
const token = localStorage.getItem('authToken');
|
|
622
|
+
const response = await fetch('/api/integration-logs/stats?days=7', {
|
|
623
|
+
headers: {
|
|
624
|
+
'Authorization': `Bearer ${token}`
|
|
625
|
+
}
|
|
626
|
+
});
|
|
627
|
+
|
|
628
|
+
if (response.ok) {
|
|
629
|
+
const data = await response.json();
|
|
630
|
+
this.displayStats(data);
|
|
631
|
+
} else {
|
|
632
|
+
this.displayError('Error loading statistics');
|
|
633
|
+
}
|
|
634
|
+
} catch (error) {
|
|
635
|
+
console.error('Error loading stats:', error);
|
|
636
|
+
this.displayError('Network error');
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
displayStats(data) {
|
|
641
|
+
const container = document.getElementById('stats-container');
|
|
642
|
+
if (!container) return;
|
|
643
|
+
|
|
644
|
+
if (!data.success) {
|
|
645
|
+
container.innerHTML = `<div class="text-red-600">${integrationLogsEscape(data.error)}</div>`;
|
|
646
|
+
container.classList.remove('hidden');
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
container.innerHTML = `
|
|
651
|
+
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
|
652
|
+
<h3 class="text-lg font-semibold text-blue-800 mb-4">Statistics for the last ${data.period_days} days</h3>
|
|
653
|
+
|
|
654
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
655
|
+
<div class="text-center">
|
|
656
|
+
<div class="text-2xl font-bold text-blue-600">${data.total_requests}</div>
|
|
657
|
+
<div class="text-sm text-blue-800">Total requests</div>
|
|
658
|
+
</div>
|
|
659
|
+
<div class="text-center">
|
|
660
|
+
<div class="text-2xl font-bold text-green-600">${data.overall_avg_duration.toFixed(0)}ms</div>
|
|
661
|
+
<div class="text-sm text-green-800">Average time</div>
|
|
662
|
+
</div>
|
|
663
|
+
<div class="text-center">
|
|
664
|
+
<div class="text-2xl font-bold text-red-600">${data.total_errors}</div>
|
|
665
|
+
<div class="text-sm text-red-800">Errors</div>
|
|
666
|
+
</div>
|
|
667
|
+
<div class="text-center">
|
|
668
|
+
<div class="text-2xl font-bold ${data.error_rate > 5 ? 'text-red-600' : 'text-yellow-600'}">${data.error_rate.toFixed(1)}%</div>
|
|
669
|
+
Error rate
|
|
670
|
+
</div>
|
|
671
|
+
</div>
|
|
672
|
+
|
|
673
|
+
<h4 class="font-semibold text-blue-800 mb-2">Statistics by host:</h4>
|
|
674
|
+
<div class="space-y-2">
|
|
675
|
+
${(data.host_stats || []).map(stat => `
|
|
676
|
+
<div class="flex justify-between items-center bg-white p-2 rounded border">
|
|
677
|
+
<div>
|
|
678
|
+
<span class="font-medium">${integrationLogsEscape(stat.host)}</span>
|
|
679
|
+
<span class="text-gray-500 text-sm ml-2">(${integrationLogsEscape(stat.method)})</span>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="flex space-x-4 text-sm">
|
|
682
|
+
<span>Requests: ${stat.request_count}</span>
|
|
683
|
+
<span>Time: ${stat.avg_duration ? stat.avg_duration.toFixed(0) + 'ms' : 'N/A'}</span>
|
|
684
|
+
<span class="${stat.error_count > 0 ? 'text-red-600' : 'text-green-600'}">
|
|
685
|
+
Errors: ${stat.error_count}
|
|
686
|
+
</span>
|
|
687
|
+
</div>
|
|
688
|
+
</div>
|
|
689
|
+
`).join('')}
|
|
690
|
+
</div>
|
|
691
|
+
</div>
|
|
692
|
+
`;
|
|
693
|
+
|
|
694
|
+
container.classList.remove('hidden');
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
async exportLogs() {
|
|
698
|
+
console.log('Exporting logs...');
|
|
699
|
+
this.displayMessage('Export is not implemented yet', 'info');
|
|
700
|
+
// Export implementation to be added later
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
async cleanupLogs() {
|
|
704
|
+
if (!confirm('Are you sure you want to clear logs older than 30 days?')) {
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
console.log('Cleaning up logs...');
|
|
709
|
+
try {
|
|
710
|
+
const token = localStorage.getItem('authToken');
|
|
711
|
+
const response = await fetch('/api/integration-logs/cleanup', {
|
|
712
|
+
method: 'POST',
|
|
713
|
+
headers: {
|
|
714
|
+
'Authorization': `Bearer ${token}`
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
if (response.ok) {
|
|
719
|
+
const data = await response.json();
|
|
720
|
+
if (data.success) {
|
|
721
|
+
this.displayMessage(data.message, 'success');
|
|
722
|
+
this.loadLogs();
|
|
723
|
+
} else {
|
|
724
|
+
this.displayError(data.error);
|
|
725
|
+
}
|
|
726
|
+
} else {
|
|
727
|
+
this.displayError('Error clearing logs');
|
|
728
|
+
}
|
|
729
|
+
} catch (error) {
|
|
730
|
+
console.error('Error cleaning up logs:', error);
|
|
731
|
+
this.displayError('Network error');
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
displayError(message) {
|
|
736
|
+
if (typeof showNotification === 'function') {
|
|
737
|
+
showNotification(message, 'error');
|
|
738
|
+
} else {
|
|
739
|
+
alert(`Error: ${message}`);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
displayMessage(message, type = 'info') {
|
|
744
|
+
if (typeof showNotification === 'function') {
|
|
745
|
+
showNotification(message, type);
|
|
746
|
+
} else {
|
|
747
|
+
alert(message);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
updateNavItemForCollapsedState(navItem, isCollapsed) {
|
|
752
|
+
const navText = navItem.querySelector('.nav-text');
|
|
753
|
+
const icon = navItem.querySelector('i');
|
|
754
|
+
|
|
755
|
+
if (isCollapsed) {
|
|
756
|
+
if (navText) navText.style.display = 'none';
|
|
757
|
+
navItem.classList.add('justify-center', 'px-3');
|
|
758
|
+
if (icon) icon.classList.remove('mr-3');
|
|
759
|
+
} else {
|
|
760
|
+
if (navText) navText.style.display = 'inline';
|
|
761
|
+
navItem.classList.remove('justify-center', 'px-3');
|
|
762
|
+
if (icon) icon.classList.add('mr-3');
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// Initialize the plugin only if it is not already initialized
|
|
768
|
+
if (!window.integrationLogsPlugin) {
|
|
769
|
+
window.integrationLogsPlugin = new IntegrationLogsPlugin();
|
|
770
|
+
|
|
771
|
+
function initIntegrationLogs() {
|
|
772
|
+
return window.integrationLogsPlugin.initialize();
|
|
773
|
+
}
|
|
774
|
+
} else {
|
|
775
|
+
console.log('Integration logs plugin already exists');
|
|
776
|
+
|
|
777
|
+
function initIntegrationLogs() {
|
|
778
|
+
return Promise.resolve(true);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// The actions this section's markup asks the shell to take (keepup-93). The log
|
|
783
|
+
// rows and the pagination are rebuilt on every load, so they name an action and
|
|
784
|
+
// carry the id or the page they were built with; every handler is resolved
|
|
785
|
+
// against the plugin instance, whose methods rely on `this`.
|
|
786
|
+
KeepupActions.register({
|
|
787
|
+
'logs.stats': () => window.integrationLogsPlugin.loadStats(),
|
|
788
|
+
'logs.export': () => window.integrationLogsPlugin.exportLogs(),
|
|
789
|
+
'logs.cleanup': () => window.integrationLogsPlugin.cleanupLogs(),
|
|
790
|
+
'logs.apply-filters': () => window.integrationLogsPlugin.applyFilters(),
|
|
791
|
+
'logs.reset-filters': () => window.integrationLogsPlugin.resetFilters(),
|
|
792
|
+
'logs.close-detail': () => window.integrationLogsPlugin.closeDetailModal(),
|
|
793
|
+
'logs.show-detail': (element) => window.integrationLogsPlugin.showLogDetail(Number(element.dataset.logId)),
|
|
794
|
+
'logs.page': (element) => window.integrationLogsPlugin.loadLogs(Number(element.dataset.page)),
|
|
795
|
+
});
|