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,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, '<')
|
|
36
|
+
.replace(/>/g, '>')
|
|
37
|
+
.replace(/"/g, '"')
|
|
38
|
+
.replace(/'/g, ''')
|
|
39
|
+
.replace(/\//g, '/');
|
|
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
|
+
}
|