django-cfg 1.4.58__py3-none-any.whl → 1.4.60__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/ipc/RPC_LOGGING.md +321 -0
- django_cfg/apps/ipc/TESTING.md +539 -0
- django_cfg/apps/ipc/__init__.py +12 -3
- django_cfg/apps/ipc/admin.py +212 -0
- django_cfg/apps/ipc/migrations/0001_initial.py +137 -0
- django_cfg/apps/ipc/migrations/__init__.py +0 -0
- django_cfg/apps/ipc/models.py +221 -0
- django_cfg/apps/ipc/serializers/__init__.py +10 -0
- django_cfg/apps/ipc/serializers/serializers.py +114 -0
- django_cfg/apps/ipc/services/client/client.py +83 -4
- django_cfg/apps/ipc/services/logging.py +239 -0
- django_cfg/apps/ipc/services/monitor.py +5 -3
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/main.mjs +269 -0
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/overview.mjs +259 -0
- django_cfg/apps/ipc/static/django_cfg_ipc/js/dashboard/testing.mjs +375 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/methods_content.html +22 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/notifications_content.html +9 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/overview_content.html +9 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/requests_content.html +23 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/stat_cards.html +50 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/system_status.html +47 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/tab_navigation.html +29 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/components/testing_tools.html +184 -0
- django_cfg/apps/ipc/templates/django_cfg_ipc/pages/dashboard.html +56 -0
- django_cfg/apps/ipc/urls.py +4 -2
- django_cfg/apps/ipc/views/__init__.py +7 -2
- django_cfg/apps/ipc/views/dashboard.py +1 -1
- django_cfg/apps/ipc/views/{viewsets.py → monitoring.py} +17 -11
- django_cfg/apps/ipc/views/testing.py +285 -0
- django_cfg/core/backends/__init__.py +1 -0
- django_cfg/core/backends/smtp.py +69 -0
- django_cfg/middleware/authentication.py +157 -0
- django_cfg/models/api/drf/config.py +2 -2
- django_cfg/models/services/email.py +11 -1
- django_cfg/modules/django_client/system/generate_mjs_clients.py +1 -1
- django_cfg/modules/django_dashboard/sections/widgets.py +209 -0
- django_cfg/modules/django_unfold/callbacks/main.py +43 -18
- django_cfg/modules/django_unfold/dashboard.py +41 -4
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/js/api/index.mjs +8 -3
- django_cfg/static/js/api/ipc/client.mjs +40 -0
- django_cfg/static/js/api/knowbase/client.mjs +309 -0
- django_cfg/static/js/api/knowbase/index.mjs +13 -0
- django_cfg/static/js/api/payments/client.mjs +46 -1215
- django_cfg/static/js/api/types.mjs +164 -337
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +13 -1
- django_cfg/templates/admin/sections/widgets_section.html +129 -0
- django_cfg/templates/admin/snippets/tabs/widgets_tab.html +38 -0
- django_cfg/utils/smart_defaults.py +1 -1
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/METADATA +1 -1
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/RECORD +58 -31
- django_cfg/apps/ipc/templates/django_cfg_ipc/dashboard.html +0 -202
- /django_cfg/apps/ipc/static/django_cfg_ipc/js/{dashboard.mjs → dashboard.mjs.old} +0 -0
- /django_cfg/apps/ipc/templates/django_cfg_ipc/{base.html → layout/base.html} +0 -0
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.58.dist-info → django_cfg-1.4.60.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main RPC Dashboard Controller
|
|
3
|
+
* Orchestrates all dashboard modules and handles tab navigation
|
|
4
|
+
*/
|
|
5
|
+
import { OverviewModule } from './overview.mjs';
|
|
6
|
+
import { TestingModule } from './testing.mjs';
|
|
7
|
+
|
|
8
|
+
class RPCDashboard {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.api = window.ipcAPI;
|
|
11
|
+
this.currentTab = 'overview';
|
|
12
|
+
this.autoRefresh = true;
|
|
13
|
+
this.refreshInterval = null;
|
|
14
|
+
this.refreshRate = 5000; // 5 seconds
|
|
15
|
+
|
|
16
|
+
// Initialize modules
|
|
17
|
+
this.overviewModule = new OverviewModule(this.api, this);
|
|
18
|
+
this.testingModule = new TestingModule(this.api, this);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize dashboard
|
|
23
|
+
*/
|
|
24
|
+
init() {
|
|
25
|
+
console.log('🚀 RPC Dashboard initializing...');
|
|
26
|
+
this.setupEventListeners();
|
|
27
|
+
this.testingModule.init();
|
|
28
|
+
this.loadInitialData();
|
|
29
|
+
this.startAutoRefresh();
|
|
30
|
+
console.log('✅ RPC Dashboard initialized');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Setup event listeners
|
|
35
|
+
*/
|
|
36
|
+
setupEventListeners() {
|
|
37
|
+
// Tab buttons
|
|
38
|
+
const tabButtons = document.querySelectorAll('.tab-button');
|
|
39
|
+
tabButtons.forEach(button => {
|
|
40
|
+
button.addEventListener('click', (e) => {
|
|
41
|
+
// Use currentTarget to get the button element, not the clicked child element
|
|
42
|
+
const tabName = e.currentTarget.dataset.tab;
|
|
43
|
+
this.switchTab(tabName);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Auto-refresh toggle
|
|
48
|
+
const autoRefreshToggle = document.getElementById('auto-refresh-toggle');
|
|
49
|
+
if (autoRefreshToggle) {
|
|
50
|
+
autoRefreshToggle.checked = this.autoRefresh;
|
|
51
|
+
autoRefreshToggle.addEventListener('change', (e) => {
|
|
52
|
+
this.autoRefresh = e.target.checked;
|
|
53
|
+
if (this.autoRefresh) {
|
|
54
|
+
this.startAutoRefresh();
|
|
55
|
+
} else {
|
|
56
|
+
this.stopAutoRefresh();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Switch tabs
|
|
64
|
+
*/
|
|
65
|
+
switchTab(tabName) {
|
|
66
|
+
console.log('Switching to tab:', tabName);
|
|
67
|
+
|
|
68
|
+
// Update tab buttons styling
|
|
69
|
+
document.querySelectorAll('.tab-button').forEach(btn => {
|
|
70
|
+
if (btn.dataset.tab === tabName) {
|
|
71
|
+
btn.classList.add('active', 'text-blue-600', 'dark:text-blue-400', 'border-blue-600', 'dark:border-blue-400');
|
|
72
|
+
btn.classList.remove('text-gray-600', 'dark:text-gray-400', 'border-transparent');
|
|
73
|
+
} else {
|
|
74
|
+
btn.classList.remove('active', 'text-blue-600', 'dark:text-blue-400', 'border-blue-600', 'dark:border-blue-400');
|
|
75
|
+
btn.classList.add('text-gray-600', 'dark:text-gray-400', 'border-transparent');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Update tab panels
|
|
80
|
+
document.querySelectorAll('.tab-panel').forEach(panel => {
|
|
81
|
+
panel.classList.add('hidden');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const activePanel = document.getElementById(`${tabName}-tab`);
|
|
85
|
+
if (activePanel) {
|
|
86
|
+
activePanel.classList.remove('hidden');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
this.currentTab = tabName;
|
|
90
|
+
this.loadTabData(tabName);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load initial data
|
|
95
|
+
*/
|
|
96
|
+
async loadInitialData() {
|
|
97
|
+
console.log('Loading initial data...');
|
|
98
|
+
await this.loadHealthStatus();
|
|
99
|
+
await this.loadTabData(this.currentTab);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Load data for specific tab
|
|
104
|
+
*/
|
|
105
|
+
async loadTabData(tabName) {
|
|
106
|
+
console.log('Loading data for tab:', tabName);
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
// Load overview stats for stat cards (always visible)
|
|
110
|
+
await this.overviewModule.loadOverviewStats();
|
|
111
|
+
|
|
112
|
+
// Load tab-specific data
|
|
113
|
+
if (tabName === 'testing') {
|
|
114
|
+
// Testing tab doesn't need data loading, it's interactive
|
|
115
|
+
} else {
|
|
116
|
+
// Overview, requests, notifications, methods tabs
|
|
117
|
+
await this.overviewModule.loadData(tabName);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
this.updateLastUpdate();
|
|
121
|
+
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error loading tab data:', error);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Load health status
|
|
129
|
+
*/
|
|
130
|
+
async loadHealthStatus() {
|
|
131
|
+
try {
|
|
132
|
+
const health = await this.api.ipcAdminApiMonitorHealthRetrieve();
|
|
133
|
+
|
|
134
|
+
if (health) {
|
|
135
|
+
// Update health indicator
|
|
136
|
+
const indicator = document.getElementById('health-indicator');
|
|
137
|
+
if (indicator) {
|
|
138
|
+
const isHealthy = health.redis_connected && health.stream_exists;
|
|
139
|
+
indicator.innerHTML = `
|
|
140
|
+
<span class="pulse-dot w-2 h-2 ${isHealthy ? 'bg-green-500' : 'bg-red-500'} rounded-full"></span>
|
|
141
|
+
<span class="text-gray-700 dark:text-gray-300">${isHealthy ? 'Connected' : 'Disconnected'}</span>
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Update system status section
|
|
146
|
+
this.updateSystemStatus(health);
|
|
147
|
+
}
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Error loading health status:', error);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Update system status section
|
|
155
|
+
*/
|
|
156
|
+
updateSystemStatus(health) {
|
|
157
|
+
const statusContainer = document.getElementById('system-status');
|
|
158
|
+
if (!statusContainer) return;
|
|
159
|
+
|
|
160
|
+
statusContainer.innerHTML = `
|
|
161
|
+
<!-- Redis Status -->
|
|
162
|
+
<div class="flex items-start gap-3">
|
|
163
|
+
<span class="material-icons flex-shrink-0 text-2xl ${health.redis_connected ? 'text-green-500' : 'text-red-500'}">
|
|
164
|
+
${health.redis_connected ? 'check_circle' : 'cancel'}
|
|
165
|
+
</span>
|
|
166
|
+
<div class="min-w-0">
|
|
167
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Redis</p>
|
|
168
|
+
<p class="text-xs text-gray-600 dark:text-gray-400">
|
|
169
|
+
${health.redis_connected ? 'Connected (DB 2)' : 'Disconnected'}
|
|
170
|
+
</p>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<!-- Stream Status -->
|
|
175
|
+
<div class="flex items-start gap-3">
|
|
176
|
+
<span class="material-icons flex-shrink-0 text-2xl ${health.stream_exists ? 'text-green-500' : 'text-gray-500'}">
|
|
177
|
+
${health.stream_exists ? 'stream' : 'stream_off'}
|
|
178
|
+
</span>
|
|
179
|
+
<div class="min-w-0">
|
|
180
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Request Stream</p>
|
|
181
|
+
<p class="text-xs text-gray-600 dark:text-gray-400">
|
|
182
|
+
${health.stream_exists ? `${health.stream_length} entries` : 'Not initialized'}
|
|
183
|
+
</p>
|
|
184
|
+
</div>
|
|
185
|
+
</div>
|
|
186
|
+
|
|
187
|
+
<!-- Activity Status -->
|
|
188
|
+
<div class="flex items-start gap-3">
|
|
189
|
+
<span class="material-icons flex-shrink-0 text-2xl ${health.recent_activity ? 'text-green-500' : 'text-yellow-500'}">
|
|
190
|
+
${health.recent_activity ? 'notifications_active' : 'notifications_paused'}
|
|
191
|
+
</span>
|
|
192
|
+
<div class="min-w-0">
|
|
193
|
+
<p class="text-sm font-medium text-gray-900 dark:text-white">Recent Activity</p>
|
|
194
|
+
<p class="text-xs text-gray-600 dark:text-gray-400">
|
|
195
|
+
${health.recent_activity ? 'Active (last 5 min)' : 'No recent activity'}
|
|
196
|
+
</p>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
`;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Update last update timestamp
|
|
204
|
+
*/
|
|
205
|
+
updateLastUpdate() {
|
|
206
|
+
const element = document.getElementById('last-update');
|
|
207
|
+
if (element) {
|
|
208
|
+
const now = new Date();
|
|
209
|
+
element.textContent = `Last updated: ${now.toLocaleTimeString()}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Start auto-refresh
|
|
215
|
+
*/
|
|
216
|
+
startAutoRefresh() {
|
|
217
|
+
if (this.refreshInterval) {
|
|
218
|
+
clearInterval(this.refreshInterval);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
this.refreshInterval = setInterval(() => {
|
|
222
|
+
if (this.autoRefresh) {
|
|
223
|
+
this.loadHealthStatus();
|
|
224
|
+
this.loadTabData(this.currentTab);
|
|
225
|
+
}
|
|
226
|
+
}, this.refreshRate);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Stop auto-refresh
|
|
231
|
+
*/
|
|
232
|
+
stopAutoRefresh() {
|
|
233
|
+
if (this.refreshInterval) {
|
|
234
|
+
clearInterval(this.refreshInterval);
|
|
235
|
+
this.refreshInterval = null;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Wait for both DOM and API to be ready
|
|
242
|
+
*/
|
|
243
|
+
async function initializeDashboard() {
|
|
244
|
+
console.log('DOM loaded, waiting for IpcAPI...');
|
|
245
|
+
|
|
246
|
+
// Wait for API to be available (max 5 seconds)
|
|
247
|
+
let attempts = 0;
|
|
248
|
+
const maxAttempts = 100; // 100 * 50ms = 5 seconds
|
|
249
|
+
|
|
250
|
+
while (!window.ipcAPI && attempts < maxAttempts) {
|
|
251
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
252
|
+
attempts++;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (!window.ipcAPI) {
|
|
256
|
+
console.error('❌ IpcAPI failed to load after 5 seconds');
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log('✅ IpcAPI ready, initializing dashboard...');
|
|
261
|
+
window.rpcDashboard = new RPCDashboard();
|
|
262
|
+
window.rpcDashboard.init();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Initialize dashboard when DOM is ready
|
|
266
|
+
document.addEventListener('DOMContentLoaded', initializeDashboard);
|
|
267
|
+
|
|
268
|
+
// Export for debugging
|
|
269
|
+
export default RPCDashboard;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RPC Overview Dashboard Module
|
|
3
|
+
* Handles overview, requests, notifications, and methods tabs
|
|
4
|
+
*/
|
|
5
|
+
export class OverviewModule {
|
|
6
|
+
constructor(api, dashboard) {
|
|
7
|
+
this.api = api;
|
|
8
|
+
this.dashboard = dashboard;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load data based on current tab
|
|
13
|
+
*/
|
|
14
|
+
async loadData(tabName) {
|
|
15
|
+
switch (tabName) {
|
|
16
|
+
case 'overview':
|
|
17
|
+
await this.loadOverviewStats();
|
|
18
|
+
break;
|
|
19
|
+
case 'requests':
|
|
20
|
+
await this.loadRecentRequests();
|
|
21
|
+
break;
|
|
22
|
+
case 'notifications':
|
|
23
|
+
await this.loadNotificationStats();
|
|
24
|
+
break;
|
|
25
|
+
case 'methods':
|
|
26
|
+
await this.loadMethodStats();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Load overview statistics
|
|
33
|
+
*/
|
|
34
|
+
async loadOverviewStats() {
|
|
35
|
+
try {
|
|
36
|
+
const stats = await this.api.ipcAdminApiMonitorOverviewRetrieve();
|
|
37
|
+
|
|
38
|
+
if (stats) {
|
|
39
|
+
// Update stats cards
|
|
40
|
+
this.updateElement('total-requests', stats.total_requests_today || 0);
|
|
41
|
+
this.updateElement('active-methods-count', stats.active_methods?.length || 0);
|
|
42
|
+
this.updateElement('avg-response-time', (stats.avg_response_time_ms || 0).toFixed(0));
|
|
43
|
+
this.updateElement('success-rate', (stats.success_rate || 0).toFixed(1));
|
|
44
|
+
|
|
45
|
+
// Update top method (XSS-safe)
|
|
46
|
+
if (stats.top_method) {
|
|
47
|
+
const topMethodElement = document.getElementById('top-method');
|
|
48
|
+
if (topMethodElement) {
|
|
49
|
+
const code = document.createElement('code');
|
|
50
|
+
code.className = 'bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded text-blue-600 dark:text-blue-300';
|
|
51
|
+
code.textContent = stats.top_method;
|
|
52
|
+
topMethodElement.innerHTML = '';
|
|
53
|
+
topMethodElement.appendChild(code);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('Error loading overview stats:', error);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load recent RPC requests
|
|
64
|
+
*/
|
|
65
|
+
async loadRecentRequests() {
|
|
66
|
+
try {
|
|
67
|
+
const data = await this.api.ipcAdminApiMonitorRequestsRetrieve({ count: 50 });
|
|
68
|
+
|
|
69
|
+
if (data) {
|
|
70
|
+
const requests = data.requests || [];
|
|
71
|
+
this.renderRequestsTable(requests);
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('Error loading recent requests:', error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Render requests table
|
|
80
|
+
*/
|
|
81
|
+
renderRequestsTable(requests) {
|
|
82
|
+
const tbody = document.getElementById('requests-table-body');
|
|
83
|
+
if (!tbody) return;
|
|
84
|
+
|
|
85
|
+
if (requests.length === 0) {
|
|
86
|
+
tbody.innerHTML = `
|
|
87
|
+
<tr>
|
|
88
|
+
<td colspan="4" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
|
89
|
+
No recent requests found
|
|
90
|
+
</td>
|
|
91
|
+
</tr>
|
|
92
|
+
`;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
tbody.innerHTML = requests.map(req => `
|
|
97
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
98
|
+
<td class="px-4 py-3 text-sm">
|
|
99
|
+
${this.formatTimestamp(req.timestamp)}
|
|
100
|
+
</td>
|
|
101
|
+
<td class="px-4 py-3">
|
|
102
|
+
<code class="text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">${this.escapeHtml(req.method || 'unknown')}</code>
|
|
103
|
+
</td>
|
|
104
|
+
<td class="px-4 py-3 text-sm font-mono">
|
|
105
|
+
${this.escapeHtml((req.correlation_id || '').substring(0, 8))}...
|
|
106
|
+
</td>
|
|
107
|
+
<td class="px-4 py-3 text-sm">
|
|
108
|
+
<details class="cursor-pointer">
|
|
109
|
+
<summary class="text-blue-600 dark:text-blue-400 hover:underline">View</summary>
|
|
110
|
+
<pre class="mt-2 text-xs bg-gray-100 dark:bg-gray-900 p-2 rounded overflow-auto max-h-40">${this.escapeHtml(JSON.stringify(req.params || {}, null, 2))}</pre>
|
|
111
|
+
</details>
|
|
112
|
+
</td>
|
|
113
|
+
</tr>
|
|
114
|
+
`).join('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load notification statistics
|
|
119
|
+
*/
|
|
120
|
+
async loadNotificationStats() {
|
|
121
|
+
try {
|
|
122
|
+
const stats = await this.api.ipcAdminApiMonitorNotificationsRetrieve();
|
|
123
|
+
|
|
124
|
+
if (stats) {
|
|
125
|
+
this.renderNotificationStats(stats);
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
console.error('Error loading notification stats:', error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render notification statistics
|
|
134
|
+
*/
|
|
135
|
+
renderNotificationStats(stats) {
|
|
136
|
+
const container = document.getElementById('notification-stats-content');
|
|
137
|
+
if (!container) return;
|
|
138
|
+
|
|
139
|
+
const byType = stats.by_type || {};
|
|
140
|
+
const total = stats.total_sent || 0;
|
|
141
|
+
|
|
142
|
+
container.innerHTML = `
|
|
143
|
+
<div class="space-y-4">
|
|
144
|
+
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded">
|
|
145
|
+
<span>Total Sent</span>
|
|
146
|
+
<span class="text-2xl font-bold">${total}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700 rounded">
|
|
149
|
+
<span>Delivery Rate</span>
|
|
150
|
+
<span class="text-2xl font-bold text-green-600 dark:text-green-400">${stats.delivery_rate || 0}%</span>
|
|
151
|
+
</div>
|
|
152
|
+
<div>
|
|
153
|
+
<h4 class="text-md font-semibold mb-3">By Type</h4>
|
|
154
|
+
<div class="space-y-2">
|
|
155
|
+
${Object.entries(byType).map(([type, count]) => `
|
|
156
|
+
<div class="flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
|
157
|
+
<span>${this.escapeHtml(type)}</span>
|
|
158
|
+
<span class="font-medium">${count}</span>
|
|
159
|
+
</div>
|
|
160
|
+
`).join('') || '<p class="text-gray-500 dark:text-gray-400">No data available</p>'}
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
${stats.recent && stats.recent.length > 0 ? `
|
|
164
|
+
<div>
|
|
165
|
+
<h4 class="text-md font-semibold mb-3">Recent Notifications</h4>
|
|
166
|
+
<div class="space-y-2">
|
|
167
|
+
${stats.recent.slice(0, 5).map(notif => `
|
|
168
|
+
<div class="p-3 bg-gray-50 dark:bg-gray-700 rounded">
|
|
169
|
+
<div class="flex justify-between items-start">
|
|
170
|
+
<span class="text-sm">${this.escapeHtml(notif.type || 'unknown')}</span>
|
|
171
|
+
<span class="text-xs text-gray-500 dark:text-gray-400">${this.formatTimestamp(notif.timestamp)}</span>
|
|
172
|
+
</div>
|
|
173
|
+
${notif.message ? `<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">${this.escapeHtml(notif.message)}</p>` : ''}
|
|
174
|
+
</div>
|
|
175
|
+
`).join('')}
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
` : ''}
|
|
179
|
+
</div>
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Load method statistics
|
|
185
|
+
*/
|
|
186
|
+
async loadMethodStats() {
|
|
187
|
+
try {
|
|
188
|
+
const data = await this.api.ipcAdminApiMonitorMethodsRetrieve();
|
|
189
|
+
|
|
190
|
+
if (data) {
|
|
191
|
+
const methods = data.methods || [];
|
|
192
|
+
this.renderMethodsTable(methods);
|
|
193
|
+
}
|
|
194
|
+
} catch (error) {
|
|
195
|
+
console.error('Error loading method stats:', error);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Render methods table
|
|
201
|
+
*/
|
|
202
|
+
renderMethodsTable(methods) {
|
|
203
|
+
const tbody = document.getElementById('methods-table-body');
|
|
204
|
+
if (!tbody) return;
|
|
205
|
+
|
|
206
|
+
if (methods.length === 0) {
|
|
207
|
+
tbody.innerHTML = `
|
|
208
|
+
<tr>
|
|
209
|
+
<td colspan="4" class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
|
|
210
|
+
No method data available
|
|
211
|
+
</td>
|
|
212
|
+
</tr>
|
|
213
|
+
`;
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
tbody.innerHTML = methods.map(method => `
|
|
218
|
+
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
|
|
219
|
+
<td class="px-4 py-3">
|
|
220
|
+
<code class="text-sm bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">${this.escapeHtml(method.method)}</code>
|
|
221
|
+
</td>
|
|
222
|
+
<td class="px-4 py-3 text-sm font-medium">${method.count}</td>
|
|
223
|
+
<td class="px-4 py-3 text-sm">${method.percentage}%</td>
|
|
224
|
+
<td class="px-4 py-3 text-sm">${method.avg_time_ms || method.avg_time || 0}ms</td>
|
|
225
|
+
</tr>
|
|
226
|
+
`).join('');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Helper: Update element text content
|
|
231
|
+
*/
|
|
232
|
+
updateElement(id, value) {
|
|
233
|
+
const element = document.getElementById(id);
|
|
234
|
+
if (element) {
|
|
235
|
+
element.textContent = value;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Helper: Format ISO timestamp to local time
|
|
241
|
+
*/
|
|
242
|
+
formatTimestamp(isoString) {
|
|
243
|
+
try {
|
|
244
|
+
const date = new Date(isoString);
|
|
245
|
+
return date.toLocaleTimeString();
|
|
246
|
+
} catch {
|
|
247
|
+
return 'N/A';
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Helper: Escape HTML to prevent XSS
|
|
253
|
+
*/
|
|
254
|
+
escapeHtml(unsafe) {
|
|
255
|
+
const div = document.createElement('div');
|
|
256
|
+
div.textContent = unsafe;
|
|
257
|
+
return div.innerHTML;
|
|
258
|
+
}
|
|
259
|
+
}
|