keepup-admin 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. keepup/THIRD-PARTY.md +44 -0
  2. keepup/__init__.py +41 -0
  3. keepup/api_versions.py +100 -0
  4. keepup/audit.py +499 -0
  5. keepup/auth/__init__.py +7 -0
  6. keepup/auth/config.py +128 -0
  7. keepup/auth/dependencies.py +485 -0
  8. keepup/auth/dto/__init__.py +0 -0
  9. keepup/auth/dto/token.py +31 -0
  10. keepup/auth/factory.py +28 -0
  11. keepup/auth/login_throttle.py +151 -0
  12. keepup/auth/oidc.py +245 -0
  13. keepup/auth/oidc_policy.py +111 -0
  14. keepup/auth/oidc_routes.py +288 -0
  15. keepup/auth/panel_session.py +220 -0
  16. keepup/auth/permissions.py +59 -0
  17. keepup/auth/providers/__init__.py +0 -0
  18. keepup/auth/providers/base.py +168 -0
  19. keepup/auth/providers/local.py +180 -0
  20. keepup/auth/routes.py +660 -0
  21. keepup/auth/seed_accounts.py +322 -0
  22. keepup/auth/session_lifetime.py +104 -0
  23. keepup/auth/signing_key.py +138 -0
  24. keepup/auth/websocket.py +86 -0
  25. keepup/cluster.py +634 -0
  26. keepup/db.py +663 -0
  27. keepup/events.py +764 -0
  28. keepup/factory.py +484 -0
  29. keepup/instance.py +46 -0
  30. keepup/integrations.py +260 -0
  31. keepup/locks.py +412 -0
  32. keepup/logging_setup.py +690 -0
  33. keepup/metrics.py +818 -0
  34. keepup/metrics_retention.py +376 -0
  35. keepup/modules.py +572 -0
  36. keepup/notification_bus.py +355 -0
  37. keepup/plugins/__init__.py +9 -0
  38. keepup/plugins/admin.py +246 -0
  39. keepup/plugins/base.py +234 -0
  40. keepup/plugins/enablement.py +184 -0
  41. keepup/plugins/registry.py +171 -0
  42. keepup/plugins/route_mask.py +338 -0
  43. keepup/plugins/routes.py +376 -0
  44. keepup/roles.py +17 -0
  45. keepup/scheduler.py +93 -0
  46. keepup/schema.py +295 -0
  47. keepup/sections.json +104 -0
  48. keepup/settings.py +184 -0
  49. keepup/static/css/aos.css +1 -0
  50. keepup/static/css/main_nebula.css +232 -0
  51. keepup/static/css/main_new.css +852 -0
  52. keepup/static/css/tailwind.css +1 -0
  53. keepup/static/index_nebula.html +293 -0
  54. keepup/static/index_new.html +286 -0
  55. keepup/static/js/aos.js +1 -0
  56. keepup/static/js/feather-icons.js +13 -0
  57. keepup/static/js/main_new.js +1861 -0
  58. keepup/static/js/tailwind.js +83 -0
  59. keepup/static/modules/css/background_tasks.css +233 -0
  60. keepup/static/modules/css/cluster.css +16 -0
  61. keepup/static/modules/css/event_manager.css +386 -0
  62. keepup/static/modules/css/integration_logs.css +33 -0
  63. keepup/static/modules/css/metrics.css +115 -0
  64. keepup/static/modules/css/modules.css +189 -0
  65. keepup/static/modules/css/themes.css +563 -0
  66. keepup/static/modules/css/users.css +278 -0
  67. keepup/static/modules/js/background_tasks.js +657 -0
  68. keepup/static/modules/js/chart.js +14 -0
  69. keepup/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
  70. keepup/static/modules/js/cluster.js +363 -0
  71. keepup/static/modules/js/event_manager.js +979 -0
  72. keepup/static/modules/js/integration_logs.js +767 -0
  73. keepup/static/modules/js/metrics.js +908 -0
  74. keepup/static/modules/js/modules.js +1086 -0
  75. keepup/static/modules/js/themes.js +653 -0
  76. keepup/static/modules/js/users.js +784 -0
  77. keepup/tables.py +302 -0
  78. keepup/themes.py +496 -0
  79. keepup/web.py +182 -0
  80. keepup_admin-0.1.0.dist-info/METADATA +117 -0
  81. keepup_admin-0.1.0.dist-info/RECORD +86 -0
  82. keepup_admin-0.1.0.dist-info/WHEEL +5 -0
  83. keepup_admin-0.1.0.dist-info/licenses/LICENSE +202 -0
  84. keepup_admin-0.1.0.dist-info/licenses/NOTICE +22 -0
  85. keepup_admin-0.1.0.dist-info/licenses/THIRD-PARTY.md +44 -0
  86. keepup_admin-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,653 @@
1
+ var themesData = [];
2
+ var themesCurrentPage = 1;
3
+ var themesTotalPages = 0;
4
+ var themesPageSize = 20;
5
+ var themesIsLoading = false;
6
+ var themesHasMore = false;
7
+ var currentActiveThemeId = null;
8
+
9
+ // Cache for themes
10
+ var themesCache = {
11
+ data: null,
12
+ timestamp: null,
13
+ TTL: 2 * 60 * 1000,
14
+ set: function(data) {
15
+ this.data = data;
16
+ this.timestamp = Date.now();
17
+ },
18
+ get: function() {
19
+ if (!this.timestamp || (Date.now() - this.timestamp > this.TTL)) {
20
+ this.clear();
21
+ return null;
22
+ }
23
+ return this.data;
24
+ },
25
+ clear: function() {
26
+ this.data = null;
27
+ this.timestamp = null;
28
+ }
29
+ };
30
+
31
+ // Helper functions
32
+ function escapeHtml(str) {
33
+ if (typeof str !== 'string') return str || '';
34
+ return str.replace(/&/g, '&')
35
+ .replace(/</g, '&lt;')
36
+ .replace(/>/g, '&gt;')
37
+ .replace(/"/g, '&quot;')
38
+ .replace(/'/g, '&#039;')
39
+ .replace(/\//g, '&#x2F;');
40
+ }
41
+
42
+ function escapeCssSelector(id) {
43
+ if (typeof id !== 'string') return id || '';
44
+ return id.replace(/[!"#$%&'()*+,.\/:;<=>?@[\\\]^`{|}~]/g, '\\$&');
45
+ }
46
+
47
+ function formatDate(date, format = 'datetime') {
48
+ if (!date) return '—';
49
+ const d = new Date(date);
50
+ if (isNaN(d.getTime())) return '—';
51
+ const options = {
52
+ date: { year: 'numeric', month: 'long', day: 'numeric' },
53
+ datetime: { year: 'numeric', month: 'long', day: 'numeric', hour: '2-digit', minute: '2-digit' },
54
+ time: { hour: '2-digit', minute: '2-digit', second: '2-digit' }
55
+ };
56
+ return d.toLocaleString('en-GB', options[format] || options.datetime);
57
+ }
58
+
59
+ // Main theme functions
60
+ async function loadThemes(page = 1, pageSize = 20, append = false) {
61
+ if (themesIsLoading) return;
62
+
63
+ try {
64
+ themesIsLoading = true;
65
+ const token = localStorage.getItem('authToken');
66
+
67
+ const response = await fetch(`/themes`, {
68
+ method: 'GET',
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ 'Authorization': `Bearer ${token}`
72
+ }
73
+ });
74
+
75
+ if (response.ok) {
76
+ const result = await response.json();
77
+ const items = result.themes || [];
78
+ themesData = items;
79
+
80
+ // Save the active theme's ID
81
+ const activeTheme = result.active_theme;
82
+ if (activeTheme && activeTheme.id) {
83
+ currentActiveThemeId = activeTheme.id;
84
+ }
85
+
86
+ displayThemes(themesData);
87
+ showNotification(`Loaded ${themesData.length} theme(s)`, 'success');
88
+ } else {
89
+ const error = await response.json();
90
+ throw new Error(error.detail || 'Failed to load themes');
91
+ }
92
+ } catch (error) {
93
+ showNotification('Load failed: ' + error.message, 'error');
94
+ displayThemesError(error.message);
95
+ } finally {
96
+ themesIsLoading = false;
97
+ }
98
+ }
99
+
100
+ function displayThemes(themes) {
101
+ const container = document.getElementById('themes-container');
102
+ if (!container) return;
103
+
104
+ if (!themes || themes.length === 0) {
105
+ container.innerHTML = `
106
+ <div class="text-center py-12">
107
+ <i data-feather="layout" class="w-12 h-12 text-gray-400 mx-auto mb-3"></i>
108
+ <p class="text-gray-500">No themes created yet</p>
109
+ <button onclick="showCreateThemeModal()"
110
+ class="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
111
+ Create first theme
112
+ </button>
113
+ </div>
114
+ `;
115
+ feather.replace();
116
+ return;
117
+ }
118
+
119
+ container.innerHTML = '';
120
+ themes.forEach(theme => {
121
+ const themeElement = createThemeElement(theme);
122
+ container.appendChild(themeElement);
123
+ });
124
+ feather.replace();
125
+ }
126
+
127
+ function createThemeElement(theme) {
128
+ const id = theme.id;
129
+ const isActive = theme.is_active === true || theme.is_active === 1;
130
+ const escapedId = escapeCssSelector(id);
131
+
132
+ const div = document.createElement('div');
133
+ div.className = `theme-item bg-white border rounded-lg p-6 hover:shadow-md transition-shadow ${isActive ? 'border-blue-400 ring-2 ring-blue-200' : ''}`;
134
+ div.setAttribute('data-theme-id', escapedId);
135
+
136
+ div.innerHTML = `
137
+ <div class="flex justify-between items-start mb-4">
138
+ <div class="flex-1">
139
+ <h3 class="font-semibold text-xl text-gray-800">${escapeHtml(theme.theme_name || 'Untitled')}</h3>
140
+ <p class="text-sm text-gray-500 mt-1">ID: ${escapeHtml(theme.id)}</p>
141
+ </div>
142
+ <div class="flex items-center space-x-2">
143
+ ${isActive ?
144
+ '<span class="px-3 py-1 text-xs rounded-full bg-green-100 text-green-800 flex items-center"><i data-feather="check-circle" class="w-3 h-3 mr-1"></i>Active</span>' :
145
+ '<span class="px-3 py-1 text-xs rounded-full bg-gray-100 text-gray-600">Inactive</span>'
146
+ }
147
+ </div>
148
+ </div>
149
+
150
+ <div class="space-y-2 mb-4">
151
+ <div class="flex items-center text-sm">
152
+ <i data-feather="file-text" class="w-4 h-4 text-gray-400 mr-2"></i>
153
+ <span class="text-gray-600">Main page file:</span>
154
+ <code class="ml-2 px-2 py-1 bg-gray-100 rounded text-sm">${escapeHtml(theme.main_page_file || 'index_new.html')}</code>
155
+ </div>
156
+ ${theme.created_at ? `
157
+ <div class="flex items-center text-sm">
158
+ <i data-feather="calendar" class="w-4 h-4 text-gray-400 mr-2"></i>
159
+ <span class="text-gray-500">Created: ${formatDate(theme.created_at, 'date')}</span>
160
+ </div>
161
+ ` : ''}
162
+ </div>
163
+
164
+ <div class="flex flex-wrap gap-2 mt-4 pt-4 border-t">
165
+ ${!isActive ? `
166
+ <button onclick="activateTheme(${id})"
167
+ class="theme-action-btn bg-green-600 hover:bg-green-700 text-white px-3 py-2 rounded text-sm flex items-center">
168
+ <i data-feather="check-circle" class="w-4 h-4 mr-1"></i>Activate
169
+ </button>
170
+ ` : ''}
171
+ <button onclick="showEditThemeModal(${id})"
172
+ class="theme-action-btn bg-blue-600 hover:bg-blue-700 text-white px-3 py-2 rounded text-sm flex items-center">
173
+ <i data-feather="edit-2" class="w-4 h-4 mr-1"></i>Edit
174
+ </button>
175
+ ${!isActive ? `
176
+ <button onclick="deleteTheme(${id}, '${escapeHtml(theme.theme_name)}')"
177
+ class="theme-action-btn bg-red-600 hover:bg-red-700 text-white px-3 py-2 rounded text-sm flex items-center">
178
+ <i data-feather="trash-2" class="w-4 h-4 mr-1"></i>Delete
179
+ </button>
180
+ ` : ''}
181
+ </div>
182
+ `;
183
+
184
+ return div;
185
+ }
186
+
187
+ function displayThemesError(message) {
188
+ const container = document.getElementById('themes-container');
189
+ if (!container) return;
190
+
191
+ container.innerHTML = `
192
+ <div class="col-span-full text-center py-12">
193
+ <i data-feather="alert-triangle" class="w-12 h-12 text-red-400 mx-auto mb-3"></i>
194
+ <p class="text-red-500 mb-4">${escapeHtml(message)}</p>
195
+ <button onclick="refreshThemes()"
196
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
197
+ Try again
198
+ </button>
199
+ </div>
200
+ `;
201
+ feather.replace();
202
+ }
203
+
204
+ function refreshThemes() {
205
+ loadThemes(1, themesPageSize, false);
206
+ }
207
+
208
+ // CRUD operations
209
+ async function activateTheme(themeId) {
210
+ if (!confirm('Are you sure you want to activate this theme? Other themes will be deactivated.')) return;
211
+
212
+ try {
213
+ themesIsLoading = true;
214
+ const token = localStorage.getItem('authToken');
215
+
216
+ const response = await fetch(`/themes/${themeId}/activate`, {
217
+ method: 'POST',
218
+ headers: {
219
+ 'Content-Type': 'application/json',
220
+ 'Authorization': `Bearer ${token}`
221
+ }
222
+ });
223
+
224
+ if (response.ok) {
225
+ const result = await response.json();
226
+ showNotification(result.message || 'Theme activated successfully', 'success');
227
+ refreshThemes();
228
+ } else {
229
+ const error = await response.json();
230
+ throw new Error(error.detail || 'Failed to activate theme');
231
+ }
232
+ } catch (error) {
233
+ showNotification('Error: ' + error.message, 'error');
234
+ } finally {
235
+ themesIsLoading = false;
236
+ }
237
+ }
238
+
239
+ async function createTheme(themeName, mainPageFile, isActive) {
240
+ try {
241
+ themesIsLoading = true;
242
+ const token = localStorage.getItem('authToken');
243
+
244
+ const response = await fetch(`/themes/create?theme_name=${encodeURIComponent(themeName)}&main_page_file=${encodeURIComponent(mainPageFile)}&is_active=${isActive}`, {
245
+ method: 'POST',
246
+ headers: {
247
+ 'Authorization': `Bearer ${token}`
248
+ }
249
+ });
250
+
251
+ if (response.ok) {
252
+ const result = await response.json();
253
+ showNotification(result.message || 'Theme created successfully', 'success');
254
+ refreshThemes();
255
+ closeThemeModal();
256
+ return true;
257
+ } else {
258
+ const error = await response.json();
259
+ throw new Error(error.detail || 'Failed to create theme');
260
+ }
261
+ } catch (error) {
262
+ showNotification('Error: ' + error.message, 'error');
263
+ return false;
264
+ } finally {
265
+ themesIsLoading = false;
266
+ }
267
+ }
268
+
269
+ async function updateTheme(themeId, themeName, mainPageFile, isActive) {
270
+ try {
271
+ themesIsLoading = true;
272
+ const token = localStorage.getItem('authToken');
273
+
274
+ // First delete the old one, then create a new one with the same ID (if supported)
275
+ // The current API implementation has no direct update, so we'd use a combination
276
+ showNotification('Theme update is not yet implemented', 'info');
277
+ return false;
278
+
279
+ } catch (error) {
280
+ showNotification('Error: ' + error.message, 'error');
281
+ return false;
282
+ } finally {
283
+ themesIsLoading = false;
284
+ }
285
+ }
286
+
287
+ async function deleteTheme(themeId, themeName) {
288
+ if (!confirm(`Are you sure you want to delete the theme "${themeName}"? This action cannot be undone.`)) return;
289
+
290
+ try {
291
+ themesIsLoading = true;
292
+ const token = localStorage.getItem('authToken');
293
+
294
+ const response = await fetch(`/themes/${themeId}`, {
295
+ method: 'DELETE',
296
+ headers: {
297
+ 'Authorization': `Bearer ${token}`
298
+ }
299
+ });
300
+
301
+ if (response.ok) {
302
+ const result = await response.json();
303
+ showNotification(result.message || 'Theme deleted successfully', 'success');
304
+ refreshThemes();
305
+ } else {
306
+ const error = await response.json();
307
+ throw new Error(error.detail || 'Failed to delete theme');
308
+ }
309
+ } catch (error) {
310
+ showNotification('Error: ' + error.message, 'error');
311
+ } finally {
312
+ themesIsLoading = false;
313
+ }
314
+ }
315
+
316
+ // Modal windows
317
+ function showCreateThemeModal() {
318
+ const existingModal = document.getElementById('theme-modal');
319
+ if (existingModal) existingModal.remove();
320
+
321
+ const modal = document.createElement('div');
322
+ modal.id = 'theme-modal';
323
+ modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
324
+ modal.innerHTML = `
325
+ <div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
326
+ <div class="flex justify-between items-center p-6 border-b">
327
+ <h3 class="text-xl font-bold text-gray-800">Create new theme</h3>
328
+ <button onclick="closeThemeModal()" class="text-gray-500 hover:text-gray-700 p-2">
329
+ <i data-feather="x" class="w-6 h-6"></i>
330
+ </button>
331
+ </div>
332
+
333
+ <div class="p-6">
334
+ <form id="theme-form" onsubmit="handleThemeFormSubmit(event)">
335
+ <div class="space-y-4">
336
+ <div>
337
+ <label class="block text-sm font-medium text-gray-700 mb-1">
338
+ Theme name <span class="text-red-500">*</span>
339
+ </label>
340
+ <input type="text" id="theme-name" required
341
+ placeholder="e.g. dark, light, blue"
342
+ class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
343
+ </div>
344
+
345
+ <div>
346
+ <label class="block text-sm font-medium text-gray-700 mb-1">
347
+ Main page file <span class="text-red-500">*</span>
348
+ </label>
349
+ <input type="text" id="theme-file" required
350
+ placeholder="e.g. index_new.html"
351
+ value="index_new.html"
352
+ class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
353
+ <p class="text-xs text-gray-500 mt-1">HTML file in the static/ folder</p>
354
+ </div>
355
+
356
+ <div>
357
+ <label class="flex items-center">
358
+ <input type="checkbox" id="theme-active" class="mr-2">
359
+ <span class="text-sm text-gray-700">Activate immediately after creation</span>
360
+ </label>
361
+ </div>
362
+ </div>
363
+ </form>
364
+ </div>
365
+
366
+ <div class="flex justify-end space-x-3 p-6 border-t bg-gray-50">
367
+ <button onclick="closeThemeModal()"
368
+ class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
369
+ Cancel
370
+ </button>
371
+ <button type="submit" form="theme-form"
372
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
373
+ Create
374
+ </button>
375
+ </div>
376
+ </div>
377
+ `;
378
+
379
+ document.body.appendChild(modal);
380
+ feather.replace();
381
+ }
382
+
383
+ function showEditThemeModal(themeId) {
384
+ const theme = themesData.find(t => t.id === themeId);
385
+ if (!theme) {
386
+ showNotification('Theme not found', 'error');
387
+ return;
388
+ }
389
+
390
+ const existingModal = document.getElementById('theme-modal');
391
+ if (existingModal) existingModal.remove();
392
+
393
+ const modal = document.createElement('div');
394
+ modal.id = 'theme-modal';
395
+ modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4';
396
+ modal.innerHTML = `
397
+ <div class="bg-white rounded-lg shadow-xl w-full max-w-md max-h-[90vh] overflow-y-auto">
398
+ <div class="flex justify-between items-center p-6 border-b">
399
+ <h3 class="text-xl font-bold text-gray-800">Edit theme</h3>
400
+ <button onclick="closeThemeModal()" class="text-gray-500 hover:text-gray-700 p-2">
401
+ <i data-feather="x" class="w-6 h-6"></i>
402
+ </button>
403
+ </div>
404
+
405
+ <div class="p-6">
406
+ <form id="theme-form" onsubmit="handleThemeUpdateSubmit(event, ${themeId})">
407
+ <div class="space-y-4">
408
+ <div>
409
+ <label class="block text-sm font-medium text-gray-700 mb-1">
410
+ Theme name <span class="text-red-500">*</span>
411
+ </label>
412
+ <input type="text" id="theme-name" required
413
+ value="${escapeHtml(theme.theme_name)}"
414
+ class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
415
+ </div>
416
+
417
+ <div>
418
+ <label class="block text-sm font-medium text-gray-700 mb-1">
419
+ Main page file <span class="text-red-500">*</span>
420
+ </label>
421
+ <input type="text" id="theme-file" required
422
+ value="${escapeHtml(theme.main_page_file)}"
423
+ class="w-full border rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
424
+ <p class="text-xs text-gray-500 mt-1">HTML file in the static/ folder</p>
425
+ </div>
426
+
427
+ <div>
428
+ <label class="flex items-center">
429
+ <input type="checkbox" id="theme-active" ${theme.is_active ? 'checked' : ''}>
430
+ <span class="text-sm text-gray-700">Active theme</span>
431
+ </label>
432
+ </div>
433
+ </div>
434
+ </form>
435
+ </div>
436
+
437
+ <div class="flex justify-end space-x-3 p-6 border-t bg-gray-50">
438
+ <button onclick="closeThemeModal()"
439
+ class="px-4 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
440
+ Cancel
441
+ </button>
442
+ <button type="submit" form="theme-form"
443
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
444
+ Save
445
+ </button>
446
+ </div>
447
+ </div>
448
+ `;
449
+
450
+ document.body.appendChild(modal);
451
+ feather.replace();
452
+ }
453
+
454
+ function closeThemeModal() {
455
+ const modal = document.getElementById('theme-modal');
456
+ if (modal) modal.remove();
457
+ }
458
+
459
+ function handleThemeFormSubmit(e) {
460
+ e.preventDefault();
461
+
462
+ const themeName = document.getElementById('theme-name')?.value;
463
+ const mainPageFile = document.getElementById('theme-file')?.value;
464
+ const isActive = document.getElementById('theme-active')?.checked || false;
465
+
466
+ if (!themeName || !mainPageFile) {
467
+ showNotification('Please fill in all required fields', 'error');
468
+ return;
469
+ }
470
+
471
+ createTheme(themeName, mainPageFile, isActive);
472
+ }
473
+
474
+ function handleThemeUpdateSubmit(e, themeId) {
475
+ e.preventDefault();
476
+
477
+ const themeName = document.getElementById('theme-name')?.value;
478
+ const mainPageFile = document.getElementById('theme-file')?.value;
479
+ const isActive = document.getElementById('theme-active')?.checked || false;
480
+
481
+ if (!themeName || !mainPageFile) {
482
+ showNotification('Please fill in all required fields', 'error');
483
+ return;
484
+ }
485
+
486
+ updateTheme(themeId, themeName, mainPageFile, isActive);
487
+ }
488
+
489
+ // Section for display
490
+ function createThemesSection() {
491
+ const mainContent = document.querySelector('main');
492
+ if (!mainContent) return;
493
+ if (document.getElementById('themes-section')) return;
494
+
495
+ const section = document.createElement('div');
496
+ section.id = 'themes-section';
497
+ section.className = 'content-section';
498
+ section.innerHTML = `
499
+ <div class="bg-white rounded-lg shadow-sm p-6">
500
+ <div class="flex justify-between items-center mb-6">
501
+ <div>
502
+ <h2 class="text-2xl font-bold text-gray-800">Visual Themes</h2>
503
+ <p class="text-gray-600 mt-1">Manage the interface's look and feel</p>
504
+ </div>
505
+ <div class="flex space-x-2">
506
+ <button onclick="refreshThemes()"
507
+ class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center">
508
+ <i data-feather="refresh-cw" class="w-4 h-4 mr-2"></i>Refresh
509
+ </button>
510
+ <button onclick="showCreateThemeModal()"
511
+ class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 flex items-center">
512
+ <i data-feather="plus" class="w-4 h-4 mr-2"></i>Create theme
513
+ </button>
514
+ </div>
515
+ </div>
516
+
517
+ <div id="themes-container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
518
+ <div class="col-span-full text-center py-12">
519
+ <i data-feather="loader" class="w-12 h-12 text-gray-400 mx-auto mb-3 animate-spin"></i>
520
+ <p class="text-gray-500">Loading themes...</p>
521
+ </div>
522
+ </div>
523
+ </div>
524
+ `;
525
+
526
+ mainContent.appendChild(section);
527
+ feather.replace();
528
+ }
529
+
530
+ function showThemesSection() {
531
+ if (window.appRouter) {
532
+ window.appRouter.navigate('/selfcare/modules/themes');
533
+ }
534
+
535
+ const contentSections = document.querySelectorAll('.content-section');
536
+ contentSections.forEach(section => {
537
+ section.classList.remove('active');
538
+ });
539
+
540
+ const targetSection = document.getElementById('themes-section');
541
+ if (targetSection) {
542
+ targetSection.classList.add('active');
543
+ const titleEl = document.getElementById('current-section-title');
544
+ if (titleEl) titleEl.textContent = 'Visual Themes';
545
+ }
546
+
547
+ // Load themes when the section is shown
548
+ loadThemes();
549
+
550
+ feather.replace();
551
+ }
552
+
553
+ // Navigation
554
+ function addThemesNavigation() {
555
+ const mainNav = document.getElementById('main-nav');
556
+ if (!mainNav) return;
557
+ if (document.querySelector('[data-target="themes-section"]')) return;
558
+
559
+ const navItem = document.createElement('a');
560
+ navItem.href = '#';
561
+ navItem.className = 'nav-item flex items-center px-6 py-3 text-blue-100 hover:bg-blue-600';
562
+ navItem.setAttribute('data-target', 'themes-section');
563
+ navItem.innerHTML = `
564
+ <i data-feather="layout" class="w-5 h-5 mr-3"></i>
565
+ <span class="nav-text">Visual Themes</span>
566
+ `;
567
+
568
+ navItem.addEventListener('click', function(e) {
569
+ e.preventDefault();
570
+ e.stopPropagation();
571
+
572
+ showThemesSection();
573
+
574
+ document.querySelectorAll('.nav-item').forEach(item => {
575
+ item.classList.remove('text-white', 'bg-blue-800');
576
+ item.classList.add('text-blue-100', 'hover:bg-blue-600');
577
+ });
578
+
579
+ this.classList.remove('text-blue-100', 'hover:bg-blue-600');
580
+ this.classList.add('text-white', 'bg-blue-800');
581
+ });
582
+
583
+ const modulesNavContainer = document.getElementById('modules-nav-container');
584
+ if (modulesNavContainer) {
585
+ modulesNavContainer.appendChild(navItem);
586
+ } else {
587
+ mainNav.appendChild(navItem);
588
+ }
589
+
590
+ // Adapt for a collapsed sidebar
591
+ const sidebar = document.getElementById('sidebar');
592
+ if (sidebar && sidebar.classList.contains('sidebar-collapsed')) {
593
+ const navText = navItem.querySelector('.nav-text');
594
+ const icon = navItem.querySelector('i');
595
+ if (navText) navText.style.display = 'none';
596
+ if (icon) icon.classList.remove('mr-3');
597
+ navItem.classList.add('justify-center', 'px-3');
598
+ }
599
+
600
+ feather.replace();
601
+ }
602
+
603
+ // Registration in the global router
604
+ function registerThemesRoutes() {
605
+ if (window.appRouter) {
606
+ window.appRouter.registerRoute('/selfcare/modules/themes', () => {
607
+ setTimeout(() => showThemesSection(), 100);
608
+ });
609
+ }
610
+ }
611
+
612
+ function registerThemesSections() {
613
+ if (window.appSections) {
614
+ window.appSections.registerSection('themes-section', '/selfcare/modules/themes', () => {
615
+ showThemesSection();
616
+ });
617
+ }
618
+ }
619
+
620
+ // Module initialization
621
+ function initThemesModule() {
622
+ // Global functions
623
+ window.themesData = themesData;
624
+ window.loadThemes = loadThemes;
625
+ window.refreshThemes = refreshThemes;
626
+ window.activateTheme = activateTheme;
627
+ window.deleteTheme = deleteTheme;
628
+ window.createTheme = createTheme;
629
+ window.showCreateThemeModal = showCreateThemeModal;
630
+ window.showEditThemeModal = showEditThemeModal;
631
+ window.closeThemeModal = closeThemeModal;
632
+ window.handleThemeFormSubmit = handleThemeFormSubmit;
633
+ window.handleThemeUpdateSubmit = handleThemeUpdateSubmit;
634
+ window.showThemesSection = showThemesSection;
635
+
636
+ // Create the section and add navigation
637
+ createThemesSection();
638
+ addThemesNavigation();
639
+ registerThemesRoutes();
640
+ registerThemesSections();
641
+
642
+ // If the current URL matches themes, show it
643
+ if (window.location.pathname === '/selfcare/modules/themes') {
644
+ setTimeout(() => showThemesSection(), 100);
645
+ }
646
+ }
647
+
648
+ // Auto-load the module
649
+ if (document.readyState === 'loading') {
650
+ document.addEventListener('DOMContentLoaded', initThemesModule);
651
+ } else {
652
+ initThemesModule();
653
+ }