@intranefr/superbackend 1.5.2 → 1.6.3
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.
- package/cookies.txt +6 -0
- package/cookies1.txt +6 -0
- package/cookies2.txt +6 -0
- package/cookies3.txt +6 -0
- package/cookies4.txt +5 -0
- package/cookies_old.txt +5 -0
- package/cookies_old_test.txt +6 -0
- package/cookies_super.txt +5 -0
- package/cookies_super_test.txt +6 -0
- package/cookies_test.txt +6 -0
- package/index.js +9 -0
- package/manage.js +745 -0
- package/package.json +6 -2
- package/plugins/core-waiting-list-migration/README.md +118 -0
- package/plugins/core-waiting-list-migration/index.js +438 -0
- package/plugins/global-settings-presets/index.js +20 -0
- package/plugins/hello-cli/index.js +17 -0
- package/plugins/ui-components-seeder/components/suiAlert.js +212 -0
- package/plugins/ui-components-seeder/components/suiToast.js +186 -0
- package/plugins/ui-components-seeder/index.js +31 -0
- package/public/js/admin-ui-components-preview.js +281 -0
- package/public/js/admin-ui-components.js +408 -0
- package/public/js/llm-provider-model-picker.js +193 -0
- package/public/test-iframe-fix.html +63 -0
- package/public/test-iframe.html +14 -0
- package/src/admin/endpointRegistry.js +68 -0
- package/src/controllers/admin.controller.js +36 -10
- package/src/controllers/adminAgents.controller.js +37 -0
- package/src/controllers/adminDataCleanup.controller.js +45 -0
- package/src/controllers/adminLlm.controller.js +19 -8
- package/src/controllers/adminLogin.controller.js +269 -0
- package/src/controllers/adminMarkdowns.controller.js +157 -0
- package/src/controllers/adminPlugins.controller.js +55 -0
- package/src/controllers/adminRegistry.controller.js +106 -0
- package/src/controllers/adminScripts.controller.js +138 -0
- package/src/controllers/adminStats.controller.js +4 -4
- package/src/controllers/adminTelegram.controller.js +72 -0
- package/src/controllers/markdowns.controller.js +42 -0
- package/src/controllers/registry.controller.js +32 -0
- package/src/controllers/waitingList.controller.js +52 -74
- package/src/helpers/mongooseHelper.js +6 -6
- package/src/helpers/scriptBase.js +2 -2
- package/src/middleware/auth.js +71 -1
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +584 -176
- package/src/models/Agent.js +105 -0
- package/src/models/AgentMessage.js +82 -0
- package/src/models/GlobalSetting.js +11 -1
- package/src/models/Markdown.js +75 -0
- package/src/models/ScriptRun.js +8 -0
- package/src/models/TelegramBot.js +42 -0
- package/src/models/UiComponent.js +2 -0
- package/src/models/User.js +1 -1
- package/src/routes/admin.routes.js +3 -3
- package/src/routes/adminAgents.routes.js +13 -0
- package/src/routes/adminAssets.routes.js +11 -11
- package/src/routes/adminBlog.routes.js +2 -2
- package/src/routes/adminBlogAi.routes.js +2 -2
- package/src/routes/adminBlogAutomation.routes.js +2 -2
- package/src/routes/adminCache.routes.js +2 -2
- package/src/routes/adminConsoleManager.routes.js +2 -2
- package/src/routes/adminCrons.routes.js +2 -2
- package/src/routes/adminDataCleanup.routes.js +26 -0
- package/src/routes/adminDbBrowser.routes.js +2 -2
- package/src/routes/adminEjsVirtual.routes.js +2 -2
- package/src/routes/adminFeatureFlags.routes.js +6 -6
- package/src/routes/adminHeadless.routes.js +2 -2
- package/src/routes/adminHealthChecks.routes.js +2 -2
- package/src/routes/adminI18n.routes.js +2 -2
- package/src/routes/adminJsonConfigs.routes.js +8 -8
- package/src/routes/adminLlm.routes.js +8 -7
- package/src/routes/adminLogin.routes.js +23 -0
- package/src/routes/adminMarkdowns.routes.js +10 -0
- package/src/routes/adminMigration.routes.js +12 -12
- package/src/routes/adminPages.routes.js +2 -2
- package/src/routes/adminPlugins.routes.js +15 -0
- package/src/routes/adminProxy.routes.js +2 -2
- package/src/routes/adminRateLimits.routes.js +8 -8
- package/src/routes/adminRbac.routes.js +2 -2
- package/src/routes/adminRegistry.routes.js +24 -0
- package/src/routes/adminScripts.routes.js +6 -3
- package/src/routes/adminSeoConfig.routes.js +10 -10
- package/src/routes/adminTelegram.routes.js +14 -0
- package/src/routes/adminTerminals.routes.js +2 -2
- package/src/routes/adminUiComponents.routes.js +2 -2
- package/src/routes/adminUploadNamespaces.routes.js +7 -7
- package/src/routes/blogInternal.routes.js +2 -2
- package/src/routes/experiments.routes.js +2 -2
- package/src/routes/formsAdmin.routes.js +6 -6
- package/src/routes/globalSettings.routes.js +8 -8
- package/src/routes/internalExperiments.routes.js +2 -2
- package/src/routes/markdowns.routes.js +16 -0
- package/src/routes/notificationAdmin.routes.js +7 -7
- package/src/routes/orgAdmin.routes.js +16 -16
- package/src/routes/pages.routes.js +3 -3
- package/src/routes/registry.routes.js +11 -0
- package/src/routes/stripeAdmin.routes.js +12 -12
- package/src/routes/userAdmin.routes.js +7 -7
- package/src/routes/waitingListAdmin.routes.js +2 -2
- package/src/routes/workflows.routes.js +3 -3
- package/src/services/agent.service.js +546 -0
- package/src/services/agentHistory.service.js +345 -0
- package/src/services/agentTools.service.js +578 -0
- package/src/services/dataCleanup.service.js +286 -0
- package/src/services/jsonConfigs.service.js +284 -10
- package/src/services/llm.service.js +219 -6
- package/src/services/markdowns.service.js +522 -0
- package/src/services/plugins.service.js +348 -0
- package/src/services/registry.service.js +452 -0
- package/src/services/scriptsRunner.service.js +328 -37
- package/src/services/telegram.service.js +130 -0
- package/src/services/uiComponents.service.js +180 -0
- package/src/services/waitingListJson.service.js +401 -0
- package/src/utils/rbac/rightsRegistry.js +118 -0
- package/test-access.js +63 -0
- package/test-iframe-fix.html +63 -0
- package/test-iframe.html +14 -0
- package/views/admin-403.ejs +92 -0
- package/views/admin-agents.ejs +273 -0
- package/views/admin-coolify-deploy.ejs +8 -8
- package/views/admin-dashboard-home.ejs +52 -2
- package/views/admin-dashboard.ejs +179 -7
- package/views/admin-data-cleanup.ejs +357 -0
- package/views/admin-experiments.ejs +1 -1
- package/views/admin-login.ejs +286 -0
- package/views/admin-markdowns.ejs +905 -0
- package/views/admin-plugins-system.ejs +223 -0
- package/views/admin-scripts.ejs +221 -4
- package/views/admin-telegram.ejs +269 -0
- package/views/admin-ui-components.ejs +82 -402
- package/views/admin-users.ejs +207 -11
- package/views/partials/dashboard/nav-items.ejs +5 -0
- package/views/partials/llm-provider-model-picker.ejs +0 -161
- package/analysis-only.skill +0 -0
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
<body class="bg-gray-50 overflow-hidden">
|
|
16
16
|
<div id="app" class="h-screen flex flex-col" v-cloak>
|
|
17
17
|
<!-- Top Header -->
|
|
18
|
-
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0">
|
|
18
|
+
<header v-if="!globalZenMode" class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0">
|
|
19
19
|
<div class="flex items-center gap-4">
|
|
20
20
|
<i class="ti ti-layout-dashboard text-2xl text-blue-600"></i>
|
|
21
21
|
<h1 class="text-xl font-bold text-gray-800">
|
|
@@ -30,6 +30,38 @@
|
|
|
30
30
|
<span class="ml-1">to search</span>
|
|
31
31
|
</div>
|
|
32
32
|
<div class="flex items-center gap-4">
|
|
33
|
+
<!-- User Info & Logout -->
|
|
34
|
+
<div class="flex items-center gap-3">
|
|
35
|
+
<div class="text-right">
|
|
36
|
+
<div class="text-sm font-medium text-gray-700">{{ currentUser.name || currentUser.username || 'Admin User' }}</div>
|
|
37
|
+
<div class="text-xs text-gray-500">{{ currentUser.email || currentUser.authType + ' authentication' }}</div>
|
|
38
|
+
</div>
|
|
39
|
+
<div class="relative">
|
|
40
|
+
<button
|
|
41
|
+
@click="showUserMenu = !showUserMenu"
|
|
42
|
+
class="flex items-center justify-center w-8 h-8 bg-blue-600 text-white rounded-full hover:bg-blue-700 transition-colors">
|
|
43
|
+
<i class="ti ti-user text-sm"></i>
|
|
44
|
+
</button>
|
|
45
|
+
|
|
46
|
+
<!-- User Dropdown Menu -->
|
|
47
|
+
<div v-if="showUserMenu" class="absolute right-0 mt-2 w-48 bg-white rounded-lg shadow-lg border border-gray-200 py-1 z-50">
|
|
48
|
+
<div class="px-4 py-2 border-b border-gray-100">
|
|
49
|
+
<div class="text-sm font-medium text-gray-900">{{ currentUser.name || currentUser.username || 'Admin User' }}</div>
|
|
50
|
+
<div class="text-xs text-gray-500">{{ currentUser.role }}</div>
|
|
51
|
+
</div>
|
|
52
|
+
<button
|
|
53
|
+
@click="handleLogout"
|
|
54
|
+
class="w-full text-left px-4 py-2 text-sm text-red-600 hover:bg-red-50 transition-colors flex items-center gap-2">
|
|
55
|
+
<i class="ti ti-logout"></i>
|
|
56
|
+
Logout
|
|
57
|
+
</button>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<button @click="globalZenMode = !globalZenMode" class="text-gray-500 hover:text-blue-600 p-1.5 rounded-lg border border-gray-200 bg-white" title="Toggle Global Zen Mode (ESC ESC to exit)">
|
|
63
|
+
<i class="ti ti-maximize"></i>
|
|
64
|
+
</button>
|
|
33
65
|
<span class="text-sm text-gray-500">v1.0.0</span>
|
|
34
66
|
<a :href="baseUrl + adminBase + '/api/test'" target="_blank" class="text-sm text-blue-600 hover:underline">API Test</a>
|
|
35
67
|
</div>
|
|
@@ -37,11 +69,15 @@
|
|
|
37
69
|
</header>
|
|
38
70
|
|
|
39
71
|
<div class="flex-1 flex overflow-hidden">
|
|
40
|
-
|
|
72
|
+
<template v-if="!globalZenMode">
|
|
73
|
+
<%- include('partials/dashboard/sidebar') %>
|
|
74
|
+
</template>
|
|
41
75
|
|
|
42
76
|
<!-- Main Content Area -->
|
|
43
77
|
<main class="flex-1 flex flex-col overflow-hidden bg-gray-50">
|
|
44
|
-
|
|
78
|
+
<template v-if="!globalZenMode">
|
|
79
|
+
<%- include('partials/dashboard/tab-bar') %>
|
|
80
|
+
</template>
|
|
45
81
|
|
|
46
82
|
<div class="flex-1 relative">
|
|
47
83
|
<!-- Iframes -->
|
|
@@ -49,7 +85,7 @@
|
|
|
49
85
|
v-for="tab in tabs"
|
|
50
86
|
v-show="activeTabId === tab.id"
|
|
51
87
|
:key="tab.id"
|
|
52
|
-
:src="
|
|
88
|
+
:src="getIframeSrc(tab.path)"
|
|
53
89
|
class="absolute inset-0 w-full h-full border-none"
|
|
54
90
|
:id="'frame-' + tab.id"
|
|
55
91
|
></iframe>
|
|
@@ -89,6 +125,15 @@
|
|
|
89
125
|
// Tabs state
|
|
90
126
|
const tabs = ref([]);
|
|
91
127
|
const activeTabId = ref(null);
|
|
128
|
+
const globalZenMode = ref(false);
|
|
129
|
+
|
|
130
|
+
// User authentication state
|
|
131
|
+
const currentUser = ref({});
|
|
132
|
+
const showUserMenu = ref(false);
|
|
133
|
+
|
|
134
|
+
// ESC ESC to exit Zen Mode
|
|
135
|
+
let escCount = 0;
|
|
136
|
+
let escTimeout = null;
|
|
92
137
|
|
|
93
138
|
// localStorage utilities
|
|
94
139
|
const STORAGE_KEY = 'adminDashboardTabs';
|
|
@@ -342,6 +387,19 @@
|
|
|
342
387
|
|
|
343
388
|
// Keyboard events
|
|
344
389
|
const handleKeydown = (e) => {
|
|
390
|
+
if (e.key === 'Escape') {
|
|
391
|
+
escCount++;
|
|
392
|
+
clearTimeout(escTimeout);
|
|
393
|
+
if (escCount >= 2) {
|
|
394
|
+
globalZenMode.value = false;
|
|
395
|
+
escCount = 0;
|
|
396
|
+
} else {
|
|
397
|
+
escTimeout = setTimeout(() => {
|
|
398
|
+
escCount = 0;
|
|
399
|
+
}, 500);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
345
403
|
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
346
404
|
e.preventDefault();
|
|
347
405
|
e.stopPropagation();
|
|
@@ -350,14 +408,69 @@
|
|
|
350
408
|
};
|
|
351
409
|
|
|
352
410
|
const handleMessage = (e) => {
|
|
353
|
-
if (e.data && e.data.type === 'keydown'
|
|
354
|
-
|
|
411
|
+
if (e.data && e.data.type === 'keydown') {
|
|
412
|
+
if (e.data.ctrlK) {
|
|
413
|
+
togglePalette('iframe-message');
|
|
414
|
+
}
|
|
415
|
+
if (e.data.key === 'Escape') {
|
|
416
|
+
handleKeydown({ key: 'Escape' });
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
// User authentication functions
|
|
422
|
+
const fetchCurrentUser = async () => {
|
|
423
|
+
try {
|
|
424
|
+
const response = await fetch(`${adminBase}/auth-status`);
|
|
425
|
+
if (response.ok) {
|
|
426
|
+
const userData = await response.json();
|
|
427
|
+
if (userData.authenticated) {
|
|
428
|
+
currentUser.value = userData.user || {};
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
console.error('Failed to fetch user data:', error);
|
|
433
|
+
// Set default user data on error
|
|
434
|
+
currentUser.value = { name: 'Admin User', authType: 'unknown' };
|
|
435
|
+
}
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const handleLogout = async () => {
|
|
439
|
+
try {
|
|
440
|
+
const response = await fetch(`${adminBase}/logout`, {
|
|
441
|
+
method: 'POST',
|
|
442
|
+
headers: {
|
|
443
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
if (response.redirected) {
|
|
448
|
+
window.location.href = response.url;
|
|
449
|
+
} else {
|
|
450
|
+
// Fallback redirect
|
|
451
|
+
window.location.href = `${adminBase}/login`;
|
|
452
|
+
}
|
|
453
|
+
} catch (error) {
|
|
454
|
+
console.error('Logout error:', error);
|
|
455
|
+
// Fallback redirect
|
|
456
|
+
window.location.href = `${adminBase}/login`;
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// Close user menu when clicking outside
|
|
461
|
+
const handleClickOutside = (event) => {
|
|
462
|
+
if (showUserMenu.value && !event.target.closest('.relative')) {
|
|
463
|
+
showUserMenu.value = false;
|
|
355
464
|
}
|
|
356
465
|
};
|
|
357
466
|
|
|
358
467
|
onMounted(() => {
|
|
359
468
|
window.addEventListener('keydown', handleKeydown);
|
|
360
469
|
window.addEventListener('message', handleMessage);
|
|
470
|
+
window.addEventListener('click', handleClickOutside);
|
|
471
|
+
|
|
472
|
+
// Fetch current user data
|
|
473
|
+
fetchCurrentUser();
|
|
361
474
|
|
|
362
475
|
// Load saved tabs from localStorage
|
|
363
476
|
const savedState = loadTabsFromStorage();
|
|
@@ -381,14 +494,26 @@
|
|
|
381
494
|
onUnmounted(() => {
|
|
382
495
|
window.removeEventListener('keydown', handleKeydown);
|
|
383
496
|
window.removeEventListener('message', handleMessage);
|
|
497
|
+
window.removeEventListener('click', handleClickOutside);
|
|
384
498
|
});
|
|
385
499
|
|
|
500
|
+
const getIframeSrc = (path) => {
|
|
501
|
+
// Ensure path starts with /
|
|
502
|
+
const cleanPath = path.startsWith('/') ? path : ('/' + path);
|
|
503
|
+
// Add iframe token for authentication
|
|
504
|
+
const separator = cleanPath.includes('?') ? '&' : '?';
|
|
505
|
+
return cleanPath + separator + 'iframe_token=authenticated';
|
|
506
|
+
};
|
|
507
|
+
|
|
386
508
|
return {
|
|
387
509
|
baseUrl,
|
|
388
510
|
adminBase,
|
|
389
511
|
navSections,
|
|
390
512
|
tabs,
|
|
391
513
|
activeTabId,
|
|
514
|
+
globalZenMode,
|
|
515
|
+
currentUser,
|
|
516
|
+
showUserMenu,
|
|
392
517
|
openTab,
|
|
393
518
|
closeTab,
|
|
394
519
|
showPalette,
|
|
@@ -400,12 +525,59 @@
|
|
|
400
525
|
closePalette,
|
|
401
526
|
navigatePalette,
|
|
402
527
|
selectPaletteItem,
|
|
403
|
-
selectModule
|
|
528
|
+
selectModule,
|
|
529
|
+
handleLogout,
|
|
530
|
+
getIframeSrc
|
|
404
531
|
};
|
|
405
532
|
}
|
|
406
533
|
}).mount('#app');
|
|
407
534
|
</script>
|
|
408
535
|
<script>
|
|
536
|
+
// Handle iframe communication for API requests
|
|
537
|
+
window.addEventListener('message', async (event) => {
|
|
538
|
+
if (event.data.type === 'stats-request') {
|
|
539
|
+
try {
|
|
540
|
+
const response = await fetch(event.data.endpoint);
|
|
541
|
+
const data = await response.json();
|
|
542
|
+
|
|
543
|
+
// Send response back to iframe
|
|
544
|
+
event.source.postMessage({
|
|
545
|
+
type: 'stats-response',
|
|
546
|
+
messageId: event.data.messageId,
|
|
547
|
+
data: data
|
|
548
|
+
}, '*');
|
|
549
|
+
} catch (error) {
|
|
550
|
+
// Send error back to iframe
|
|
551
|
+
event.source.postMessage({
|
|
552
|
+
type: 'stats-response',
|
|
553
|
+
messageId: event.data.messageId,
|
|
554
|
+
error: error.message
|
|
555
|
+
}, '*');
|
|
556
|
+
}
|
|
557
|
+
} else if (event.data.type === 'api-request') {
|
|
558
|
+
try {
|
|
559
|
+
const response = await fetch(event.data.url, event.data.options);
|
|
560
|
+
const data = await response.json();
|
|
561
|
+
|
|
562
|
+
// Send response back to iframe
|
|
563
|
+
event.source.postMessage({
|
|
564
|
+
type: 'api-response',
|
|
565
|
+
messageId: event.data.messageId,
|
|
566
|
+
ok: response.ok,
|
|
567
|
+
data: data
|
|
568
|
+
}, '*');
|
|
569
|
+
} catch (error) {
|
|
570
|
+
// Send error back to iframe
|
|
571
|
+
event.source.postMessage({
|
|
572
|
+
type: 'api-response',
|
|
573
|
+
messageId: event.data.messageId,
|
|
574
|
+
ok: false,
|
|
575
|
+
error: error.message
|
|
576
|
+
}, '*');
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
|
|
409
581
|
window.addEventListener("keydown", (e) => {
|
|
410
582
|
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
411
583
|
e.preventDefault();
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin Data Cleanup</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
9
|
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
|
|
10
|
+
<style>[v-cloak]{display:none}</style>
|
|
11
|
+
</head>
|
|
12
|
+
<body class="bg-gray-50">
|
|
13
|
+
<div id="app" class="max-w-7xl mx-auto px-6 py-6" v-cloak>
|
|
14
|
+
<div class="flex items-center justify-between mb-6">
|
|
15
|
+
<div>
|
|
16
|
+
<h1 class="text-2xl font-semibold text-gray-900">Data cleanup</h1>
|
|
17
|
+
<div class="text-sm text-gray-500">Inspect MongoDB storage and run manual cleanup for old documents.</div>
|
|
18
|
+
</div>
|
|
19
|
+
<button @click="loadOverview" :disabled="loadingOverview" class="px-3 py-2 rounded bg-gray-800 text-white text-sm hover:bg-gray-900 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
20
|
+
<i v-if="!loadingOverview" class="ti ti-refresh mr-1"></i>
|
|
21
|
+
<span v-if="loadingOverview">Refreshing...</span>
|
|
22
|
+
<span v-else>Refresh</span>
|
|
23
|
+
</button>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<div v-if="error" class="mb-4 p-3 rounded border border-red-200 bg-red-50 text-red-700 text-sm">{{ error }}</div>
|
|
27
|
+
|
|
28
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
|
|
29
|
+
<div class="bg-white border rounded p-4">
|
|
30
|
+
<div class="text-xs text-gray-500 uppercase">Collections</div>
|
|
31
|
+
<div class="text-2xl font-bold text-gray-900">{{ overview.global.collections || 0 }}</div>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="bg-white border rounded p-4">
|
|
34
|
+
<div class="text-xs text-gray-500 uppercase">Data size</div>
|
|
35
|
+
<div class="text-2xl font-bold text-gray-900">{{ bytes(overview.global.dataSizeBytes) }}</div>
|
|
36
|
+
</div>
|
|
37
|
+
<div class="bg-white border rounded p-4">
|
|
38
|
+
<div class="text-xs text-gray-500 uppercase">Total size</div>
|
|
39
|
+
<div class="text-2xl font-bold text-gray-900">{{ bytes(overview.global.totalSizeBytes) }}</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<div class="bg-white border rounded mb-6">
|
|
44
|
+
<div class="p-4 border-b font-semibold text-gray-800">Collections</div>
|
|
45
|
+
<div class="overflow-auto" style="max-height:320px;">
|
|
46
|
+
<table class="min-w-full text-sm">
|
|
47
|
+
<thead class="bg-gray-50 sticky top-0">
|
|
48
|
+
<tr>
|
|
49
|
+
<th class="text-left p-2">Collection</th>
|
|
50
|
+
<th class="text-left p-2">Count</th>
|
|
51
|
+
<th class="text-left p-2">Data</th>
|
|
52
|
+
<th class="text-left p-2">Storage</th>
|
|
53
|
+
<th class="text-left p-2">Indexes</th>
|
|
54
|
+
</tr>
|
|
55
|
+
</thead>
|
|
56
|
+
<tbody>
|
|
57
|
+
<tr
|
|
58
|
+
v-for="c in sortedCollections"
|
|
59
|
+
:key="c.name"
|
|
60
|
+
class="border-t cursor-pointer hover:bg-gray-50"
|
|
61
|
+
@click="form.collection = c.name"
|
|
62
|
+
>
|
|
63
|
+
<td class="p-2">{{ c.name }}</td>
|
|
64
|
+
<td class="p-2">{{ c.count }}</td>
|
|
65
|
+
<td class="p-2">{{ bytes(c.sizeBytes) }}</td>
|
|
66
|
+
<td class="p-2">{{ bytes(c.storageSizeBytes) }}</td>
|
|
67
|
+
<td class="p-2">{{ bytes(c.totalIndexSizeBytes) }}</td>
|
|
68
|
+
</tr>
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
75
|
+
<div class="bg-white border rounded p-4 space-y-3">
|
|
76
|
+
<h2 class="font-semibold text-gray-900">Dry run</h2>
|
|
77
|
+
<div>
|
|
78
|
+
<label class="text-xs text-gray-600">Collection</label>
|
|
79
|
+
<select v-model="form.collection" class="mt-1 w-full border rounded px-3 py-2">
|
|
80
|
+
<option value="">Select collection</option>
|
|
81
|
+
<option v-for="c in overview.collections" :key="c.name" :value="c.name">{{ c.name }}</option>
|
|
82
|
+
</select>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="grid grid-cols-2 gap-3">
|
|
85
|
+
<div>
|
|
86
|
+
<label class="text-xs text-gray-600">Older than days</label>
|
|
87
|
+
<input v-model.number="form.olderThanDays" type="number" min="1" class="mt-1 w-full border rounded px-3 py-2" />
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<label class="text-xs text-gray-600">Date field</label>
|
|
91
|
+
<input v-model="form.dateField" class="mt-1 w-full border rounded px-3 py-2" />
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
<div v-if="loadingFields" class="text-xs text-gray-500">Loading fields...</div>
|
|
95
|
+
<div v-else-if="availableFields.length > 0" class="text-xs text-gray-600">
|
|
96
|
+
<span class="font-medium">Available fields:</span> {{ availableFields.join(', ') }}
|
|
97
|
+
</div>
|
|
98
|
+
<button @click="runDryRun" :disabled="loadingDryRun" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
99
|
+
<span v-if="loadingDryRun">Running...</span>
|
|
100
|
+
<span v-else>Run dry run</span>
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
<div v-if="dryRun" class="border rounded p-3 bg-blue-50 text-sm space-y-1">
|
|
104
|
+
<div><strong>Candidates:</strong> {{ dryRun.candidateCount }}</div>
|
|
105
|
+
<div><strong>Estimated reclaim:</strong> {{ bytes(dryRun.estimatedReclaimableBytes) }}</div>
|
|
106
|
+
<div><strong>Cutoff:</strong> {{ dryRun.cutoffIso }}</div>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<div class="bg-white border rounded p-4 space-y-3">
|
|
111
|
+
<h2 class="font-semibold text-gray-900">Execute cleanup</h2>
|
|
112
|
+
<p class="text-xs text-orange-700 bg-orange-50 border border-orange-200 rounded p-2">
|
|
113
|
+
This operation is destructive. Dry run first, then confirm execution.
|
|
114
|
+
</p>
|
|
115
|
+
|
|
116
|
+
<!-- Recap section -->
|
|
117
|
+
<div v-if="form.collection" class="bg-gray-50 border rounded p-3 text-sm">
|
|
118
|
+
<div class="font-medium text-gray-700 mb-2">Operation recap:</div>
|
|
119
|
+
<div class="space-y-1">
|
|
120
|
+
<div><strong>Collection:</strong> {{ form.collection }}</div>
|
|
121
|
+
<div><strong>Older than:</strong> {{ form.olderThanDays }} days</div>
|
|
122
|
+
<div><strong>Date field:</strong> {{ form.dateField }}</div>
|
|
123
|
+
<div><strong>Delete limit:</strong> {{ form.limit }}</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
<div v-else class="bg-gray-50 border rounded p-3 text-sm text-gray-500">
|
|
127
|
+
Select a collection in the dry run section to see operation details.
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div>
|
|
131
|
+
<label class="text-xs text-gray-600">Delete limit for this run</label>
|
|
132
|
+
<input v-model.number="form.limit" type="number" min="1" class="mt-1 w-full border rounded px-3 py-2" />
|
|
133
|
+
</div>
|
|
134
|
+
<label class="flex items-center gap-2 text-sm text-gray-700">
|
|
135
|
+
<input type="checkbox" v-model="form.confirm" /> I understand this deletes data permanently.
|
|
136
|
+
</label>
|
|
137
|
+
<button @click="executeCleanup" :disabled="loadingExecute || !form.collection || !form.confirm" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed">
|
|
138
|
+
<span v-if="loadingExecute">Executing...</span>
|
|
139
|
+
<span v-else>Execute cleanup</span>
|
|
140
|
+
</button>
|
|
141
|
+
|
|
142
|
+
<div v-if="execution" class="border rounded p-3 bg-red-50 text-sm space-y-1">
|
|
143
|
+
<div><strong>Deleted:</strong> {{ execution.deletedCount }}</div>
|
|
144
|
+
<div><strong>Duration:</strong> {{ execution.durationMs }} ms</div>
|
|
145
|
+
<div><strong>Limit:</strong> {{ execution.limitApplied }}</div>
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<!-- Confirmation Dialog - positioned outside main container but still in Vue app -->
|
|
151
|
+
<div v-if="showConfirmDialog && isMounted" v-cloak class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
152
|
+
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
|
153
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Confirm Delete Operation</h3>
|
|
154
|
+
<div class="bg-red-50 border border-red-200 rounded p-3 mb-4 text-sm">
|
|
155
|
+
<div class="font-medium text-red-800 mb-2">You are about to permanently delete documents from:</div>
|
|
156
|
+
<div class="space-y-1">
|
|
157
|
+
<div><strong>Collection:</strong> <span class="text-red-600">{{ form.collection }}</span></div>
|
|
158
|
+
<div><strong>Older than:</strong> {{ form.olderThanDays }} days</div>
|
|
159
|
+
<div><strong>Date field:</strong> {{ form.dateField }}</div>
|
|
160
|
+
<div><strong>Max documents:</strong> {{ form.limit }}</div>
|
|
161
|
+
</div>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="mb-4 text-sm text-gray-600">
|
|
164
|
+
Type the collection name <strong>"{{ form.collection }}"</strong> to confirm:
|
|
165
|
+
</div>
|
|
166
|
+
<input
|
|
167
|
+
v-model="confirmCollectionName"
|
|
168
|
+
type="text"
|
|
169
|
+
class="w-full border rounded px-3 py-2 mb-4"
|
|
170
|
+
placeholder="Enter collection name to confirm"
|
|
171
|
+
>
|
|
172
|
+
<div class="flex gap-3">
|
|
173
|
+
<button
|
|
174
|
+
@click="executeCleanup"
|
|
175
|
+
:disabled="confirmCollectionName !== form.collection || loadingExecute"
|
|
176
|
+
class="flex-1 px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
177
|
+
>
|
|
178
|
+
<span v-if="loadingExecute">Deleting...</span>
|
|
179
|
+
<span v-else>Delete Documents</span>
|
|
180
|
+
</button>
|
|
181
|
+
<button
|
|
182
|
+
@click="cancelExecute"
|
|
183
|
+
:disabled="loadingExecute"
|
|
184
|
+
class="flex-1 px-3 py-2 rounded bg-gray-300 text-gray-700 text-sm hover:bg-gray-400 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
185
|
+
>
|
|
186
|
+
Cancel
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<script>
|
|
194
|
+
const { createApp, reactive, ref, computed, watch } = Vue;
|
|
195
|
+
|
|
196
|
+
createApp({
|
|
197
|
+
setup() {
|
|
198
|
+
const baseUrl = '<%= baseUrl %>';
|
|
199
|
+
const apiBase = baseUrl + '/api/admin/data-cleanup';
|
|
200
|
+
|
|
201
|
+
const error = ref('');
|
|
202
|
+
const overview = reactive({ global: {}, collections: [] });
|
|
203
|
+
const dryRun = ref(null);
|
|
204
|
+
const execution = ref(null);
|
|
205
|
+
const form = reactive({
|
|
206
|
+
collection: '',
|
|
207
|
+
olderThanDays: 30,
|
|
208
|
+
dateField: 'createdAt',
|
|
209
|
+
limit: 5000,
|
|
210
|
+
confirm: false,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const sortedCollections = computed(() => {
|
|
214
|
+
return [...(overview.collections || [])].sort((a, b) => (b.sizeBytes || 0) - (a.sizeBytes || 0));
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const availableFields = ref([]);
|
|
218
|
+
const loadingFields = ref(false);
|
|
219
|
+
const loadingOverview = ref(false);
|
|
220
|
+
const loadingDryRun = ref(false);
|
|
221
|
+
const loadingExecute = ref(false);
|
|
222
|
+
const showConfirmDialog = ref(false);
|
|
223
|
+
const confirmCollectionName = ref('');
|
|
224
|
+
const isMounted = ref(false);
|
|
225
|
+
|
|
226
|
+
function bytes(v) {
|
|
227
|
+
const n = Number(v || 0);
|
|
228
|
+
if (!Number.isFinite(n) || n <= 0) return '0 B';
|
|
229
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
|
230
|
+
let i = 0;
|
|
231
|
+
let x = n;
|
|
232
|
+
while (x >= 1024 && i < units.length - 1) { x /= 1024; i += 1; }
|
|
233
|
+
return `${x.toFixed(i === 0 ? 0 : 2)} ${units[i]}`;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function api(path, opts = {}) {
|
|
237
|
+
error.value = '';
|
|
238
|
+
const res = await fetch(apiBase + path, {
|
|
239
|
+
credentials: 'same-origin',
|
|
240
|
+
headers: { 'Content-Type': 'application/json' },
|
|
241
|
+
...opts,
|
|
242
|
+
});
|
|
243
|
+
const data = await res.json().catch(() => ({}));
|
|
244
|
+
if (!res.ok) throw new Error(data.error || 'Request failed');
|
|
245
|
+
return data;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async function loadOverview() {
|
|
249
|
+
loadingOverview.value = true;
|
|
250
|
+
try {
|
|
251
|
+
const data = await api('/overview');
|
|
252
|
+
overview.global = data.global || {};
|
|
253
|
+
overview.collections = data.collections || [];
|
|
254
|
+
} finally {
|
|
255
|
+
loadingOverview.value = false;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function runDryRun() {
|
|
260
|
+
loadingDryRun.value = true;
|
|
261
|
+
try {
|
|
262
|
+
dryRun.value = await api('/dry-run', {
|
|
263
|
+
method: 'POST',
|
|
264
|
+
body: JSON.stringify({
|
|
265
|
+
collection: form.collection,
|
|
266
|
+
olderThanDays: form.olderThanDays,
|
|
267
|
+
dateField: form.dateField,
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
} finally {
|
|
271
|
+
loadingDryRun.value = false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function loadAvailableFields() {
|
|
276
|
+
if (!form.collection) {
|
|
277
|
+
availableFields.value = [];
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
loadingFields.value = true;
|
|
281
|
+
try {
|
|
282
|
+
const data = await api(`/infer-fields?collection=${encodeURIComponent(form.collection)}`);
|
|
283
|
+
availableFields.value = data.fields || [];
|
|
284
|
+
} catch (e) {
|
|
285
|
+
availableFields.value = [];
|
|
286
|
+
} finally {
|
|
287
|
+
loadingFields.value = false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function executeCleanup() {
|
|
292
|
+
if (!showConfirmDialog.value) {
|
|
293
|
+
showConfirmDialog.value = true;
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
loadingExecute.value = true;
|
|
298
|
+
try {
|
|
299
|
+
execution.value = await api('/execute', {
|
|
300
|
+
method: 'POST',
|
|
301
|
+
body: JSON.stringify({
|
|
302
|
+
collection: form.collection,
|
|
303
|
+
olderThanDays: form.olderThanDays,
|
|
304
|
+
dateField: form.dateField,
|
|
305
|
+
limit: form.limit,
|
|
306
|
+
confirm: form.confirm,
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
await loadOverview();
|
|
310
|
+
showConfirmDialog.value = false;
|
|
311
|
+
} finally {
|
|
312
|
+
loadingExecute.value = false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function cancelExecute() {
|
|
317
|
+
showConfirmDialog.value = false;
|
|
318
|
+
confirmCollectionName.value = '';
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
loadOverview().catch((e) => { error.value = e.message || String(e); });
|
|
322
|
+
|
|
323
|
+
// Add watcher for collection field
|
|
324
|
+
watch(() => form.collection, () => {
|
|
325
|
+
loadAvailableFields();
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// Set mounted state after Vue is ready
|
|
329
|
+
isMounted.value = true;
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
error,
|
|
333
|
+
overview,
|
|
334
|
+
sortedCollections,
|
|
335
|
+
dryRun,
|
|
336
|
+
execution,
|
|
337
|
+
form,
|
|
338
|
+
bytes,
|
|
339
|
+
loadOverview,
|
|
340
|
+
runDryRun,
|
|
341
|
+
executeCleanup,
|
|
342
|
+
cancelExecute,
|
|
343
|
+
availableFields,
|
|
344
|
+
loadingFields,
|
|
345
|
+
loadAvailableFields,
|
|
346
|
+
loadingOverview,
|
|
347
|
+
loadingDryRun,
|
|
348
|
+
loadingExecute,
|
|
349
|
+
showConfirmDialog,
|
|
350
|
+
confirmCollectionName,
|
|
351
|
+
isMounted,
|
|
352
|
+
};
|
|
353
|
+
},
|
|
354
|
+
}).mount('#app');
|
|
355
|
+
</script>
|
|
356
|
+
</body>
|
|
357
|
+
</html>
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>Experiments</title>
|
|
7
|
-
<link rel="stylesheet" href="<%= baseUrl %><%= adminPath %>/assets/styles.css" />
|
|
7
|
+
<link rel="stylesheet" href="<%= baseUrl %><%= adminPath %>/assets/css/styles.css" />
|
|
8
8
|
<style>
|
|
9
9
|
body { font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif; padding: 16px; }
|
|
10
10
|
.row { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
|