@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.
Files changed (195) hide show
  1. package/.env.example +5 -0
  2. package/README.md +11 -0
  3. package/index.js +39 -1
  4. package/package.json +11 -3
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/ui-components/browser/src/index.js +228 -0
  7. package/src/admin/endpointRegistry.js +120 -0
  8. package/src/controllers/admin.controller.js +111 -5
  9. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  10. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  11. package/src/controllers/adminCache.controller.js +342 -0
  12. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  13. package/src/controllers/adminCrons.controller.js +388 -0
  14. package/src/controllers/adminDbBrowser.controller.js +124 -0
  15. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  16. package/src/controllers/adminHeadless.controller.js +91 -2
  17. package/src/controllers/adminHealthChecks.controller.js +570 -0
  18. package/src/controllers/adminI18n.controller.js +51 -29
  19. package/src/controllers/adminLlm.controller.js +126 -2
  20. package/src/controllers/adminPages.controller.js +720 -0
  21. package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
  22. package/src/controllers/adminProxy.controller.js +113 -0
  23. package/src/controllers/adminRateLimits.controller.js +138 -0
  24. package/src/controllers/adminRbac.controller.js +803 -0
  25. package/src/controllers/adminScripts.controller.js +320 -0
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/adminTerminals.controller.js +39 -0
  28. package/src/controllers/adminUiComponents.controller.js +315 -0
  29. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  30. package/src/controllers/blogAdmin.controller.js +279 -0
  31. package/src/controllers/blogAiAdmin.controller.js +224 -0
  32. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  33. package/src/controllers/blogInternal.controller.js +26 -0
  34. package/src/controllers/blogPublic.controller.js +89 -0
  35. package/src/controllers/fileManager.controller.js +190 -0
  36. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  37. package/src/controllers/healthChecksPublic.controller.js +196 -0
  38. package/src/controllers/metrics.controller.js +64 -4
  39. package/src/controllers/orgAdmin.controller.js +366 -0
  40. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  41. package/src/middleware/auth.js +7 -0
  42. package/src/middleware/internalCronAuth.js +29 -0
  43. package/src/middleware/rbac.js +62 -0
  44. package/src/middleware.js +879 -56
  45. package/src/models/BlockDefinition.js +27 -0
  46. package/src/models/BlogAutomationLock.js +14 -0
  47. package/src/models/BlogAutomationRun.js +39 -0
  48. package/src/models/BlogPost.js +42 -0
  49. package/src/models/CacheEntry.js +26 -0
  50. package/src/models/ConsoleEntry.js +32 -0
  51. package/src/models/ConsoleLog.js +23 -0
  52. package/src/models/ContextBlockDefinition.js +33 -0
  53. package/src/models/CronExecution.js +47 -0
  54. package/src/models/CronJob.js +70 -0
  55. package/src/models/ExternalDbConnection.js +49 -0
  56. package/src/models/FileEntry.js +22 -0
  57. package/src/models/HeadlessModelDefinition.js +10 -0
  58. package/src/models/HealthAutoHealAttempt.js +57 -0
  59. package/src/models/HealthCheck.js +132 -0
  60. package/src/models/HealthCheckRun.js +51 -0
  61. package/src/models/HealthIncident.js +49 -0
  62. package/src/models/Page.js +95 -0
  63. package/src/models/PageCollection.js +42 -0
  64. package/src/models/ProxyEntry.js +66 -0
  65. package/src/models/RateLimitCounter.js +19 -0
  66. package/src/models/RateLimitMetricBucket.js +20 -0
  67. package/src/models/RbacGrant.js +25 -0
  68. package/src/models/RbacGroup.js +16 -0
  69. package/src/models/RbacGroupMember.js +13 -0
  70. package/src/models/RbacGroupRole.js +13 -0
  71. package/src/models/RbacRole.js +25 -0
  72. package/src/models/RbacUserRole.js +13 -0
  73. package/src/models/ScriptDefinition.js +42 -0
  74. package/src/models/ScriptRun.js +22 -0
  75. package/src/models/UiComponent.js +29 -0
  76. package/src/models/UiComponentProject.js +26 -0
  77. package/src/models/UiComponentProjectComponent.js +18 -0
  78. package/src/routes/admin.routes.js +1 -0
  79. package/src/routes/adminBlog.routes.js +21 -0
  80. package/src/routes/adminBlogAi.routes.js +16 -0
  81. package/src/routes/adminBlogAutomation.routes.js +27 -0
  82. package/src/routes/adminCache.routes.js +20 -0
  83. package/src/routes/adminConsoleManager.routes.js +302 -0
  84. package/src/routes/adminCrons.routes.js +25 -0
  85. package/src/routes/adminDbBrowser.routes.js +65 -0
  86. package/src/routes/adminEjsVirtual.routes.js +2 -1
  87. package/src/routes/adminHeadless.routes.js +8 -1
  88. package/src/routes/adminHealthChecks.routes.js +28 -0
  89. package/src/routes/adminI18n.routes.js +4 -3
  90. package/src/routes/adminLlm.routes.js +4 -2
  91. package/src/routes/adminPages.routes.js +55 -0
  92. package/src/routes/adminProxy.routes.js +15 -0
  93. package/src/routes/adminRateLimits.routes.js +17 -0
  94. package/src/routes/adminRbac.routes.js +38 -0
  95. package/src/routes/adminScripts.routes.js +21 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminTerminals.routes.js +13 -0
  98. package/src/routes/adminUiComponents.routes.js +30 -0
  99. package/src/routes/blogInternal.routes.js +14 -0
  100. package/src/routes/blogPublic.routes.js +9 -0
  101. package/src/routes/fileManager.routes.js +62 -0
  102. package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
  103. package/src/routes/healthChecksPublic.routes.js +9 -0
  104. package/src/routes/log.routes.js +43 -60
  105. package/src/routes/metrics.routes.js +4 -2
  106. package/src/routes/orgAdmin.routes.js +6 -0
  107. package/src/routes/pages.routes.js +123 -0
  108. package/src/routes/proxy.routes.js +46 -0
  109. package/src/routes/rbac.routes.js +47 -0
  110. package/src/routes/uiComponentsPublic.routes.js +9 -0
  111. package/src/routes/webhook.routes.js +2 -1
  112. package/src/routes/workflows.routes.js +4 -0
  113. package/src/services/blockDefinitionsAi.service.js +247 -0
  114. package/src/services/blog.service.js +99 -0
  115. package/src/services/blogAutomation.service.js +978 -0
  116. package/src/services/blogCronsBootstrap.service.js +184 -0
  117. package/src/services/blogPublishing.service.js +58 -0
  118. package/src/services/cacheLayer.service.js +696 -0
  119. package/src/services/consoleManager.service.js +700 -0
  120. package/src/services/consoleOverride.service.js +6 -1
  121. package/src/services/cronScheduler.service.js +350 -0
  122. package/src/services/dbBrowser.service.js +536 -0
  123. package/src/services/ejsVirtual.service.js +102 -32
  124. package/src/services/fileManager.service.js +475 -0
  125. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  126. package/src/services/headlessExternalModels.service.js +292 -0
  127. package/src/services/headlessModels.service.js +26 -6
  128. package/src/services/healthChecks.service.js +650 -0
  129. package/src/services/healthChecksBootstrap.service.js +109 -0
  130. package/src/services/healthChecksScheduler.service.js +106 -0
  131. package/src/services/llmDefaults.service.js +190 -0
  132. package/src/services/migrationAssets/s3.js +2 -2
  133. package/src/services/pages.service.js +602 -0
  134. package/src/services/pagesContext.service.js +331 -0
  135. package/src/services/pagesContextBlocksAi.service.js +349 -0
  136. package/src/services/proxy.service.js +535 -0
  137. package/src/services/rateLimiter.service.js +623 -0
  138. package/src/services/rbac.service.js +212 -0
  139. package/src/services/scriptsRunner.service.js +259 -0
  140. package/src/services/terminals.service.js +152 -0
  141. package/src/services/terminalsWs.service.js +100 -0
  142. package/src/services/uiComponentsAi.service.js +299 -0
  143. package/src/services/uiComponentsCrypto.service.js +39 -0
  144. package/src/services/workflow.service.js +23 -8
  145. package/src/utils/orgRoles.js +14 -0
  146. package/src/utils/rbac/engine.js +60 -0
  147. package/src/utils/rbac/rightsRegistry.js +29 -0
  148. package/views/admin-blog-automation.ejs +877 -0
  149. package/views/admin-blog-edit.ejs +542 -0
  150. package/views/admin-blog.ejs +399 -0
  151. package/views/admin-cache.ejs +681 -0
  152. package/views/admin-console-manager.ejs +680 -0
  153. package/views/admin-crons.ejs +645 -0
  154. package/views/admin-db-browser.ejs +445 -0
  155. package/views/admin-ejs-virtual.ejs +16 -10
  156. package/views/admin-file-manager.ejs +942 -0
  157. package/views/admin-headless.ejs +294 -24
  158. package/views/admin-health-checks.ejs +725 -0
  159. package/views/admin-i18n.ejs +59 -5
  160. package/views/admin-llm.ejs +99 -1
  161. package/views/admin-organizations.ejs +528 -10
  162. package/views/admin-pages.ejs +2424 -0
  163. package/views/admin-proxy.ejs +491 -0
  164. package/views/admin-rate-limiter.ejs +625 -0
  165. package/views/admin-rbac.ejs +1331 -0
  166. package/views/admin-scripts.ejs +497 -0
  167. package/views/admin-seo-config.ejs +61 -7
  168. package/views/admin-terminals.ejs +328 -0
  169. package/views/admin-ui-components.ejs +741 -0
  170. package/views/admin-users.ejs +261 -4
  171. package/views/admin-workflows.ejs +7 -7
  172. package/views/file-manager.ejs +866 -0
  173. package/views/pages/blocks/contact.ejs +27 -0
  174. package/views/pages/blocks/cta.ejs +18 -0
  175. package/views/pages/blocks/faq.ejs +20 -0
  176. package/views/pages/blocks/features.ejs +19 -0
  177. package/views/pages/blocks/hero.ejs +13 -0
  178. package/views/pages/blocks/html.ejs +5 -0
  179. package/views/pages/blocks/image.ejs +14 -0
  180. package/views/pages/blocks/testimonials.ejs +26 -0
  181. package/views/pages/blocks/text.ejs +10 -0
  182. package/views/pages/layouts/default.ejs +51 -0
  183. package/views/pages/layouts/minimal.ejs +42 -0
  184. package/views/pages/layouts/sidebar.ejs +54 -0
  185. package/views/pages/partials/footer.ejs +13 -0
  186. package/views/pages/partials/header.ejs +12 -0
  187. package/views/pages/partials/sidebar.ejs +8 -0
  188. package/views/pages/runtime/page.ejs +10 -0
  189. package/views/pages/templates/article.ejs +20 -0
  190. package/views/pages/templates/default.ejs +12 -0
  191. package/views/pages/templates/landing.ejs +14 -0
  192. package/views/pages/templates/listing.ejs +15 -0
  193. package/views/partials/admin-image-upload-modal.ejs +221 -0
  194. package/views/partials/dashboard/nav-items.ejs +14 -0
  195. 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>