@intranefr/superbackend 1.5.0 → 1.5.2

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 (198) hide show
  1. package/.env.example +15 -0
  2. package/README.md +11 -0
  3. package/analysis-only.skill +0 -0
  4. package/index.js +23 -0
  5. package/package.json +8 -2
  6. package/src/admin/endpointRegistry.js +120 -0
  7. package/src/controllers/admin.controller.js +90 -6
  8. package/src/controllers/adminBlockDefinitions.controller.js +127 -0
  9. package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
  10. package/src/controllers/adminCache.controller.js +342 -0
  11. package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
  12. package/src/controllers/adminCrons.controller.js +388 -0
  13. package/src/controllers/adminDbBrowser.controller.js +124 -0
  14. package/src/controllers/adminEjsVirtual.controller.js +13 -3
  15. package/src/controllers/adminExperiments.controller.js +200 -0
  16. package/src/controllers/adminHeadless.controller.js +9 -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 +126 -4
  26. package/src/controllers/adminSeoConfig.controller.js +71 -48
  27. package/src/controllers/blogAdmin.controller.js +279 -0
  28. package/src/controllers/blogAiAdmin.controller.js +224 -0
  29. package/src/controllers/blogAutomationAdmin.controller.js +141 -0
  30. package/src/controllers/blogInternal.controller.js +26 -0
  31. package/src/controllers/blogPublic.controller.js +89 -0
  32. package/src/controllers/experiments.controller.js +85 -0
  33. package/src/controllers/fileManager.controller.js +190 -0
  34. package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
  35. package/src/controllers/healthChecksPublic.controller.js +196 -0
  36. package/src/controllers/internalExperiments.controller.js +17 -0
  37. package/src/controllers/metrics.controller.js +64 -4
  38. package/src/controllers/orgAdmin.controller.js +80 -0
  39. package/src/helpers/mongooseHelper.js +258 -0
  40. package/src/helpers/scriptBase.js +230 -0
  41. package/src/helpers/scriptRunner.js +335 -0
  42. package/src/middleware/rbac.js +62 -0
  43. package/src/middleware.js +810 -48
  44. package/src/models/BlockDefinition.js +27 -0
  45. package/src/models/BlogAutomationLock.js +14 -0
  46. package/src/models/BlogAutomationRun.js +39 -0
  47. package/src/models/BlogPost.js +42 -0
  48. package/src/models/CacheEntry.js +26 -0
  49. package/src/models/ConsoleEntry.js +32 -0
  50. package/src/models/ConsoleLog.js +23 -0
  51. package/src/models/ContextBlockDefinition.js +33 -0
  52. package/src/models/CronExecution.js +47 -0
  53. package/src/models/CronJob.js +70 -0
  54. package/src/models/Experiment.js +75 -0
  55. package/src/models/ExperimentAssignment.js +23 -0
  56. package/src/models/ExperimentEvent.js +26 -0
  57. package/src/models/ExperimentMetricBucket.js +30 -0
  58. package/src/models/ExternalDbConnection.js +49 -0
  59. package/src/models/FileEntry.js +22 -0
  60. package/src/models/GlobalSetting.js +1 -2
  61. package/src/models/HealthAutoHealAttempt.js +57 -0
  62. package/src/models/HealthCheck.js +132 -0
  63. package/src/models/HealthCheckRun.js +51 -0
  64. package/src/models/HealthIncident.js +49 -0
  65. package/src/models/Page.js +95 -0
  66. package/src/models/PageCollection.js +42 -0
  67. package/src/models/ProxyEntry.js +66 -0
  68. package/src/models/RateLimitCounter.js +19 -0
  69. package/src/models/RateLimitMetricBucket.js +20 -0
  70. package/src/models/RbacGrant.js +25 -0
  71. package/src/models/RbacGroup.js +16 -0
  72. package/src/models/RbacGroupMember.js +13 -0
  73. package/src/models/RbacGroupRole.js +13 -0
  74. package/src/models/RbacRole.js +25 -0
  75. package/src/models/RbacUserRole.js +13 -0
  76. package/src/models/ScriptDefinition.js +1 -0
  77. package/src/models/Webhook.js +2 -0
  78. package/src/routes/admin.routes.js +2 -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/adminExperiments.routes.js +29 -0
  88. package/src/routes/adminHeadless.routes.js +2 -1
  89. package/src/routes/adminHealthChecks.routes.js +28 -0
  90. package/src/routes/adminI18n.routes.js +4 -3
  91. package/src/routes/adminLlm.routes.js +4 -2
  92. package/src/routes/adminPages.routes.js +55 -0
  93. package/src/routes/adminProxy.routes.js +15 -0
  94. package/src/routes/adminRateLimits.routes.js +17 -0
  95. package/src/routes/adminRbac.routes.js +38 -0
  96. package/src/routes/adminSeoConfig.routes.js +5 -4
  97. package/src/routes/adminUiComponents.routes.js +2 -1
  98. package/src/routes/blogInternal.routes.js +14 -0
  99. package/src/routes/blogPublic.routes.js +9 -0
  100. package/src/routes/experiments.routes.js +30 -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/internalExperiments.routes.js +15 -0
  105. package/src/routes/log.routes.js +43 -60
  106. package/src/routes/metrics.routes.js +4 -2
  107. package/src/routes/orgAdmin.routes.js +1 -0
  108. package/src/routes/pages.routes.js +123 -0
  109. package/src/routes/proxy.routes.js +46 -0
  110. package/src/routes/rbac.routes.js +47 -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 +185 -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 +738 -0
  120. package/src/services/consoleOverride.service.js +7 -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/experiments.service.js +273 -0
  125. package/src/services/experimentsAggregation.service.js +308 -0
  126. package/src/services/experimentsCronsBootstrap.service.js +118 -0
  127. package/src/services/experimentsRetention.service.js +43 -0
  128. package/src/services/experimentsWs.service.js +134 -0
  129. package/src/services/fileManager.service.js +475 -0
  130. package/src/services/fileManagerStoragePolicy.service.js +285 -0
  131. package/src/services/globalSettings.service.js +15 -0
  132. package/src/services/healthChecks.service.js +650 -0
  133. package/src/services/healthChecksBootstrap.service.js +109 -0
  134. package/src/services/healthChecksScheduler.service.js +106 -0
  135. package/src/services/jsonConfigs.service.js +2 -2
  136. package/src/services/llmDefaults.service.js +190 -0
  137. package/src/services/migrationAssets/s3.js +2 -2
  138. package/src/services/pages.service.js +602 -0
  139. package/src/services/pagesContext.service.js +331 -0
  140. package/src/services/pagesContextBlocksAi.service.js +349 -0
  141. package/src/services/proxy.service.js +535 -0
  142. package/src/services/rateLimiter.service.js +623 -0
  143. package/src/services/rbac.service.js +212 -0
  144. package/src/services/scriptsRunner.service.js +215 -15
  145. package/src/services/uiComponentsAi.service.js +6 -19
  146. package/src/services/workflow.service.js +23 -8
  147. package/src/utils/orgRoles.js +14 -0
  148. package/src/utils/rbac/engine.js +60 -0
  149. package/src/utils/rbac/rightsRegistry.js +33 -0
  150. package/views/admin-blog-automation.ejs +877 -0
  151. package/views/admin-blog-edit.ejs +542 -0
  152. package/views/admin-blog.ejs +399 -0
  153. package/views/admin-cache.ejs +681 -0
  154. package/views/admin-console-manager.ejs +680 -0
  155. package/views/admin-crons.ejs +645 -0
  156. package/views/admin-dashboard.ejs +28 -8
  157. package/views/admin-db-browser.ejs +445 -0
  158. package/views/admin-ejs-virtual.ejs +16 -10
  159. package/views/admin-experiments.ejs +91 -0
  160. package/views/admin-file-manager.ejs +942 -0
  161. package/views/admin-health-checks.ejs +725 -0
  162. package/views/admin-i18n.ejs +59 -5
  163. package/views/admin-llm.ejs +99 -1
  164. package/views/admin-organizations.ejs +163 -1
  165. package/views/admin-pages.ejs +2424 -0
  166. package/views/admin-proxy.ejs +491 -0
  167. package/views/admin-rate-limiter.ejs +625 -0
  168. package/views/admin-rbac.ejs +1331 -0
  169. package/views/admin-scripts.ejs +597 -3
  170. package/views/admin-seo-config.ejs +61 -7
  171. package/views/admin-ui-components.ejs +57 -25
  172. package/views/admin-workflows.ejs +7 -7
  173. package/views/file-manager.ejs +866 -0
  174. package/views/pages/blocks/contact.ejs +27 -0
  175. package/views/pages/blocks/cta.ejs +18 -0
  176. package/views/pages/blocks/faq.ejs +20 -0
  177. package/views/pages/blocks/features.ejs +19 -0
  178. package/views/pages/blocks/hero.ejs +13 -0
  179. package/views/pages/blocks/html.ejs +5 -0
  180. package/views/pages/blocks/image.ejs +14 -0
  181. package/views/pages/blocks/testimonials.ejs +26 -0
  182. package/views/pages/blocks/text.ejs +10 -0
  183. package/views/pages/layouts/default.ejs +51 -0
  184. package/views/pages/layouts/minimal.ejs +42 -0
  185. package/views/pages/layouts/sidebar.ejs +54 -0
  186. package/views/pages/partials/footer.ejs +13 -0
  187. package/views/pages/partials/header.ejs +12 -0
  188. package/views/pages/partials/sidebar.ejs +8 -0
  189. package/views/pages/runtime/page.ejs +10 -0
  190. package/views/pages/templates/article.ejs +20 -0
  191. package/views/pages/templates/default.ejs +12 -0
  192. package/views/pages/templates/landing.ejs +14 -0
  193. package/views/pages/templates/listing.ejs +15 -0
  194. package/views/partials/admin-image-upload-modal.ejs +221 -0
  195. package/views/partials/dashboard/nav-items.ejs +12 -0
  196. package/views/partials/dashboard/palette.ejs +5 -3
  197. package/views/partials/llm-provider-model-picker.ejs +183 -0
  198. package/src/routes/llmUi.routes.js +0 -26
@@ -0,0 +1,680 @@
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 Console Manager</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">Console Manager</h1>
16
+ <div class="text-sm text-gray-500">Manage backend console entries, tags, and persisted logs</div>
17
+ </div>
18
+ <div class="flex items-center gap-2">
19
+ <button @click="refreshAll" 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
+ <button @click="saveConfig" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">
23
+ <i class="ti ti-device-floppy mr-1"></i> Save Config
24
+ </button>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="bg-white border border-gray-200 rounded-lg">
29
+ <div class="p-4 border-b border-gray-200 flex items-center gap-2">
30
+ <button @click="activeTab='entries'" :class="tabClass('entries')" class="px-3 py-2 rounded text-sm">
31
+ Entries
32
+ </button>
33
+ <button @click="activeTab='logs'" :class="tabClass('logs')" class="px-3 py-2 rounded text-sm">
34
+ Logs
35
+ </button>
36
+ <button @click="activeTab='config'" :class="tabClass('config')" class="px-3 py-2 rounded text-sm">
37
+ Config
38
+ </button>
39
+ <div class="flex-1"></div>
40
+ <div v-if="status" class="text-sm text-gray-600">{{ status }}</div>
41
+ </div>
42
+
43
+ <div class="p-4" v-if="activeTab==='entries'">
44
+ <div class="grid grid-cols-12 gap-4 mb-4">
45
+ <div class="col-span-4">
46
+ <label class="text-xs font-semibold text-gray-600">Search</label>
47
+ <input v-model="entriesQuery.q" @keyup.enter="loadEntries" class="mt-1 w-full border rounded px-3 py-2" placeholder="message, hash, topFrame" />
48
+ </div>
49
+ <div class="col-span-2">
50
+ <label class="text-xs font-semibold text-gray-600">Method</label>
51
+ <select v-model="entriesQuery.method" class="mt-1 w-full border rounded px-3 py-2">
52
+ <option value="">(any)</option>
53
+ <option value="debug">debug</option>
54
+ <option value="log">log</option>
55
+ <option value="info">info</option>
56
+ <option value="warn">warn</option>
57
+ <option value="error">error</option>
58
+ </select>
59
+ </div>
60
+ <div class="col-span-2">
61
+ <label class="text-xs font-semibold text-gray-600">Enabled</label>
62
+ <select v-model="entriesQuery.enabled" class="mt-1 w-full border rounded px-3 py-2">
63
+ <option value="">(any)</option>
64
+ <option value="true">enabled</option>
65
+ <option value="false">disabled</option>
66
+ </select>
67
+ </div>
68
+ <div class="col-span-2">
69
+ <label class="text-xs font-semibold text-gray-600">Page size</label>
70
+ <select v-model.number="entriesQuery.pageSize" class="mt-1 w-full border rounded px-3 py-2">
71
+ <option :value="20">20</option>
72
+ <option :value="50">50</option>
73
+ <option :value="100">100</option>
74
+ <option :value="200">200</option>
75
+ </select>
76
+ </div>
77
+ <div class="col-span-2 flex items-end">
78
+ <button @click="loadEntries" class="w-full px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">
79
+ Load
80
+ </button>
81
+ </div>
82
+ </div>
83
+
84
+ <div class="mb-4">
85
+ <div class="text-xs font-semibold text-gray-600 mb-2">Tags</div>
86
+ <div class="flex flex-wrap gap-2">
87
+ <button
88
+ v-for="t in tags"
89
+ :key="t.tag"
90
+ @click="toggleTag(t.tag)"
91
+ class="px-2.5 py-1 rounded-full text-xs border"
92
+ :class="selectedTags.includes(t.tag) ? 'bg-blue-600 text-white border-blue-600' : 'bg-white text-gray-700 border-gray-200 hover:bg-gray-50'"
93
+ >
94
+ {{ t.tag }}
95
+ <span class="ml-1 opacity-75">({{ t.count }})</span>
96
+ </button>
97
+ <div v-if="!tags.length" class="text-xs text-gray-500">No tags yet.</div>
98
+ </div>
99
+ </div>
100
+
101
+ <div class="flex flex-wrap items-center gap-2 mb-4">
102
+ <button @click="bulkEnable(true)" class="px-3 py-2 rounded bg-green-600 text-white text-sm hover:bg-green-700" :disabled="!selectedHashes.length">
103
+ Enable selected
104
+ </button>
105
+ <button @click="bulkEnable(false)" class="px-3 py-2 rounded bg-orange-600 text-white text-sm hover:bg-orange-700" :disabled="!selectedHashes.length">
106
+ Disable selected
107
+ </button>
108
+ <div class="flex items-center gap-2">
109
+ <input v-model="bulkTag" class="border rounded px-3 py-2 text-sm" placeholder="Tag name" />
110
+ <button @click="bulkAddTag" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700" :disabled="!selectedHashes.length || !bulkTag.trim()">
111
+ Add tag
112
+ </button>
113
+ <button @click="bulkRemoveTag" class="px-3 py-2 rounded bg-gray-600 text-white text-sm hover:bg-gray-700" :disabled="!selectedHashes.length || !bulkTag.trim()">
114
+ Remove tag
115
+ </button>
116
+ <button @click="confirmBulkDelete" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700" :disabled="!selectedHashes.length">
117
+ Delete selected
118
+ </button>
119
+ </div>
120
+ <div class="text-sm text-gray-500">Selected: {{ selectedHashes.length }}</div>
121
+ </div>
122
+
123
+ <!-- Confirmation Modal -->
124
+ <div v-if="showDeleteModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
125
+ <div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
126
+ <div class="flex items-center mb-4">
127
+ <i class="ti ti-alert-triangle text-red-600 text-2xl mr-3"></i>
128
+ <h3 class="text-lg font-semibold text-gray-900">Delete Console Entries</h3>
129
+ </div>
130
+ <div class="text-sm text-gray-600 mb-4">
131
+ <p class="mb-2">You are about to delete <span class="font-semibold">{{ selectedHashes.length }}</span> console entr{{ selectedHashes.length === 1 ? 'y' : 'ies' }}.</p>
132
+ <p class="mb-3">This action cannot be undone.</p>
133
+ <label class="flex items-center gap-2">
134
+ <input type="checkbox" v-model="deleteLogsAlso" class="rounded" />
135
+ <span class="text-sm">Also delete associated logs ({{ selectedHashes.length }} entr{{ selectedHashes.length === 1 ? 'y' : 'ies' }})</span>
136
+ </label>
137
+ </div>
138
+ <div class="flex justify-end gap-3">
139
+ <button @click="showDeleteModal = false" class="px-4 py-2 rounded border border-gray-300 text-gray-700 hover:bg-gray-50">
140
+ Cancel
141
+ </button>
142
+ <button @click="bulkDelete" class="px-4 py-2 rounded bg-red-600 text-white hover:bg-red-700" :disabled="isDeleting">
143
+ <span v-if="isDeleting">Deleting...</span>
144
+ <span v-else>Delete{{ deleteLogsAlso ? ' Entries & Logs' : ' Entries' }}</span>
145
+ </button>
146
+ </div>
147
+ </div>
148
+ </div>
149
+
150
+ <div class="overflow-x-auto border border-gray-200 rounded">
151
+ <table class="w-full">
152
+ <thead class="bg-gray-50 border-b border-gray-200">
153
+ <tr>
154
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">
155
+ <input type="checkbox" @change="toggleSelectAll($event)" :checked="allSelected" />
156
+ </th>
157
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Enabled</th>
158
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
159
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Message</th>
160
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Top frame</th>
161
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Last seen</th>
162
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Count</th>
163
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Tags</th>
164
+ </tr>
165
+ </thead>
166
+ <tbody class="divide-y divide-gray-200">
167
+ <tr v-for="it in entries" :key="it.hash" class="hover:bg-gray-50">
168
+ <td class="px-3 py-2">
169
+ <input type="checkbox" :value="it.hash" v-model="selectedHashes" />
170
+ </td>
171
+ <td class="px-3 py-2">
172
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium"
173
+ :class="it.enabled ? 'bg-green-100 text-green-800' : 'bg-gray-100 text-gray-800'">
174
+ {{ it.enabled ? 'enabled' : 'disabled' }}
175
+ </span>
176
+ </td>
177
+ <td class="px-3 py-2 text-xs font-mono">{{ it.method }}</td>
178
+ <td class="px-3 py-2 text-sm text-gray-900">{{ it.messageTemplate }}</td>
179
+ <td class="px-3 py-2 text-xs text-gray-600 font-mono">{{ it.topFrame }}</td>
180
+ <td class="px-3 py-2 text-xs text-gray-700">{{ formatDate(it.lastSeenAt) }}</td>
181
+ <td class="px-3 py-2 text-xs text-gray-700">{{ it.countTotal }}</td>
182
+ <td class="px-3 py-2">
183
+ <span v-for="tg in (it.tags || [])" :key="tg" class="inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-blue-50 text-blue-700 border border-blue-200 mr-1">
184
+ {{ tg }}
185
+ </span>
186
+ </td>
187
+ </tr>
188
+ <tr v-if="!entries.length">
189
+ <td colspan="8" class="px-4 py-6 text-sm text-gray-500">No entries found.</td>
190
+ </tr>
191
+ </tbody>
192
+ </table>
193
+ </div>
194
+
195
+ <div class="flex items-center justify-between mt-4 text-sm text-gray-600">
196
+ <div>
197
+ Page {{ entriesPagination.page }} / {{ entriesPagination.totalPages }} ({{ entriesPagination.total }} total)
198
+ </div>
199
+ <div class="flex items-center gap-2">
200
+ <button @click="prevEntries" class="px-3 py-2 rounded border hover:bg-gray-50" :disabled="entriesPagination.page<=1">Prev</button>
201
+ <button @click="nextEntries" class="px-3 py-2 rounded border hover:bg-gray-50" :disabled="entriesPagination.page>=entriesPagination.totalPages">Next</button>
202
+ </div>
203
+ </div>
204
+ </div>
205
+
206
+ <div class="p-4" v-if="activeTab==='logs'">
207
+ <div class="grid grid-cols-12 gap-4 mb-4">
208
+ <div class="col-span-4">
209
+ <label class="text-xs font-semibold text-gray-600">Search</label>
210
+ <input v-model="logsQuery.q" @keyup.enter="loadLogs" class="mt-1 w-full border rounded px-3 py-2" placeholder="message, argsPreview, entryHash" />
211
+ </div>
212
+ <div class="col-span-2">
213
+ <label class="text-xs font-semibold text-gray-600">Method</label>
214
+ <select v-model="logsQuery.method" class="mt-1 w-full border rounded px-3 py-2">
215
+ <option value="">(any)</option>
216
+ <option value="debug">debug</option>
217
+ <option value="log">log</option>
218
+ <option value="info">info</option>
219
+ <option value="warn">warn</option>
220
+ <option value="error">error</option>
221
+ </select>
222
+ </div>
223
+ <div class="col-span-3">
224
+ <label class="text-xs font-semibold text-gray-600">Entry hash (optional)</label>
225
+ <input v-model="logsQuery.entryHash" class="mt-1 w-full border rounded px-3 py-2 font-mono text-xs" placeholder="hash" />
226
+ </div>
227
+ <div class="col-span-1">
228
+ <label class="text-xs font-semibold text-gray-600">Size</label>
229
+ <select v-model.number="logsQuery.pageSize" class="mt-1 w-full border rounded px-3 py-2">
230
+ <option :value="20">20</option>
231
+ <option :value="50">50</option>
232
+ <option :value="100">100</option>
233
+ <option :value="200">200</option>
234
+ </select>
235
+ </div>
236
+ <div class="col-span-2 flex items-end">
237
+ <button @click="loadLogs" class="w-full px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">Load</button>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="overflow-x-auto border border-gray-200 rounded">
242
+ <table class="w-full">
243
+ <thead class="bg-gray-50 border-b border-gray-200">
244
+ <tr>
245
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">At</th>
246
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Method</th>
247
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Entry</th>
248
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Message</th>
249
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 uppercase">Args</th>
250
+ </tr>
251
+ </thead>
252
+ <tbody class="divide-y divide-gray-200">
253
+ <tr v-for="it in logs" :key="it._id" class="hover:bg-gray-50">
254
+ <td class="px-3 py-2 text-xs text-gray-700">{{ formatDate(it.createdAt) }}</td>
255
+ <td class="px-3 py-2 text-xs font-mono">{{ it.method }}</td>
256
+ <td class="px-3 py-2 text-xs font-mono text-gray-600">{{ it.entryHash }}</td>
257
+ <td class="px-3 py-2 text-sm text-gray-900">{{ it.message }}</td>
258
+ <td class="px-3 py-2 text-xs text-gray-600 font-mono">{{ it.argsPreview }}</td>
259
+ </tr>
260
+ <tr v-if="!logs.length">
261
+ <td colspan="5" class="px-4 py-6 text-sm text-gray-500">No logs found (enable DB persistence in Config).</td>
262
+ </tr>
263
+ </tbody>
264
+ </table>
265
+ </div>
266
+
267
+ <div class="flex items-center justify-between mt-4 text-sm text-gray-600">
268
+ <div>
269
+ Page {{ logsPagination.page }} / {{ logsPagination.totalPages }} ({{ logsPagination.total }} total)
270
+ </div>
271
+ <div class="flex items-center gap-2">
272
+ <button @click="prevLogs" class="px-3 py-2 rounded border hover:bg-gray-50" :disabled="logsPagination.page<=1">Prev</button>
273
+ <button @click="nextLogs" class="px-3 py-2 rounded border hover:bg-gray-50" :disabled="logsPagination.page>=logsPagination.totalPages">Next</button>
274
+ </div>
275
+ </div>
276
+ </div>
277
+
278
+ <div class="p-4" v-if="activeTab==='config'">
279
+ <div class="grid grid-cols-12 gap-6">
280
+ <div class="col-span-6 space-y-4">
281
+ <div class="bg-white border border-gray-200 rounded p-4">
282
+ <div class="text-sm font-semibold text-gray-800 mb-3">General</div>
283
+ <div class="space-y-3">
284
+ <label class="flex items-center gap-2 text-sm">
285
+ <input type="checkbox" v-model="config.defaultEntryEnabled" />
286
+ New entries enabled by default
287
+ </label>
288
+ </div>
289
+ </div>
290
+
291
+ <div class="bg-white border border-gray-200 rounded p-4">
292
+ <div class="text-sm font-semibold text-gray-800 mb-3">Persistence defaults</div>
293
+ <div class="space-y-3">
294
+ <label class="flex items-center gap-2 text-sm">
295
+ <input type="checkbox" v-model="config.defaults.persist.cache" />
296
+ Default persist to Cache
297
+ </label>
298
+ <label class="flex items-center gap-2 text-sm">
299
+ <input type="checkbox" v-model="config.defaults.persist.db" />
300
+ Default persist to DB
301
+ </label>
302
+ <label class="flex items-center gap-2 text-sm">
303
+ <input type="checkbox" v-model="config.defaults.persist.warnErrorToCacheDb" />
304
+ For warn/error, enable persistence defaults for new entries
305
+ </label>
306
+ </div>
307
+ </div>
308
+
309
+ <div class="bg-white border border-gray-200 rounded p-4">
310
+ <div class="text-sm font-semibold text-gray-800 mb-3">DB logs</div>
311
+ <div class="grid grid-cols-2 gap-4">
312
+ <label class="flex items-center gap-2 text-sm">
313
+ <input type="checkbox" v-model="config.db.enabled" />
314
+ Enable DB persistence
315
+ </label>
316
+ <div>
317
+ <label class="text-xs font-semibold text-gray-600">Retention (days)</label>
318
+ <input type="number" v-model.number="config.db.ttlDays" class="mt-1 w-full border rounded px-3 py-2" />
319
+ </div>
320
+ <div class="col-span-2">
321
+ <label class="text-xs font-semibold text-gray-600">Sample rate (%)</label>
322
+ <input type="number" v-model.number="config.db.sampleRatePercent" class="mt-1 w-full border rounded px-3 py-2" />
323
+ </div>
324
+ </div>
325
+ </div>
326
+ </div>
327
+
328
+ <div class="col-span-6 space-y-4">
329
+ <div class="bg-white border border-gray-200 rounded p-4">
330
+ <div class="text-sm font-semibold text-gray-800 mb-3">Global Settings</div>
331
+ <div class="space-y-3">
332
+ <label class="flex items-center gap-2 text-sm">
333
+ <input type="checkbox" v-model="globalSettings.consoleManagerEnabled" @change="updateGlobalSetting" />
334
+ Enable Console Manager (requires restart)
335
+ </label>
336
+ <div class="text-xs text-gray-500">
337
+ Controls console manager initialization. When disabled, console methods are not overridden and no entries are tracked.
338
+ This setting uses the Global Settings system and requires a server restart to take effect.
339
+ </div>
340
+ <div v-if="globalSettingsStatus" class="text-xs p-2 rounded" :class="globalSettingsStatus.includes('Error') ? 'bg-red-100 text-red-700' : 'bg-blue-100 text-blue-700'">
341
+ {{ globalSettingsStatus }}
342
+ </div>
343
+ </div>
344
+ </div>
345
+
346
+ <div class="bg-white border border-gray-200 rounded p-4">
347
+ <div class="text-sm font-semibold text-gray-800 mb-3">Cache</div>
348
+ <div class="grid grid-cols-2 gap-4">
349
+ <label class="flex items-center gap-2 text-sm">
350
+ <input type="checkbox" v-model="config.cache.enabled" />
351
+ Enable cache counters
352
+ </label>
353
+ <div>
354
+ <label class="text-xs font-semibold text-gray-600">TTL seconds</label>
355
+ <input type="number" v-model.number="config.cache.ttlSeconds" class="mt-1 w-full border rounded px-3 py-2" />
356
+ </div>
357
+ <div class="col-span-2">
358
+ <label class="text-xs font-semibold text-gray-600">Namespace</label>
359
+ <input v-model="config.cache.namespace" class="mt-1 w-full border rounded px-3 py-2" />
360
+ </div>
361
+ </div>
362
+ </div>
363
+
364
+ <div class="bg-white border border-gray-200 rounded p-4">
365
+ <div class="text-sm font-semibold text-gray-800 mb-3">Performance</div>
366
+ <div class="grid grid-cols-2 gap-4">
367
+ <div>
368
+ <label class="text-xs font-semibold text-gray-600">Max arg chars</label>
369
+ <input type="number" v-model.number="config.performance.maxArgChars" class="mt-1 w-full border rounded px-3 py-2" />
370
+ </div>
371
+ <div>
372
+ <label class="text-xs font-semibold text-gray-600">Max args serialized</label>
373
+ <input type="number" v-model.number="config.performance.maxArgsSerialized" class="mt-1 w-full border rounded px-3 py-2" />
374
+ </div>
375
+ </div>
376
+ <div class="text-xs text-gray-500 mt-2">Config is stored using the JSON Configs system under the hood.</div>
377
+ </div>
378
+ </div>
379
+ </div>
380
+ </div>
381
+
382
+ </div>
383
+ </div>
384
+
385
+ <style>
386
+ [v-cloak] { display: none; }
387
+ </style>
388
+
389
+ <script>
390
+ window.BASE_URL = '<%= baseUrl %>';
391
+
392
+ const { createApp } = Vue;
393
+
394
+ createApp({
395
+ data() {
396
+ return {
397
+ activeTab: 'entries',
398
+ status: '',
399
+
400
+ config: {
401
+ defaultEntryEnabled: true,
402
+ defaults: { persist: { cache: false, db: false, warnErrorToCacheDb: false } },
403
+ db: { enabled: false, ttlDays: 7, sampleRatePercent: 100 },
404
+ cache: { enabled: false, ttlSeconds: 3600, namespace: 'console-manager' },
405
+ performance: { maxArgChars: 2000, maxArgsSerialized: 5 },
406
+ },
407
+
408
+ tags: [],
409
+ selectedTags: [],
410
+
411
+ entries: [],
412
+ entriesPagination: { page: 1, pageSize: 50, total: 0, totalPages: 1 },
413
+ entriesQuery: { q: '', method: '', enabled: '', page: 1, pageSize: 50 },
414
+
415
+ logs: [],
416
+ logsPagination: { page: 1, pageSize: 50, total: 0, totalPages: 1 },
417
+ logsQuery: { q: '', method: '', entryHash: '', page: 1, pageSize: 50 },
418
+
419
+ selectedHashes: [],
420
+ bulkTag: '',
421
+ showDeleteModal: false,
422
+ deleteLogsAlso: false,
423
+ isDeleting: false,
424
+
425
+ // Global settings
426
+ globalSettings: {
427
+ consoleManagerEnabled: true,
428
+ },
429
+ globalSettingsStatus: '',
430
+ };
431
+ },
432
+ computed: {
433
+ allSelected() {
434
+ return this.entries.length > 0 && this.selectedHashes.length === this.entries.length;
435
+ }
436
+ },
437
+ methods: {
438
+ tabClass(tab) {
439
+ return this.activeTab === tab
440
+ ? 'bg-gray-900 text-white'
441
+ : 'bg-gray-100 text-gray-700 hover:bg-gray-200';
442
+ },
443
+ async api(path, opts) {
444
+ const base = window.BASE_URL || '';
445
+ const res = await fetch(base + path, {
446
+ credentials: 'same-origin',
447
+ headers: { 'Content-Type': 'application/json' },
448
+ ...opts,
449
+ });
450
+ const json = await res.json().catch(() => ({}));
451
+ if (!res.ok) throw new Error(json.error || 'Request failed');
452
+ return json;
453
+ },
454
+ formatDate(d) {
455
+ if (!d) return '';
456
+ const dt = new Date(d);
457
+ if (Number.isNaN(dt.getTime())) return '';
458
+ return dt.toISOString().replace('T', ' ').slice(0, 19);
459
+ },
460
+ async loadConfig() {
461
+ const data = await this.api('/api/admin/console-manager/config');
462
+ if (data && data.config) {
463
+ this.config = data.config;
464
+ }
465
+ },
466
+ async saveConfig() {
467
+ this.status = 'Saving...';
468
+ try {
469
+ await this.api('/api/admin/console-manager/config', {
470
+ method: 'PUT',
471
+ body: JSON.stringify(this.config),
472
+ });
473
+ this.status = 'Saved config (retroactive defaults applied).';
474
+ } catch (e) {
475
+ this.status = e.message;
476
+ }
477
+ },
478
+ async loadTags() {
479
+ const data = await this.api('/api/admin/console-manager/tags');
480
+ this.tags = Array.isArray(data.items) ? data.items : [];
481
+ },
482
+ toggleTag(tag) {
483
+ const idx = this.selectedTags.indexOf(tag);
484
+ if (idx >= 0) this.selectedTags.splice(idx, 1);
485
+ else this.selectedTags.push(tag);
486
+ this.entriesQuery.page = 1;
487
+ this.loadEntries();
488
+ },
489
+ buildTagsQuery() {
490
+ return this.selectedTags.join(',');
491
+ },
492
+ async loadEntries() {
493
+ this.status = 'Loading entries...';
494
+ try {
495
+ const params = new URLSearchParams();
496
+ if (this.entriesQuery.q) params.set('q', this.entriesQuery.q);
497
+ if (this.entriesQuery.method) params.set('method', this.entriesQuery.method);
498
+ if (this.entriesQuery.enabled) params.set('enabled', this.entriesQuery.enabled);
499
+ params.set('page', String(this.entriesQuery.page || 1));
500
+ params.set('pageSize', String(this.entriesQuery.pageSize || 50));
501
+ const tagQ = this.buildTagsQuery();
502
+ if (tagQ) params.set('tags', tagQ);
503
+
504
+ const data = await this.api('/api/admin/console-manager/entries?' + params.toString());
505
+ this.entries = Array.isArray(data.items) ? data.items : [];
506
+ this.entriesPagination = data.pagination || this.entriesPagination;
507
+ this.selectedHashes = [];
508
+ this.status = '';
509
+ } catch (e) {
510
+ this.status = e.message;
511
+ }
512
+ },
513
+ prevEntries() {
514
+ if (this.entriesPagination.page <= 1) return;
515
+ this.entriesQuery.page = this.entriesPagination.page - 1;
516
+ this.loadEntries();
517
+ },
518
+ nextEntries() {
519
+ if (this.entriesPagination.page >= this.entriesPagination.totalPages) return;
520
+ this.entriesQuery.page = this.entriesPagination.page + 1;
521
+ this.loadEntries();
522
+ },
523
+ toggleSelectAll(e) {
524
+ const checked = Boolean(e?.target?.checked);
525
+ if (!checked) {
526
+ this.selectedHashes = [];
527
+ } else {
528
+ this.selectedHashes = this.entries.map((it) => it.hash);
529
+ }
530
+ },
531
+ async bulkEnable(enabled) {
532
+ if (!this.selectedHashes.length) return;
533
+ this.status = enabled ? 'Enabling...' : 'Disabling...';
534
+ try {
535
+ await this.api('/api/admin/console-manager/entries/bulk-enable', {
536
+ method: 'PUT',
537
+ body: JSON.stringify({ hashes: this.selectedHashes, enabled: Boolean(enabled) }),
538
+ });
539
+ await this.loadEntries();
540
+ this.status = '';
541
+ } catch (e) {
542
+ this.status = e.message;
543
+ }
544
+ },
545
+ async bulkAddTag() {
546
+ const tag = String(this.bulkTag || '').trim();
547
+ if (!tag || !this.selectedHashes.length) return;
548
+ this.status = 'Updating tags...';
549
+ try {
550
+ await this.api('/api/admin/console-manager/entries/bulk-tags', {
551
+ method: 'PUT',
552
+ body: JSON.stringify({ hashes: this.selectedHashes, add: [tag] }),
553
+ });
554
+ this.bulkTag = '';
555
+ await this.loadTags();
556
+ await this.loadEntries();
557
+ this.status = '';
558
+ } catch (e) {
559
+ this.status = e.message;
560
+ }
561
+ },
562
+ async bulkRemoveTag() {
563
+ const tag = String(this.bulkTag || '').trim();
564
+ if (!tag || !this.selectedHashes.length) return;
565
+ this.status = 'Updating tags...';
566
+ try {
567
+ await this.api('/api/admin/console-manager/entries/bulk-tags', {
568
+ method: 'PUT',
569
+ body: JSON.stringify({ hashes: this.selectedHashes, remove: [tag] }),
570
+ });
571
+ this.bulkTag = '';
572
+ await this.loadTags();
573
+ await this.loadEntries();
574
+ this.status = '';
575
+ } catch (e) {
576
+ this.status = e.message;
577
+ }
578
+ },
579
+ confirmBulkDelete() {
580
+ if (!this.selectedHashes.length) return;
581
+ this.showDeleteModal = true;
582
+ this.deleteLogsAlso = false;
583
+ },
584
+ async bulkDelete() {
585
+ if (!this.selectedHashes.length) return;
586
+ this.isDeleting = true;
587
+ this.status = 'Deleting...';
588
+ try {
589
+ const data = await this.api('/api/admin/console-manager/entries/bulk-delete', {
590
+ method: 'DELETE',
591
+ body: JSON.stringify({
592
+ hashes: this.selectedHashes,
593
+ deleteLogs: this.deleteLogsAlso
594
+ }),
595
+ });
596
+ this.showDeleteModal = false;
597
+ this.selectedHashes = [];
598
+ await this.loadTags();
599
+ await this.loadEntries();
600
+ this.status = `Deleted ${data.deletedEntries} entr${data.deletedEntries === 1 ? 'y' : 'ies'}${data.deletedLogs ? ` and ${data.deletedLogs} log${data.deletedLogs === 1 ? '' : 's'}` : ''}.`;
601
+ } catch (e) {
602
+ this.status = e.message;
603
+ } finally {
604
+ this.isDeleting = false;
605
+ }
606
+ },
607
+ async loadLogs() {
608
+ this.status = 'Loading logs...';
609
+ try {
610
+ const params = new URLSearchParams();
611
+ if (this.logsQuery.q) params.set('q', this.logsQuery.q);
612
+ if (this.logsQuery.method) params.set('method', this.logsQuery.method);
613
+ if (this.logsQuery.entryHash) params.set('entryHash', this.logsQuery.entryHash);
614
+ params.set('page', String(this.logsQuery.page || 1));
615
+ params.set('pageSize', String(this.logsQuery.pageSize || 50));
616
+
617
+ const data = await this.api('/api/admin/console-manager/logs?' + params.toString());
618
+ this.logs = Array.isArray(data.items) ? data.items : [];
619
+ this.logsPagination = data.pagination || this.logsPagination;
620
+ this.status = '';
621
+ } catch (e) {
622
+ this.status = e.message;
623
+ }
624
+ },
625
+ prevLogs() {
626
+ if (this.logsPagination.page <= 1) return;
627
+ this.logsQuery.page = this.logsPagination.page - 1;
628
+ this.loadLogs();
629
+ },
630
+ nextLogs() {
631
+ if (this.logsPagination.page >= this.logsPagination.totalPages) return;
632
+ this.logsQuery.page = this.logsPagination.page + 1;
633
+ this.loadLogs();
634
+ },
635
+ async refreshAll() {
636
+ this.status = 'Refreshing...';
637
+ try {
638
+ await Promise.all([
639
+ this.loadConfig(),
640
+ this.loadTags(),
641
+ this.loadEntries(),
642
+ ]);
643
+ this.status = '';
644
+ } catch (e) {
645
+ this.status = e.message;
646
+ }
647
+ },
648
+ async loadGlobalSettings() {
649
+ try {
650
+ const data = await this.api('/api/admin/console-manager/global-setting');
651
+ this.globalSettings.consoleManagerEnabled = data.enabled;
652
+ } catch (e) {
653
+ console.error('Failed to load global settings:', e);
654
+ }
655
+ },
656
+ async updateGlobalSetting() {
657
+ this.globalSettingsStatus = 'Updating...';
658
+ try {
659
+ const data = await this.api('/api/admin/console-manager/global-setting', {
660
+ method: 'PUT',
661
+ body: JSON.stringify({ enabled: this.globalSettings.consoleManagerEnabled }),
662
+ });
663
+ this.globalSettingsStatus = data.message || 'Updated. Restart required.';
664
+ setTimeout(() => { this.globalSettingsStatus = ''; }, 5000);
665
+ } catch (e) {
666
+ this.globalSettingsStatus = e.message;
667
+ setTimeout(() => { this.globalSettingsStatus = ''; }, 5000);
668
+ }
669
+ },
670
+ },
671
+ async mounted() {
672
+ await Promise.all([
673
+ this.refreshAll(),
674
+ this.loadGlobalSettings(),
675
+ ]);
676
+ }
677
+ }).mount('#app');
678
+ </script>
679
+ </body>
680
+ </html>