@intranefr/superbackend 1.4.4 → 1.5.1
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/.env.example +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminHeadless.controller.js +91 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminHeadless.routes.js +8 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/uiComponentsPublic.routes.js +9 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +29 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +528 -10
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,681 @@
|
|
|
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 Cache Layer</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
|
+
</head>
|
|
11
|
+
<body class="bg-gray-50">
|
|
12
|
+
<div id="app" class="max-w-7xl mx-auto px-6 py-6" v-cloak>
|
|
13
|
+
<div class="flex items-center justify-between mb-6">
|
|
14
|
+
<div>
|
|
15
|
+
<h1 class="text-2xl font-semibold text-gray-900">Cache Layer</h1>
|
|
16
|
+
<div class="text-sm text-gray-500">Configure caching backend, inspect keys, and manage cache entries</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="flex items-center gap-2">
|
|
19
|
+
<button @click="loadAll" class="px-3 py-2 rounded bg-gray-600 text-white text-sm hover:bg-gray-700">
|
|
20
|
+
<i class="ti ti-refresh mr-1"></i> Refresh
|
|
21
|
+
</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
|
|
25
|
+
<div class="grid grid-cols-12 gap-6">
|
|
26
|
+
<!-- Left column: Config + Operations + Create + Info + Metrics -->
|
|
27
|
+
<div class="col-span-5 space-y-6">
|
|
28
|
+
<!-- Configuration -->
|
|
29
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
30
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
31
|
+
<div class="text-sm font-semibold text-gray-800">Configuration</div>
|
|
32
|
+
<div class="text-xs text-gray-400">Saved to Global Settings</div>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="p-4 space-y-4">
|
|
35
|
+
<div class="grid grid-cols-2 gap-4">
|
|
36
|
+
<div>
|
|
37
|
+
<label class="text-xs font-semibold text-gray-600">Backend</label>
|
|
38
|
+
<select v-model="config.backend" class="mt-1 w-full border rounded px-3 py-2">
|
|
39
|
+
<option value="memory">memory (memory + mongo offload)</option>
|
|
40
|
+
<option value="redis">redis (redis only)</option>
|
|
41
|
+
</select>
|
|
42
|
+
</div>
|
|
43
|
+
<div>
|
|
44
|
+
<label class="text-xs font-semibold text-gray-600">Eviction policy</label>
|
|
45
|
+
<select v-model="config.evictionPolicy" class="mt-1 w-full border rounded px-3 py-2">
|
|
46
|
+
<option value="fifo">fifo</option>
|
|
47
|
+
<option value="lru">lru</option>
|
|
48
|
+
<option value="lfu">lfu</option>
|
|
49
|
+
</select>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
53
|
+
<div class="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-700">
|
|
54
|
+
<div class="font-semibold text-gray-800 mb-2">Info: When to choose each eviction policy</div>
|
|
55
|
+
<div class="space-y-2">
|
|
56
|
+
<div>
|
|
57
|
+
<div class="font-semibold">FIFO (First In, First Out)</div>
|
|
58
|
+
<div>Choose FIFO when simplicity and low overhead matter most, access patterns are uniform, and you want minimal bookkeeping.</div>
|
|
59
|
+
</div>
|
|
60
|
+
<div>
|
|
61
|
+
<div class="font-semibold">LRU (Least Recently Used)</div>
|
|
62
|
+
<div>Choose LRU when recent access predicts future access. Solid default for most real workloads with temporal locality.</div>
|
|
63
|
+
</div>
|
|
64
|
+
<div>
|
|
65
|
+
<div class="font-semibold">LFU (Least Frequently Used)</div>
|
|
66
|
+
<div>Choose LFU when long-term popularity matters more than recency and access patterns are stable.</div>
|
|
67
|
+
</div>
|
|
68
|
+
<div>
|
|
69
|
+
<div class="font-semibold">Rule of thumb</div>
|
|
70
|
+
<div>Unsure → LRU. Ultra-simple/streaming → FIFO. Stable popularity distribution → LFU.</div>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div class="grid grid-cols-2 gap-4">
|
|
76
|
+
<div>
|
|
77
|
+
<label class="text-xs font-semibold text-gray-600">Default TTL (seconds)</label>
|
|
78
|
+
<input v-model.number="config.defaultTtlSeconds" type="number" class="mt-1 w-full border rounded px-3 py-2" />
|
|
79
|
+
<div class="text-xs text-gray-400 mt-1">Default: 600 (10 minutes). Use null per entry for no-expiry.</div>
|
|
80
|
+
</div>
|
|
81
|
+
<div>
|
|
82
|
+
<label class="text-xs font-semibold text-gray-600">Max entry size (bytes)</label>
|
|
83
|
+
<input v-model.number="config.maxEntryBytes" type="number" class="mt-1 w-full border rounded px-3 py-2" />
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-700">
|
|
88
|
+
<div class="font-semibold text-gray-800 mb-2">Info: TTL defaults</div>
|
|
89
|
+
<div class="space-y-2">
|
|
90
|
+
<div><span class="font-semibold">General-purpose app cache:</span> 5–15 minutes (if forced: 10 minutes).</div>
|
|
91
|
+
<div><span class="font-semibold">Highly dynamic data:</span> 30 seconds – 2 minutes.</div>
|
|
92
|
+
<div><span class="font-semibold">Mostly static data:</span> 1–24 hours.</div>
|
|
93
|
+
<div><span class="font-semibold">Negative caching:</span> 30–60 seconds.</div>
|
|
94
|
+
<div class="text-gray-500">Rule: TTL ≈ how long the data can be wrong without users noticing.</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div v-if="config.backend === 'memory'" class="grid grid-cols-1 gap-4">
|
|
99
|
+
<div>
|
|
100
|
+
<label class="text-xs font-semibold text-gray-600">Mongo offload threshold (bytes)</label>
|
|
101
|
+
<input v-model.number="config.offloadThresholdBytes" type="number" class="mt-1 w-full border rounded px-3 py-2" />
|
|
102
|
+
<div class="text-xs text-gray-400 mt-1">When memory usage exceeds this, entries are offloaded to Mongo.</div>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="grid grid-cols-2 gap-4">
|
|
107
|
+
<div>
|
|
108
|
+
<label class="text-xs font-semibold text-gray-600">At-rest format (global)</label>
|
|
109
|
+
<select v-model="config.atRestFormat" class="mt-1 w-full border rounded px-3 py-2">
|
|
110
|
+
<option value="string">string</option>
|
|
111
|
+
<option value="base64">base64</option>
|
|
112
|
+
</select>
|
|
113
|
+
<div class="text-xs text-gray-400 mt-1">String at rest auto-decodes JSON. Base64 is raw bytes.</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div>
|
|
116
|
+
<label class="text-xs font-semibold text-gray-600">Redis prefix</label>
|
|
117
|
+
<input v-model="config.redisPrefix" class="mt-1 w-full border rounded px-3 py-2" placeholder="superbackend:" />
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<div>
|
|
122
|
+
<label class="text-xs font-semibold text-gray-600">Redis URL</label>
|
|
123
|
+
<input v-model="config.redisUrl" class="mt-1 w-full border rounded px-3 py-2" placeholder="redis://localhost:6379" />
|
|
124
|
+
<div class="text-xs text-gray-400 mt-1">Stored encrypted in Global Settings.</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<div class="flex items-center gap-2">
|
|
128
|
+
<button @click="saveConfig" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">
|
|
129
|
+
<i class="ti ti-device-floppy mr-1"></i> Save
|
|
130
|
+
</button>
|
|
131
|
+
<div v-if="saveStatus" class="text-sm text-gray-600">{{ saveStatus }}</div>
|
|
132
|
+
</div>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
<!-- Operations -->
|
|
137
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
138
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
139
|
+
<div class="text-sm font-semibold text-gray-800">Operations</div>
|
|
140
|
+
</div>
|
|
141
|
+
<div class="p-4 space-y-4">
|
|
142
|
+
<div class="grid grid-cols-2 gap-4">
|
|
143
|
+
<div>
|
|
144
|
+
<label class="text-xs font-semibold text-gray-600">Namespace (optional)</label>
|
|
145
|
+
<input v-model="ops.namespace" class="mt-1 w-full border rounded px-3 py-2" placeholder="default" />
|
|
146
|
+
</div>
|
|
147
|
+
<div>
|
|
148
|
+
<label class="text-xs font-semibold text-gray-600">Prefix (optional)</label>
|
|
149
|
+
<input v-model="ops.prefix" class="mt-1 w-full border rounded px-3 py-2" placeholder="user:" />
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
<div class="flex flex-wrap gap-2">
|
|
154
|
+
<button @click="clearCache('memory')" class="px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">
|
|
155
|
+
Clear memory
|
|
156
|
+
</button>
|
|
157
|
+
<button @click="clearCache('mongo')" class="px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">
|
|
158
|
+
Clear mongo
|
|
159
|
+
</button>
|
|
160
|
+
<button @click="clearCache('redis')" class="px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">
|
|
161
|
+
Clear redis
|
|
162
|
+
</button>
|
|
163
|
+
<button @click="clearCache('all')" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700">
|
|
164
|
+
Clear all
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div v-if="clearStatus" class="text-sm text-gray-600">{{ clearStatus }}</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<!-- Create new entry -->
|
|
173
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
174
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
175
|
+
<div class="text-sm font-semibold text-gray-800">Create new entry</div>
|
|
176
|
+
</div>
|
|
177
|
+
<div class="p-4 space-y-4">
|
|
178
|
+
<div class="grid grid-cols-2 gap-4">
|
|
179
|
+
<div>
|
|
180
|
+
<label class="text-xs font-semibold text-gray-600">Namespace</label>
|
|
181
|
+
<input v-model="create.namespace" class="mt-1 w-full border rounded px-3 py-2" placeholder="default" />
|
|
182
|
+
</div>
|
|
183
|
+
<div>
|
|
184
|
+
<label class="text-xs font-semibold text-gray-600">Key</label>
|
|
185
|
+
<input v-model="create.key" class="mt-1 w-full border rounded px-3 py-2" placeholder="my-key" />
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
|
|
189
|
+
<div class="grid grid-cols-2 gap-4">
|
|
190
|
+
<div>
|
|
191
|
+
<label class="text-xs font-semibold text-gray-600">TTL seconds</label>
|
|
192
|
+
<input v-model.number="create.ttlSeconds" class="mt-1 w-full border rounded px-3 py-2" placeholder="600" />
|
|
193
|
+
<div class="text-xs text-gray-400 mt-1">Use null for no-expiry.</div>
|
|
194
|
+
</div>
|
|
195
|
+
<div>
|
|
196
|
+
<label class="text-xs font-semibold text-gray-600">At-rest format</label>
|
|
197
|
+
<select v-model="create.atRestFormat" class="mt-1 w-full border rounded px-3 py-2">
|
|
198
|
+
<option value="">(use global)</option>
|
|
199
|
+
<option value="string">string</option>
|
|
200
|
+
<option value="base64">base64</option>
|
|
201
|
+
</select>
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
<div>
|
|
206
|
+
<label class="text-xs font-semibold text-gray-600">Value</label>
|
|
207
|
+
<textarea v-model="create.value" class="mt-1 w-full h-32 border rounded px-3 py-2 font-mono text-xs" placeholder='{"example": "data"} or plain text'></textarea>
|
|
208
|
+
<div class="text-xs text-gray-400 mt-1">JSON will be auto-decoded on reads. Plain text stored as-is.</div>
|
|
209
|
+
</div>
|
|
210
|
+
|
|
211
|
+
<div class="flex items-center gap-2">
|
|
212
|
+
<button @click="createEntry" class="px-3 py-2 rounded bg-green-600 text-white text-sm hover:bg-green-700">
|
|
213
|
+
<i class="ti ti-plus mr-1"></i> Create entry
|
|
214
|
+
</button>
|
|
215
|
+
<div v-if="createStatus" class="text-sm text-gray-600">{{ createStatus }}</div>
|
|
216
|
+
</div>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
|
|
220
|
+
<!-- Information -->
|
|
221
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
222
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
223
|
+
<div class="text-sm font-semibold text-gray-800">Information</div>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="p-4 space-y-4">
|
|
226
|
+
<div class="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-700">
|
|
227
|
+
<div class="font-semibold text-gray-800 mb-2">Programmatic Usage</div>
|
|
228
|
+
<div class="space-y-2">
|
|
229
|
+
<div><pre class="bg-gray-800 text-gray-100 p-2 rounded overflow-x-auto">const { cacheLayer } = require('superbackend');
|
|
230
|
+
|
|
231
|
+
// Set a cache entry
|
|
232
|
+
await cacheLayer.set('my-key', { data: 'value' }, {
|
|
233
|
+
namespace: 'my-app',
|
|
234
|
+
ttlSeconds: 600
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Get a cache entry
|
|
238
|
+
const value = await cacheLayer.get('my-key', {
|
|
239
|
+
namespace: 'my-app'
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// Delete a cache entry
|
|
243
|
+
await cacheLayer.delete('my-key', {
|
|
244
|
+
namespace: 'my-app'
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// Clear cache
|
|
248
|
+
await cacheLayer.clear({ backend: 'all' });</pre></div>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<div class="bg-gray-50 border border-gray-200 rounded p-3 text-xs text-gray-700">
|
|
253
|
+
<div class="font-semibold text-gray-800 mb-2">API Usage</div>
|
|
254
|
+
<div class="space-y-2">
|
|
255
|
+
<div><pre class="bg-gray-800 text-gray-100 p-2 rounded overflow-x-auto"># Set a cache entry
|
|
256
|
+
curl -X PUT /api/admin/cache/entry \
|
|
257
|
+
-H "Content-Type: application/json" \
|
|
258
|
+
-d '{
|
|
259
|
+
"namespace": "my-app",
|
|
260
|
+
"key": "my-key",
|
|
261
|
+
"value": {"data": "value"},
|
|
262
|
+
"ttlSeconds": 600
|
|
263
|
+
}'
|
|
264
|
+
|
|
265
|
+
# Get a cache entry
|
|
266
|
+
curl "/api/admin/cache/entry?namespace=my-app&key=my-key"
|
|
267
|
+
|
|
268
|
+
# Delete a cache entry
|
|
269
|
+
curl -X DELETE "/api/admin/cache/entry?namespace=my-app&key=my-key"
|
|
270
|
+
|
|
271
|
+
# List keys
|
|
272
|
+
curl "/api/admin/cache/keys?namespace=my-app"
|
|
273
|
+
|
|
274
|
+
# Clear cache
|
|
275
|
+
curl -X POST /api/admin/cache/clear \
|
|
276
|
+
-H "Content-Type: application/json" \
|
|
277
|
+
-d '{"backend": "all"}'</pre></div>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="bg-blue-50 border border-blue-200 rounded p-3 text-xs text-blue-700">
|
|
282
|
+
<div class="font-semibold text-blue-800 mb-2">Quick Tips</div>
|
|
283
|
+
<div class="space-y-1">
|
|
284
|
+
<div>• Use namespaces to isolate cache keys by app or feature</div>
|
|
285
|
+
<div>• JSON values are automatically decoded when retrieved</div>
|
|
286
|
+
<div>• Set TTL to null for entries that should never expire</div>
|
|
287
|
+
<div>• Use base64 format for binary data or special characters</div>
|
|
288
|
+
<div>• Monitor metrics to optimize your cache hit ratio</div>
|
|
289
|
+
</div>
|
|
290
|
+
</div>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
<!-- Metrics -->
|
|
295
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
296
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
297
|
+
<div class="text-sm font-semibold text-gray-800">Metrics</div>
|
|
298
|
+
</div>
|
|
299
|
+
<div class="p-4">
|
|
300
|
+
<div v-if="metrics" class="grid grid-cols-2 gap-4 text-sm">
|
|
301
|
+
<div>
|
|
302
|
+
<div class="text-xs text-gray-500">Backend</div>
|
|
303
|
+
<div class="font-semibold">{{ metrics.backend }}</div>
|
|
304
|
+
</div>
|
|
305
|
+
<div>
|
|
306
|
+
<div class="text-xs text-gray-500">Eviction policy</div>
|
|
307
|
+
<div class="font-semibold">{{ metrics.evictionPolicy }}</div>
|
|
308
|
+
</div>
|
|
309
|
+
<div>
|
|
310
|
+
<div class="text-xs text-gray-500">Memory entries</div>
|
|
311
|
+
<div class="font-semibold">{{ metrics.memory.entries }}</div>
|
|
312
|
+
</div>
|
|
313
|
+
<div>
|
|
314
|
+
<div class="text-xs text-gray-500">Memory bytes (est.)</div>
|
|
315
|
+
<div class="font-semibold">{{ metrics.memory.estimatedBytes }}</div>
|
|
316
|
+
</div>
|
|
317
|
+
<div>
|
|
318
|
+
<div class="text-xs text-gray-500">Mongo entries</div>
|
|
319
|
+
<div class="font-semibold">{{ metrics.mongo.entries }}</div>
|
|
320
|
+
</div>
|
|
321
|
+
<div>
|
|
322
|
+
<div class="text-xs text-gray-500">Mongo bytes (est.)</div>
|
|
323
|
+
<div class="font-semibold">{{ metrics.mongo.estimatedBytes }}</div>
|
|
324
|
+
</div>
|
|
325
|
+
<div>
|
|
326
|
+
<div class="text-xs text-gray-500">Hits / Misses</div>
|
|
327
|
+
<div class="font-semibold">{{ metrics.memory.hits }} / {{ metrics.memory.misses }}</div>
|
|
328
|
+
</div>
|
|
329
|
+
<div>
|
|
330
|
+
<div class="text-xs text-gray-500">Offloads</div>
|
|
331
|
+
<div class="font-semibold">{{ metrics.memory.offloads }}</div>
|
|
332
|
+
</div>
|
|
333
|
+
<div v-if="metrics.redis">
|
|
334
|
+
<div class="text-xs text-gray-500">Redis memory bytes</div>
|
|
335
|
+
<div class="font-semibold">{{ metrics.redis.usedMemoryBytes || metrics.redis.error }}</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
<div v-else class="text-sm text-gray-500">No metrics yet.</div>
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
<!-- Right column: Explorer -->
|
|
344
|
+
<div class="col-span-7">
|
|
345
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
346
|
+
<div class="p-4 border-b border-gray-200 flex items-center justify-between">
|
|
347
|
+
<div class="text-sm font-semibold text-gray-800">Explorer</div>
|
|
348
|
+
<div class="flex items-center gap-2">
|
|
349
|
+
<input v-model="explorer.namespace" class="border rounded px-3 py-2 text-sm" placeholder="namespace (optional)" />
|
|
350
|
+
<input v-model="explorer.prefix" class="border rounded px-3 py-2 text-sm" placeholder="prefix (optional)" />
|
|
351
|
+
<button @click="loadKeys" class="px-3 py-2 rounded bg-gray-600 text-white text-sm hover:bg-gray-700">
|
|
352
|
+
Load keys
|
|
353
|
+
</button>
|
|
354
|
+
</div>
|
|
355
|
+
</div>
|
|
356
|
+
|
|
357
|
+
<div class="p-4 grid grid-cols-12 gap-4">
|
|
358
|
+
<div class="col-span-5">
|
|
359
|
+
<div class="text-xs font-semibold text-gray-500 mb-2">Keys</div>
|
|
360
|
+
<div class="border rounded p-2 max-h-[70vh] overflow-auto">
|
|
361
|
+
<div v-if="Array.isArray(keys)" class="space-y-1">
|
|
362
|
+
<button
|
|
363
|
+
v-for="it in keys"
|
|
364
|
+
:key="it.namespace + ':' + it.key + ':' + it.backend"
|
|
365
|
+
@click="selectEntry(it.namespace, it.key)"
|
|
366
|
+
class="w-full text-left px-2 py-1 rounded hover:bg-gray-50"
|
|
367
|
+
>
|
|
368
|
+
<div class="flex items-center justify-between">
|
|
369
|
+
<div class="font-mono text-xs">{{ it.namespace }}:{{ it.key }}</div>
|
|
370
|
+
<div class="text-[10px] text-gray-500">{{ it.backend }}</div>
|
|
371
|
+
</div>
|
|
372
|
+
</button>
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<div v-else>
|
|
376
|
+
<div class="text-xs text-gray-500 mb-2">Memory</div>
|
|
377
|
+
<div class="space-y-1">
|
|
378
|
+
<button
|
|
379
|
+
v-for="it in keys.memory"
|
|
380
|
+
:key="it.namespace + ':' + it.key + ':' + it.backend"
|
|
381
|
+
@click="selectEntry(it.namespace, it.key)"
|
|
382
|
+
class="w-full text-left px-2 py-1 rounded hover:bg-gray-50"
|
|
383
|
+
>
|
|
384
|
+
<div class="flex items-center justify-between">
|
|
385
|
+
<div class="font-mono text-xs">{{ it.namespace }}:{{ it.key }}</div>
|
|
386
|
+
<div class="text-[10px] text-gray-500">{{ it.backend }}</div>
|
|
387
|
+
</div>
|
|
388
|
+
</button>
|
|
389
|
+
</div>
|
|
390
|
+
|
|
391
|
+
<div class="text-xs text-gray-500 mt-4 mb-2">Mongo</div>
|
|
392
|
+
<div class="space-y-1">
|
|
393
|
+
<button
|
|
394
|
+
v-for="it in keys.mongo"
|
|
395
|
+
:key="it.namespace + ':' + it.key + ':' + it.backend"
|
|
396
|
+
@click="selectEntry(it.namespace, it.key)"
|
|
397
|
+
class="w-full text-left px-2 py-1 rounded hover:bg-gray-50"
|
|
398
|
+
>
|
|
399
|
+
<div class="flex items-center justify-between">
|
|
400
|
+
<div class="font-mono text-xs">{{ it.namespace }}:{{ it.key }}</div>
|
|
401
|
+
<div class="text-[10px] text-gray-500">{{ it.backend }}</div>
|
|
402
|
+
</div>
|
|
403
|
+
</button>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
</div>
|
|
408
|
+
|
|
409
|
+
<div class="col-span-7">
|
|
410
|
+
<div class="text-xs font-semibold text-gray-500 mb-2">Entry</div>
|
|
411
|
+
|
|
412
|
+
<div v-if="selected" class="border rounded p-3 space-y-3">
|
|
413
|
+
<div class="grid grid-cols-2 gap-4 text-sm">
|
|
414
|
+
<div>
|
|
415
|
+
<div class="text-xs text-gray-500">Namespace</div>
|
|
416
|
+
<div class="font-mono">{{ selected.namespace }}</div>
|
|
417
|
+
</div>
|
|
418
|
+
<div>
|
|
419
|
+
<div class="text-xs text-gray-500">Key</div>
|
|
420
|
+
<div class="font-mono">{{ selected.key }}</div>
|
|
421
|
+
</div>
|
|
422
|
+
<div>
|
|
423
|
+
<div class="text-xs text-gray-500">Backend</div>
|
|
424
|
+
<div class="font-semibold">{{ selected.backend }}</div>
|
|
425
|
+
</div>
|
|
426
|
+
<div>
|
|
427
|
+
<div class="text-xs text-gray-500">At-rest format</div>
|
|
428
|
+
<div class="font-semibold">{{ selected.atRestFormat || 'string' }}</div>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
<div class="grid grid-cols-2 gap-4">
|
|
433
|
+
<div>
|
|
434
|
+
<label class="text-xs font-semibold text-gray-600">TTL seconds</label>
|
|
435
|
+
<input v-model="edit.ttlSeconds" class="mt-1 w-full border rounded px-3 py-2" placeholder="e.g. 600 or null" />
|
|
436
|
+
<div class="text-xs text-gray-400 mt-1">Use <span class="font-mono">null</span> for no-expiry.</div>
|
|
437
|
+
</div>
|
|
438
|
+
<div>
|
|
439
|
+
<label class="text-xs font-semibold text-gray-600">At-rest format override</label>
|
|
440
|
+
<select v-model="edit.atRestFormat" class="mt-1 w-full border rounded px-3 py-2">
|
|
441
|
+
<option value="">(use global)</option>
|
|
442
|
+
<option value="string">string</option>
|
|
443
|
+
<option value="base64">base64</option>
|
|
444
|
+
</select>
|
|
445
|
+
</div>
|
|
446
|
+
</div>
|
|
447
|
+
|
|
448
|
+
<div>
|
|
449
|
+
<label class="text-xs font-semibold text-gray-600">Value</label>
|
|
450
|
+
<textarea v-model="edit.value" class="mt-1 w-full h-48 border rounded px-3 py-2 font-mono text-xs"></textarea>
|
|
451
|
+
<div class="text-xs text-gray-400 mt-1">Stored as string at rest. If valid JSON, reads auto-decode to JSON.</div>
|
|
452
|
+
</div>
|
|
453
|
+
|
|
454
|
+
<div class="flex items-center gap-2">
|
|
455
|
+
<button @click="saveEntry" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">
|
|
456
|
+
<i class="ti ti-device-floppy mr-1"></i> Save entry
|
|
457
|
+
</button>
|
|
458
|
+
<button @click="deleteEntry" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700">
|
|
459
|
+
<i class="ti ti-trash mr-1"></i> Delete
|
|
460
|
+
</button>
|
|
461
|
+
<div v-if="entryStatus" class="text-sm text-gray-600">{{ entryStatus }}</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
<div v-else class="text-sm text-gray-500">Select a key to view/edit its value.</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
</div>
|
|
473
|
+
|
|
474
|
+
<style>
|
|
475
|
+
[v-cloak] { display: none; }
|
|
476
|
+
</style>
|
|
477
|
+
|
|
478
|
+
<script>
|
|
479
|
+
window.BASE_URL = '<%= baseUrl %>';
|
|
480
|
+
|
|
481
|
+
const { createApp } = Vue;
|
|
482
|
+
|
|
483
|
+
createApp({
|
|
484
|
+
data() {
|
|
485
|
+
return {
|
|
486
|
+
config: {
|
|
487
|
+
backend: 'memory',
|
|
488
|
+
evictionPolicy: 'lru',
|
|
489
|
+
redisPrefix: 'superbackend:',
|
|
490
|
+
redisUrl: '',
|
|
491
|
+
offloadThresholdBytes: 5 * 1024 * 1024,
|
|
492
|
+
maxEntryBytes: 256 * 1024,
|
|
493
|
+
defaultTtlSeconds: 600,
|
|
494
|
+
atRestFormat: 'string',
|
|
495
|
+
},
|
|
496
|
+
saveStatus: '',
|
|
497
|
+
clearStatus: '',
|
|
498
|
+
entryStatus: '',
|
|
499
|
+
createStatus: '',
|
|
500
|
+
metrics: null,
|
|
501
|
+
keys: { memory: [], mongo: [] },
|
|
502
|
+
selected: null,
|
|
503
|
+
edit: { ttlSeconds: '', atRestFormat: '', value: '' },
|
|
504
|
+
create: { namespace: '', key: '', ttlSeconds: 600, atRestFormat: '', value: '' },
|
|
505
|
+
explorer: { namespace: '', prefix: '' },
|
|
506
|
+
ops: { namespace: '', prefix: '' },
|
|
507
|
+
};
|
|
508
|
+
},
|
|
509
|
+
methods: {
|
|
510
|
+
async api(path, opts) {
|
|
511
|
+
const base = window.BASE_URL || '';
|
|
512
|
+
const res = await fetch(base + path, {
|
|
513
|
+
credentials: 'same-origin',
|
|
514
|
+
headers: { 'Content-Type': 'application/json' },
|
|
515
|
+
...opts,
|
|
516
|
+
});
|
|
517
|
+
const json = await res.json().catch(() => ({}));
|
|
518
|
+
if (!res.ok) throw new Error(json.error || 'Request failed');
|
|
519
|
+
return json;
|
|
520
|
+
},
|
|
521
|
+
async loadConfig() {
|
|
522
|
+
const data = await this.api('/api/admin/cache/config', { method: 'GET' });
|
|
523
|
+
const cfg = data.config || {};
|
|
524
|
+
this.config.backend = cfg.backend || 'memory';
|
|
525
|
+
this.config.evictionPolicy = cfg.evictionPolicy || 'lru';
|
|
526
|
+
this.config.redisPrefix = cfg.redisPrefix || 'superbackend:';
|
|
527
|
+
this.config.offloadThresholdBytes = cfg.offloadThresholdBytes || this.config.offloadThresholdBytes;
|
|
528
|
+
this.config.maxEntryBytes = cfg.maxEntryBytes || this.config.maxEntryBytes;
|
|
529
|
+
this.config.defaultTtlSeconds = cfg.defaultTtlSeconds || this.config.defaultTtlSeconds;
|
|
530
|
+
this.config.atRestFormat = cfg.atRestFormat || 'string';
|
|
531
|
+
// URL is not returned; keep existing input
|
|
532
|
+
},
|
|
533
|
+
async saveConfig() {
|
|
534
|
+
this.saveStatus = '';
|
|
535
|
+
try {
|
|
536
|
+
await this.api('/api/admin/cache/config', {
|
|
537
|
+
method: 'PUT',
|
|
538
|
+
body: JSON.stringify({
|
|
539
|
+
backend: this.config.backend,
|
|
540
|
+
evictionPolicy: this.config.evictionPolicy,
|
|
541
|
+
redisUrl: this.config.redisUrl,
|
|
542
|
+
redisPrefix: this.config.redisPrefix,
|
|
543
|
+
offloadThresholdBytes: this.config.offloadThresholdBytes,
|
|
544
|
+
maxEntryBytes: this.config.maxEntryBytes,
|
|
545
|
+
defaultTtlSeconds: this.config.defaultTtlSeconds,
|
|
546
|
+
atRestFormat: this.config.atRestFormat,
|
|
547
|
+
}),
|
|
548
|
+
});
|
|
549
|
+
this.saveStatus = 'Saved.';
|
|
550
|
+
await this.loadAll();
|
|
551
|
+
} catch (e) {
|
|
552
|
+
this.saveStatus = e.message || 'Failed.';
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
async loadMetrics() {
|
|
556
|
+
const data = await this.api('/api/admin/cache/metrics', { method: 'GET' });
|
|
557
|
+
this.metrics = data.metrics || null;
|
|
558
|
+
},
|
|
559
|
+
async clearCache(backend) {
|
|
560
|
+
this.clearStatus = '';
|
|
561
|
+
try {
|
|
562
|
+
const payload = {
|
|
563
|
+
backend,
|
|
564
|
+
namespace: this.ops.namespace || undefined,
|
|
565
|
+
prefix: this.ops.prefix || undefined,
|
|
566
|
+
};
|
|
567
|
+
const data = await this.api('/api/admin/cache/clear', { method: 'POST', body: JSON.stringify(payload) });
|
|
568
|
+
this.clearStatus = `Cleared: ${JSON.stringify(data.cleared)}`;
|
|
569
|
+
await this.loadAll();
|
|
570
|
+
} catch (e) {
|
|
571
|
+
this.clearStatus = e.message || 'Failed.';
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
async loadKeys() {
|
|
575
|
+
const qs = new URLSearchParams();
|
|
576
|
+
if (this.explorer.namespace) qs.set('namespace', this.explorer.namespace);
|
|
577
|
+
if (this.explorer.prefix) qs.set('prefix', this.explorer.prefix);
|
|
578
|
+
const data = await this.api('/api/admin/cache/keys?' + qs.toString(), { method: 'GET' });
|
|
579
|
+
this.keys = data.items;
|
|
580
|
+
this.selected = null;
|
|
581
|
+
},
|
|
582
|
+
async selectEntry(namespace, key) {
|
|
583
|
+
const qs = new URLSearchParams();
|
|
584
|
+
qs.set('namespace', namespace);
|
|
585
|
+
qs.set('key', key);
|
|
586
|
+
const data = await this.api('/api/admin/cache/entry?' + qs.toString(), { method: 'GET' });
|
|
587
|
+
this.selected = data.item;
|
|
588
|
+
this.edit.value = this.selected.value || '';
|
|
589
|
+
this.edit.ttlSeconds = '';
|
|
590
|
+
this.edit.atRestFormat = '';
|
|
591
|
+
this.entryStatus = '';
|
|
592
|
+
},
|
|
593
|
+
async saveEntry() {
|
|
594
|
+
if (!this.selected) return;
|
|
595
|
+
this.entryStatus = '';
|
|
596
|
+
try {
|
|
597
|
+
let ttlSeconds = this.edit.ttlSeconds;
|
|
598
|
+
if (ttlSeconds === 'null') ttlSeconds = null;
|
|
599
|
+
if (ttlSeconds === '') ttlSeconds = undefined;
|
|
600
|
+
|
|
601
|
+
await this.api('/api/admin/cache/entry', {
|
|
602
|
+
method: 'PUT',
|
|
603
|
+
body: JSON.stringify({
|
|
604
|
+
namespace: this.selected.namespace,
|
|
605
|
+
key: this.selected.key,
|
|
606
|
+
value: this.edit.value,
|
|
607
|
+
ttlSeconds,
|
|
608
|
+
atRestFormat: this.edit.atRestFormat || undefined,
|
|
609
|
+
}),
|
|
610
|
+
});
|
|
611
|
+
this.entryStatus = 'Saved.';
|
|
612
|
+
await this.selectEntry(this.selected.namespace, this.selected.key);
|
|
613
|
+
await this.loadKeys();
|
|
614
|
+
await this.loadMetrics();
|
|
615
|
+
} catch (e) {
|
|
616
|
+
this.entryStatus = e.message || 'Failed.';
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
async deleteEntry() {
|
|
620
|
+
if (!this.selected) return;
|
|
621
|
+
this.entryStatus = '';
|
|
622
|
+
try {
|
|
623
|
+
const qs = new URLSearchParams();
|
|
624
|
+
qs.set('namespace', this.selected.namespace);
|
|
625
|
+
qs.set('key', this.selected.key);
|
|
626
|
+
await this.api('/api/admin/cache/entry?' + qs.toString(), { method: 'DELETE' });
|
|
627
|
+
this.entryStatus = 'Deleted.';
|
|
628
|
+
this.selected = null;
|
|
629
|
+
await this.loadKeys();
|
|
630
|
+
await this.loadMetrics();
|
|
631
|
+
} catch (e) {
|
|
632
|
+
this.entryStatus = e.message || 'Failed.';
|
|
633
|
+
}
|
|
634
|
+
},
|
|
635
|
+
async createEntry() {
|
|
636
|
+
this.createStatus = '';
|
|
637
|
+
try {
|
|
638
|
+
if (!this.create.key) {
|
|
639
|
+
throw new Error('Key is required');
|
|
640
|
+
}
|
|
641
|
+
if (this.create.value === '') {
|
|
642
|
+
throw new Error('Value is required');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
await this.api('/api/admin/cache/entry', {
|
|
646
|
+
method: 'PUT',
|
|
647
|
+
body: JSON.stringify({
|
|
648
|
+
namespace: this.create.namespace || 'default',
|
|
649
|
+
key: this.create.key,
|
|
650
|
+
value: this.create.value,
|
|
651
|
+
ttlSeconds: this.create.ttlSeconds,
|
|
652
|
+
atRestFormat: this.create.atRestFormat || undefined,
|
|
653
|
+
}),
|
|
654
|
+
});
|
|
655
|
+
this.createStatus = 'Created.';
|
|
656
|
+
// Reset form
|
|
657
|
+
this.create.key = '';
|
|
658
|
+
this.create.value = '';
|
|
659
|
+
this.create.namespace = '';
|
|
660
|
+
this.create.atRestFormat = '';
|
|
661
|
+
this.create.ttlSeconds = 600;
|
|
662
|
+
// Refresh keys and metrics
|
|
663
|
+
await this.loadKeys();
|
|
664
|
+
await this.loadMetrics();
|
|
665
|
+
} catch (e) {
|
|
666
|
+
this.createStatus = e.message || 'Failed.';
|
|
667
|
+
}
|
|
668
|
+
},
|
|
669
|
+
async loadAll() {
|
|
670
|
+
await this.loadConfig();
|
|
671
|
+
await this.loadMetrics();
|
|
672
|
+
await this.loadKeys();
|
|
673
|
+
},
|
|
674
|
+
},
|
|
675
|
+
async mounted() {
|
|
676
|
+
await this.loadAll();
|
|
677
|
+
},
|
|
678
|
+
}).mount('#app');
|
|
679
|
+
</script>
|
|
680
|
+
</body>
|
|
681
|
+
</html>
|