@intranefr/superbackend 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (188) hide show
  1. package/.commiat +4 -0
  2. package/.env.example +47 -0
  3. package/README.md +110 -0
  4. package/index.js +94 -0
  5. package/package.json +67 -0
  6. package/public/css/styles.css +139 -0
  7. package/public/js/animations.js +41 -0
  8. package/sdk/error-tracking/browser/package.json +16 -0
  9. package/sdk/error-tracking/browser/src/core.js +270 -0
  10. package/sdk/error-tracking/browser/src/embed.js +18 -0
  11. package/sdk/error-tracking/browser/src/index.js +1 -0
  12. package/server.js +5 -0
  13. package/src/admin/endpointRegistry.js +300 -0
  14. package/src/controllers/admin.controller.js +321 -0
  15. package/src/controllers/adminAssets.controller.js +530 -0
  16. package/src/controllers/adminAssetsStorage.controller.js +260 -0
  17. package/src/controllers/adminEjsVirtual.controller.js +354 -0
  18. package/src/controllers/adminFeatureFlags.controller.js +155 -0
  19. package/src/controllers/adminHeadless.controller.js +1071 -0
  20. package/src/controllers/adminI18n.controller.js +604 -0
  21. package/src/controllers/adminJsonConfigs.controller.js +97 -0
  22. package/src/controllers/adminLlm.controller.js +273 -0
  23. package/src/controllers/adminMigration.controller.js +257 -0
  24. package/src/controllers/adminSeoConfig.controller.js +515 -0
  25. package/src/controllers/adminStats.controller.js +121 -0
  26. package/src/controllers/adminUploadNamespaces.controller.js +208 -0
  27. package/src/controllers/assets.controller.js +248 -0
  28. package/src/controllers/auth.controller.js +93 -0
  29. package/src/controllers/billing.controller.js +223 -0
  30. package/src/controllers/featureFlags.controller.js +35 -0
  31. package/src/controllers/forms.controller.js +217 -0
  32. package/src/controllers/globalSettings.controller.js +252 -0
  33. package/src/controllers/headlessCrud.controller.js +126 -0
  34. package/src/controllers/i18n.controller.js +12 -0
  35. package/src/controllers/invite.controller.js +249 -0
  36. package/src/controllers/jsonConfigs.controller.js +19 -0
  37. package/src/controllers/metrics.controller.js +149 -0
  38. package/src/controllers/notificationAdmin.controller.js +264 -0
  39. package/src/controllers/notifications.controller.js +131 -0
  40. package/src/controllers/org.controller.js +357 -0
  41. package/src/controllers/orgAdmin.controller.js +491 -0
  42. package/src/controllers/stripeAdmin.controller.js +410 -0
  43. package/src/controllers/user.controller.js +361 -0
  44. package/src/controllers/userAdmin.controller.js +277 -0
  45. package/src/controllers/waitingList.controller.js +167 -0
  46. package/src/controllers/webhook.controller.js +200 -0
  47. package/src/middleware/auth.js +66 -0
  48. package/src/middleware/errorCapture.js +170 -0
  49. package/src/middleware/headlessApiTokenAuth.js +57 -0
  50. package/src/middleware/org.js +108 -0
  51. package/src/middleware.js +901 -0
  52. package/src/models/ActionEvent.js +31 -0
  53. package/src/models/ActivityLog.js +41 -0
  54. package/src/models/Asset.js +84 -0
  55. package/src/models/AuditEvent.js +93 -0
  56. package/src/models/EmailLog.js +28 -0
  57. package/src/models/ErrorAggregate.js +72 -0
  58. package/src/models/FormSubmission.js +41 -0
  59. package/src/models/GlobalSetting.js +38 -0
  60. package/src/models/HeadlessApiToken.js +24 -0
  61. package/src/models/HeadlessModelDefinition.js +41 -0
  62. package/src/models/I18nEntry.js +77 -0
  63. package/src/models/I18nLocale.js +33 -0
  64. package/src/models/Invite.js +70 -0
  65. package/src/models/JsonConfig.js +46 -0
  66. package/src/models/Notification.js +60 -0
  67. package/src/models/Organization.js +57 -0
  68. package/src/models/OrganizationMember.js +43 -0
  69. package/src/models/StripeCatalogItem.js +77 -0
  70. package/src/models/StripeWebhookEvent.js +57 -0
  71. package/src/models/User.js +89 -0
  72. package/src/models/VirtualEjsFile.js +60 -0
  73. package/src/models/VirtualEjsFileVersion.js +43 -0
  74. package/src/models/VirtualEjsGroupChange.js +32 -0
  75. package/src/models/WaitingList.js +41 -0
  76. package/src/models/Webhook.js +63 -0
  77. package/src/models/Workflow.js +29 -0
  78. package/src/models/WorkflowExecution.js +12 -0
  79. package/src/routes/admin.routes.js +26 -0
  80. package/src/routes/adminAssets.routes.js +28 -0
  81. package/src/routes/adminAssetsStorage.routes.js +13 -0
  82. package/src/routes/adminAudit.routes.js +196 -0
  83. package/src/routes/adminEjsVirtual.routes.js +17 -0
  84. package/src/routes/adminErrors.routes.js +164 -0
  85. package/src/routes/adminFeatureFlags.routes.js +12 -0
  86. package/src/routes/adminHeadless.routes.js +38 -0
  87. package/src/routes/adminI18n.routes.js +22 -0
  88. package/src/routes/adminJsonConfigs.routes.js +15 -0
  89. package/src/routes/adminLlm.routes.js +12 -0
  90. package/src/routes/adminMigration.routes.js +81 -0
  91. package/src/routes/adminSeoConfig.routes.js +20 -0
  92. package/src/routes/adminUploadNamespaces.routes.js +13 -0
  93. package/src/routes/assets.routes.js +21 -0
  94. package/src/routes/auth.routes.js +12 -0
  95. package/src/routes/billing.routes.js +11 -0
  96. package/src/routes/errorTracking.routes.js +31 -0
  97. package/src/routes/featureFlags.routes.js +9 -0
  98. package/src/routes/forms.routes.js +9 -0
  99. package/src/routes/formsAdmin.routes.js +13 -0
  100. package/src/routes/globalSettings.routes.js +18 -0
  101. package/src/routes/headless.routes.js +15 -0
  102. package/src/routes/i18n.routes.js +8 -0
  103. package/src/routes/invite.routes.js +9 -0
  104. package/src/routes/jsonConfigs.routes.js +8 -0
  105. package/src/routes/log.routes.js +111 -0
  106. package/src/routes/metrics.routes.js +9 -0
  107. package/src/routes/notificationAdmin.routes.js +15 -0
  108. package/src/routes/notifications.routes.js +12 -0
  109. package/src/routes/org.routes.js +31 -0
  110. package/src/routes/orgAdmin.routes.js +20 -0
  111. package/src/routes/publicAssets.routes.js +7 -0
  112. package/src/routes/stripeAdmin.routes.js +20 -0
  113. package/src/routes/user.routes.js +22 -0
  114. package/src/routes/userAdmin.routes.js +15 -0
  115. package/src/routes/waitingList.routes.js +13 -0
  116. package/src/routes/waitingListAdmin.routes.js +9 -0
  117. package/src/routes/webhook.routes.js +32 -0
  118. package/src/routes/workflowWebhook.routes.js +54 -0
  119. package/src/routes/workflows.routes.js +110 -0
  120. package/src/services/assets.service.js +110 -0
  121. package/src/services/audit.service.js +62 -0
  122. package/src/services/auditLogger.js +165 -0
  123. package/src/services/ejsVirtual.service.js +614 -0
  124. package/src/services/email.service.js +351 -0
  125. package/src/services/errorLogger.js +221 -0
  126. package/src/services/featureFlags.service.js +202 -0
  127. package/src/services/forms.service.js +214 -0
  128. package/src/services/globalSettings.service.js +49 -0
  129. package/src/services/headlessApiTokens.service.js +158 -0
  130. package/src/services/headlessCrypto.service.js +31 -0
  131. package/src/services/headlessModels.service.js +356 -0
  132. package/src/services/i18n.service.js +314 -0
  133. package/src/services/i18nInferredKeys.service.js +337 -0
  134. package/src/services/jsonConfigs.service.js +392 -0
  135. package/src/services/llm.service.js +749 -0
  136. package/src/services/migration.service.js +581 -0
  137. package/src/services/migrationAssets/fsLocal.js +58 -0
  138. package/src/services/migrationAssets/index.js +134 -0
  139. package/src/services/migrationAssets/s3.js +75 -0
  140. package/src/services/migrationAssets/sftp.js +92 -0
  141. package/src/services/notification.service.js +212 -0
  142. package/src/services/objectStorage.service.js +514 -0
  143. package/src/services/seoConfig.service.js +402 -0
  144. package/src/services/storage.js +150 -0
  145. package/src/services/stripe.service.js +185 -0
  146. package/src/services/stripeHelper.service.js +264 -0
  147. package/src/services/uploadNamespaces.service.js +326 -0
  148. package/src/services/webhook.service.js +157 -0
  149. package/src/services/workflow.service.js +271 -0
  150. package/src/utils/asyncHandler.js +5 -0
  151. package/src/utils/encryption.js +80 -0
  152. package/src/utils/jwt.js +40 -0
  153. package/src/utils/orgRoles.js +156 -0
  154. package/src/utils/validation.js +26 -0
  155. package/src/utils/webhookRetry.js +93 -0
  156. package/views/admin-assets.ejs +444 -0
  157. package/views/admin-audit.ejs +283 -0
  158. package/views/admin-coolify-deploy.ejs +207 -0
  159. package/views/admin-dashboard-home.ejs +291 -0
  160. package/views/admin-dashboard.ejs +397 -0
  161. package/views/admin-ejs-virtual.ejs +280 -0
  162. package/views/admin-errors.ejs +368 -0
  163. package/views/admin-feature-flags.ejs +390 -0
  164. package/views/admin-forms.ejs +526 -0
  165. package/views/admin-global-settings.ejs +436 -0
  166. package/views/admin-headless.ejs +2020 -0
  167. package/views/admin-i18n-locales.ejs +221 -0
  168. package/views/admin-i18n.ejs +728 -0
  169. package/views/admin-json-configs.ejs +410 -0
  170. package/views/admin-llm.ejs +884 -0
  171. package/views/admin-metrics.ejs +274 -0
  172. package/views/admin-migration.ejs +814 -0
  173. package/views/admin-notifications.ejs +430 -0
  174. package/views/admin-organizations.ejs +984 -0
  175. package/views/admin-seo-config.ejs +673 -0
  176. package/views/admin-stripe-pricing.ejs +558 -0
  177. package/views/admin-test.ejs +342 -0
  178. package/views/admin-users.ejs +452 -0
  179. package/views/admin-waiting-list.ejs +547 -0
  180. package/views/admin-webhooks.ejs +329 -0
  181. package/views/admin-workflows.ejs +310 -0
  182. package/views/partials/admin-assets-script.ejs +2022 -0
  183. package/views/partials/admin-test-sidebar.ejs +14 -0
  184. package/views/partials/dashboard/nav-items.ejs +66 -0
  185. package/views/partials/dashboard/palette.ejs +63 -0
  186. package/views/partials/dashboard/sidebar.ejs +21 -0
  187. package/views/partials/dashboard/tab-bar.ejs +26 -0
  188. package/views/partials/footer.ejs +3 -0
@@ -0,0 +1,291 @@
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>SaaSBackend Command Center</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
10
+ <style>
11
+ .stat-card { @apply bg-white p-5 rounded-xl border border-gray-200 shadow-sm transition-all hover:shadow-md flex flex-col justify-between; }
12
+ .category-title { @apply text-xs font-semibold text-gray-400 uppercase tracking-wider mb-4 flex items-center gap-2; }
13
+ .chart-container { position: relative; height: 250px; width: 100%; }
14
+ .status-badge { @apply px-2 py-0.5 rounded-full text-[10px] font-bold uppercase; }
15
+ </style>
16
+ </head>
17
+ <body class="bg-gray-50 min-h-screen p-6">
18
+ <div id="app" class="max-w-7xl mx-auto space-y-8">
19
+ <!-- Header -->
20
+ <div class="flex justify-between items-center">
21
+ <div>
22
+ <h1 class="text-2xl font-bold text-gray-900">Command Center</h1>
23
+ <p class="text-sm text-gray-500">System-wide health and growth metrics.</p>
24
+ </div>
25
+ <div class="flex items-center gap-3">
26
+ <div id="system-health-badge" class="flex items-center gap-2 text-xs font-semibold"></div>
27
+ <button onclick="fetchStats()" class="p-2 bg-white border border-gray-200 rounded-lg hover:bg-gray-50 transition-colors shadow-sm">
28
+ <i class="ti ti-refresh text-gray-600"></i>
29
+ </button>
30
+ </div>
31
+ </div>
32
+
33
+ <!-- Categorized Stats Grid -->
34
+ <div class="space-y-8">
35
+ <!-- Section: User Management -->
36
+ <div>
37
+ <h2 class="category-title"><i class="ti ti-users"></i> User Management</h2>
38
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
39
+ <div class="stat-card">
40
+ <span class="text-sm text-gray-500">Total Users</span>
41
+ <div class="flex items-end justify-between mt-2">
42
+ <h3 id="users-total" class="text-2xl font-bold">-</h3>
43
+ <span id="users-new" class="text-xs font-medium text-green-600 bg-green-50 px-2 py-1 rounded">+0 today</span>
44
+ </div>
45
+ </div>
46
+ <div class="stat-card">
47
+ <span class="text-sm text-gray-500">Organizations</span>
48
+ <h3 id="users-orgs" class="text-2xl font-bold mt-2">-</h3>
49
+ </div>
50
+ <div class="stat-card">
51
+ <span class="text-sm text-gray-500">Active Invites</span>
52
+ <h3 id="users-invites" class="text-2xl font-bold mt-2">-</h3>
53
+ </div>
54
+ <div class="stat-card">
55
+ <span class="text-sm text-gray-500">Conversion (est)</span>
56
+ <h3 class="text-2xl font-bold mt-2 text-blue-600">84.2%</h3>
57
+ </div>
58
+ </div>
59
+ </div>
60
+
61
+ <!-- Section: Monitoring & Health -->
62
+ <div>
63
+ <h2 class="category-title"><i class="ti ti-activity"></i> System Monitoring</h2>
64
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
65
+ <div class="stat-card border-red-100 bg-red-50/10">
66
+ <span class="text-sm text-gray-500">Unresolved Errors</span>
67
+ <h3 id="mon-errors" class="text-2xl font-bold text-red-600 mt-2">-</h3>
68
+ </div>
69
+ <div class="stat-card">
70
+ <span class="text-sm text-gray-500">Audit Events (24h)</span>
71
+ <h3 id="mon-audit" class="text-2xl font-bold mt-2">-</h3>
72
+ </div>
73
+ <div class="stat-card">
74
+ <span class="text-sm text-gray-500">Emails Sent</span>
75
+ <h3 id="mon-emails-sent" class="text-2xl font-bold text-green-600 mt-2">-</h3>
76
+ </div>
77
+ <div class="stat-card">
78
+ <span class="text-sm text-gray-500">Emails Failed</span>
79
+ <h3 id="mon-emails-failed" class="text-2xl font-bold text-orange-600 mt-2">-</h3>
80
+ </div>
81
+ </div>
82
+ </div>
83
+
84
+ <!-- Section: Content & Configuration -->
85
+ <div>
86
+ <h2 class="category-title"><i class="ti ti-folder"></i> Content & Configuration</h2>
87
+ <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
88
+ <div class="stat-card">
89
+ <span class="text-sm text-gray-500">Total Assets</span>
90
+ <h3 id="cont-assets" class="text-2xl font-bold mt-2">-</h3>
91
+ </div>
92
+ <div class="stat-card">
93
+ <span class="text-sm text-gray-500">Virtual EJS Files</span>
94
+ <h3 id="cont-ejs" class="text-2xl font-bold mt-2">-</h3>
95
+ </div>
96
+ <div class="stat-card">
97
+ <span class="text-sm text-gray-500">JSON Configs</span>
98
+ <h3 id="cont-json" class="text-2xl font-bold mt-2">-</h3>
99
+ </div>
100
+ <div class="stat-card">
101
+ <span class="text-sm text-gray-500">Active Plans</span>
102
+ <h3 id="saas-plans" class="text-2xl font-bold mt-2">-</h3>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ </div>
107
+
108
+ <!-- Charts Row -->
109
+ <div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
110
+ <div class="lg:col-span-2 stat-card">
111
+ <h3 class="text-sm font-bold text-gray-900 mb-6">User Growth & Activity (7d)</h3>
112
+ <div class="chart-container">
113
+ <canvas id="growthChart"></canvas>
114
+ </div>
115
+ </div>
116
+ <div class="stat-card">
117
+ <h3 class="text-sm font-bold text-gray-900 mb-6">Email Delivery Trend</h3>
118
+ <div class="chart-container">
119
+ <canvas id="emailChart"></canvas>
120
+ </div>
121
+ </div>
122
+ </div>
123
+
124
+ <!-- Recent Activity Table -->
125
+ <div class="stat-card overflow-hidden">
126
+ <div class="flex items-center justify-between mb-4">
127
+ <h3 class="text-sm font-bold text-gray-900">Latest Audit Logs</h3>
128
+ <a href="#" onclick="parent.window.app.openTab({id:'audit', label:'Audit Logs', path:'/admin/audit', icon:'ti-history'})" class="text-xs text-blue-600 hover:underline">View All</a>
129
+ </div>
130
+ <div class="overflow-x-auto">
131
+ <table class="w-full text-left">
132
+ <thead class="bg-gray-50 text-[10px] font-bold text-gray-400 uppercase">
133
+ <tr>
134
+ <th class="px-4 py-2">Action</th>
135
+ <th class="px-4 py-2">Actor</th>
136
+ <th class="px-4 py-2">Details</th>
137
+ <th class="px-4 py-2">Time</th>
138
+ </tr>
139
+ </thead>
140
+ <tbody id="activity-list" class="divide-y divide-gray-100">
141
+ <!-- Dynamically populated -->
142
+ </tbody>
143
+ </table>
144
+ </div>
145
+ </div>
146
+
147
+ <%- include('partials/footer') %>
148
+ </div>
149
+
150
+ <script>
151
+ let growthChart, emailChart;
152
+
153
+ async function fetchStats() {
154
+ try {
155
+ const res = await fetch('<%= baseUrl %>/api/admin/stats/overview');
156
+ const data = await res.json();
157
+ updateUI(data);
158
+ } catch (err) {
159
+ console.error('Failed to load stats:', err);
160
+ }
161
+ }
162
+
163
+ function updateUI(data) {
164
+ const { categories, recentActivity, timeSeries } = data;
165
+
166
+ const setVal = (id, val) => {
167
+ const el = document.getElementById(id);
168
+ if (el) el.innerText = val;
169
+ };
170
+
171
+ // Update Health Badge
172
+ const healthBadge = document.getElementById('system-health-badge');
173
+ if (healthBadge) {
174
+ const health = categories.monitoring.health;
175
+ const healthMeta = {
176
+ healthy: { cls: 'bg-green-100 text-green-700', icon: 'ti ti-heartbeat', label: 'Healthy' },
177
+ warning: { cls: 'bg-yellow-100 text-yellow-700', icon: 'ti ti-alert-triangle', label: 'Warnings present' },
178
+ critical: { cls: 'bg-red-100 text-red-700', icon: 'ti ti-flame', label: 'Critical issues' }
179
+ };
180
+ const meta = healthMeta[health] || healthMeta.healthy;
181
+ healthBadge.innerHTML = `<span class="status-badge ${meta.cls}" title="${meta.label}"><i class="${meta.icon}"></i></span>`;
182
+ }
183
+
184
+ // Update Users
185
+ setVal('users-total', categories.users.total);
186
+ setVal('users-new', `+${categories.users.newToday} today`);
187
+ setVal('users-orgs', categories.users.orgs);
188
+ setVal('users-invites', categories.users.invites);
189
+
190
+ // Update Monitoring
191
+ setVal('mon-errors', categories.monitoring.errors);
192
+ setVal('mon-audit', categories.monitoring.audit24h);
193
+ setVal('mon-emails-sent', categories.monitoring.emailsSent);
194
+ setVal('mon-emails-failed', categories.monitoring.emailsFailed);
195
+
196
+ // Update Content
197
+ setVal('cont-assets', categories.content.assets);
198
+ setVal('cont-ejs', categories.content.virtualEjs);
199
+ setVal('saas-workflows', categories.content.workflows || 0);
200
+
201
+ const activityBody = document.getElementById('activity-list');
202
+ if (activityBody) {
203
+ activityBody.innerHTML = recentActivity.map(event => `
204
+ <tr class="text-xs text-gray-700">
205
+ <td class="px-4 py-3"><span class="font-medium text-gray-900">${event.action}</span></td>
206
+ <td class="px-4 py-3">${event.actorEmail || 'System'}</td>
207
+ <td class="px-4 py-3 truncate max-w-[200px] text-gray-400">${event.resourceId || '-'}</td>
208
+ <td class="px-4 py-3 text-gray-400">${new Date(event.createdAt).toLocaleTimeString()}</td>
209
+ </tr>
210
+ `).join('');
211
+ }
212
+
213
+ // Update Charts
214
+ updateCharts(timeSeries);
215
+ }
216
+
217
+ function updateCharts(timeSeries) {
218
+ const labels = timeSeries.map(d => d.date);
219
+
220
+ // Growth & Activity Chart
221
+ if (growthChart) growthChart.destroy();
222
+ growthChart = new Chart(document.getElementById('growthChart'), {
223
+ type: 'line',
224
+ data: {
225
+ labels,
226
+ datasets: [
227
+ {
228
+ label: 'Signups',
229
+ data: timeSeries.map(d => d.users),
230
+ borderColor: 'rgb(59, 130, 246)',
231
+ backgroundColor: 'rgba(59, 130, 246, 0.1)',
232
+ fill: true,
233
+ tension: 0.4
234
+ },
235
+ {
236
+ label: 'Activity',
237
+ data: timeSeries.map(d => d.activity),
238
+ borderColor: 'rgb(139, 92, 246)',
239
+ backgroundColor: 'transparent',
240
+ tension: 0.4
241
+ }
242
+ ]
243
+ },
244
+ options: {
245
+ responsive: true,
246
+ maintainAspectRatio: false,
247
+ plugins: { legend: { position: 'bottom', labels: { boxWidth: 12, font: { size: 10 } } } },
248
+ scales: {
249
+ y: { grid: { display: false } },
250
+ x: { grid: { display: false } }
251
+ }
252
+ }
253
+ });
254
+
255
+ // Email Delivery Chart
256
+ if (emailChart) emailChart.destroy();
257
+ emailChart = new Chart(document.getElementById('emailChart'), {
258
+ type: 'bar',
259
+ data: {
260
+ labels,
261
+ datasets: [{
262
+ label: 'Sent Emails',
263
+ data: timeSeries.map(d => d.emails),
264
+ backgroundColor: 'rgba(34, 197, 94, 0.8)',
265
+ borderRadius: 4
266
+ }]
267
+ },
268
+ options: {
269
+ responsive: true,
270
+ maintainAspectRatio: false,
271
+ plugins: { legend: { display: false } },
272
+ scales: {
273
+ y: { grid: { display: false }, ticks: { stepSize: 1 } },
274
+ x: { grid: { display: false } }
275
+ }
276
+ }
277
+ });
278
+ }
279
+
280
+ window.onload = fetchStats;
281
+ </script>
282
+ <script>
283
+ window.addEventListener("keydown", (e) => {
284
+ if ((e.ctrlKey || e.metaKey) && e.key === "k") {
285
+ e.preventDefault();
286
+ window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
287
+ }
288
+ });
289
+ </script>
290
+ </body>
291
+ </html>
@@ -0,0 +1,397 @@
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>Superbackend Admin Dashboard</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
10
+ <style>
11
+ [v-cloak] { display: none; }
12
+ .sidebar-link.active { @apply bg-blue-50 text-blue-700 border-r-4 border-blue-700; }
13
+ </style>
14
+ </head>
15
+ <body class="bg-gray-50 overflow-hidden">
16
+ <div id="app" class="h-screen flex flex-col" v-cloak>
17
+ <!-- Top Header -->
18
+ <header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shrink-0">
19
+ <div class="flex items-center gap-4">
20
+ <i class="ti ti-layout-dashboard text-2xl text-blue-600"></i>
21
+ <h1 class="text-xl font-bold text-gray-800">
22
+ Superbackend <span class="text-xs font-normal text-gray-500 ml-2 align-middle">(saasbackend)</span>
23
+ </h1>
24
+ </div>
25
+ <div class="flex items-center gap-6">
26
+ <div class="hidden md:flex items-center gap-2 text-xs text-gray-400 bg-gray-50 px-3 py-1.5 rounded-lg border border-gray-200">
27
+ <kbd class="px-1.5 py-0.5 bg-white border border-gray-300 rounded shadow-sm font-sans">Ctrl</kbd>
28
+ <span>+</span>
29
+ <kbd class="px-1.5 py-0.5 bg-white border border-gray-300 rounded shadow-sm font-sans">K</kbd>
30
+ <span class="ml-1">to search</span>
31
+ </div>
32
+ <div class="flex items-center gap-4">
33
+ <span class="text-sm text-gray-500">v1.0.0</span>
34
+ <a :href="baseUrl + adminBase + '/api/test'" target="_blank" class="text-sm text-blue-600 hover:underline">API Test</a>
35
+ </div>
36
+ </div>
37
+ </header>
38
+
39
+ <div class="flex-1 flex overflow-hidden">
40
+ <%- include('partials/dashboard/sidebar') %>
41
+
42
+ <!-- Main Content Area -->
43
+ <main class="flex-1 flex flex-col overflow-hidden bg-gray-50">
44
+ <%- include('partials/dashboard/tab-bar') %>
45
+
46
+ <div class="flex-1 relative">
47
+ <!-- Iframes -->
48
+ <iframe
49
+ v-for="tab in tabs"
50
+ v-show="activeTabId === tab.id"
51
+ :key="tab.id"
52
+ :src="baseUrl + tab.path"
53
+ class="absolute inset-0 w-full h-full border-none"
54
+ :id="'frame-' + tab.id"
55
+ ></iframe>
56
+
57
+ <!-- Empty State -->
58
+ <div v-if="tabs.length === 0" class="flex items-center justify-center h-full text-gray-400">
59
+ <div class="text-center">
60
+ <i class="ti ti-layout-dashboard text-6xl mb-4 opacity-20"></i>
61
+ <p class="text-lg font-medium">No open modules</p>
62
+ <p class="text-sm opacity-60">Select a module from the sidebar or press Ctrl+K</p>
63
+ </div>
64
+ </div>
65
+ </div>
66
+ </main>
67
+ </div>
68
+
69
+ <%- include('partials/dashboard/palette') %>
70
+ </div>
71
+
72
+ <script>
73
+ // Initialize globals from EJS locals first (must run before nav-items)
74
+ window.BASE_URL = '<%= baseUrl %>';
75
+ window.ADMIN_PATH = '<%= adminPath %>';
76
+ </script>
77
+
78
+ <%- include('partials/dashboard/nav-items') %>
79
+
80
+ <script>
81
+ const { createApp, ref, computed, nextTick, onMounted, onUnmounted, watch } = Vue;
82
+
83
+ createApp({
84
+ setup() {
85
+ const baseUrl = window.BASE_URL;
86
+ const adminBase = window.ADMIN_PATH || '/admin';
87
+ const navSections = window.NAV_SECTIONS || [];
88
+
89
+ // Tabs state
90
+ const tabs = ref([]);
91
+ const activeTabId = ref(null);
92
+
93
+ // localStorage utilities
94
+ const STORAGE_KEY = 'adminDashboardTabs';
95
+
96
+ const saveTabsToStorage = () => {
97
+ try {
98
+ const data = {
99
+ tabs: tabs.value,
100
+ activeTabId: activeTabId.value
101
+ };
102
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
103
+ } catch (error) {
104
+ console.warn('Failed to save tabs to localStorage:', error);
105
+ }
106
+ // Always save to URL as fallback
107
+ saveTabsToURL();
108
+ };
109
+
110
+ const loadTabsFromStorage = () => {
111
+ // Try localStorage first
112
+ try {
113
+ const stored = localStorage.getItem(STORAGE_KEY);
114
+ if (stored) {
115
+ const data = JSON.parse(stored);
116
+ if (data && Array.isArray(data.tabs)) {
117
+ // Validate tabs have required fields
118
+ const validTabs = data.tabs.filter(tab =>
119
+ tab &&
120
+ typeof tab.id === 'string' &&
121
+ typeof tab.label === 'string' &&
122
+ typeof tab.path === 'string'
123
+ );
124
+
125
+ // Ensure tabs still exist in available modules
126
+ const availableModuleIds = allModules.value.map(m => m.id);
127
+ const existingTabs = validTabs.filter(tab =>
128
+ availableModuleIds.includes(tab.id)
129
+ );
130
+
131
+ if (existingTabs.length > 0) {
132
+ return {
133
+ tabs: existingTabs,
134
+ activeTabId: existingTabs.some(t => t.id === data.activeTabId)
135
+ ? data.activeTabId
136
+ : (existingTabs.length > 0 ? existingTabs[0].id : null)
137
+ };
138
+ }
139
+ }
140
+ }
141
+ } catch (error) {
142
+ console.warn('Failed to load tabs from localStorage:', error);
143
+ }
144
+
145
+ // Fallback to URL parameters
146
+ return loadTabsFromURL();
147
+ };
148
+
149
+ // URL utilities for fallback persistence
150
+ let urlUpdateTimeout = null;
151
+
152
+ const saveTabsToURL = () => {
153
+ clearTimeout(urlUpdateTimeout);
154
+ urlUpdateTimeout = setTimeout(() => {
155
+ try {
156
+ const url = new URL(window.location);
157
+
158
+ if (tabs.value.length > 0) {
159
+ const tabIds = tabs.value.map(t => t.id).join(',');
160
+ url.searchParams.set('openTabs', tabIds);
161
+ if (activeTabId.value) {
162
+ url.searchParams.set('activeTab', activeTabId.value);
163
+ }
164
+ } else {
165
+ url.searchParams.delete('openTabs');
166
+ url.searchParams.delete('activeTab');
167
+ }
168
+
169
+ // Update URL without page reload
170
+ window.history.replaceState({}, '', url);
171
+ } catch (error) {
172
+ console.warn('Failed to save tabs to URL:', error);
173
+ }
174
+ }, 300); // Debounce URL updates
175
+ };
176
+
177
+ const loadTabsFromURL = () => {
178
+ try {
179
+ const url = new URL(window.location);
180
+ const openTabsParam = url.searchParams.get('openTabs');
181
+ const activeTabParam = url.searchParams.get('activeTab');
182
+
183
+ if (!openTabsParam) return null;
184
+
185
+ // Parse comma-separated tab IDs
186
+ const tabIds = openTabsParam.split(',').filter(id => id.trim());
187
+ if (tabIds.length === 0) return null;
188
+
189
+ // Get available modules
190
+ const availableModules = allModules.value;
191
+
192
+ // Reconstruct full tab objects from IDs
193
+ const tabs = tabIds.map(id => {
194
+ const module = availableModules.find(m => m.id === id);
195
+ if (module) {
196
+ return {
197
+ id: module.id,
198
+ label: module.label,
199
+ icon: module.icon,
200
+ path: module.path
201
+ };
202
+ }
203
+ return null;
204
+ }).filter(tab => tab !== null);
205
+
206
+ if (tabs.length === 0) return null;
207
+
208
+ return {
209
+ tabs,
210
+ activeTabId: tabs.some(t => t.id === activeTabParam)
211
+ ? activeTabParam
212
+ : tabs[0].id
213
+ };
214
+ } catch (error) {
215
+ console.warn('Failed to load tabs from URL:', error);
216
+ return null;
217
+ }
218
+ };
219
+
220
+ // Palette state
221
+ const showPalette = ref(false);
222
+ const paletteQuery = ref('');
223
+ const paletteCursor = ref(0);
224
+ const paletteInput = ref(null);
225
+
226
+ // Flattened modules for search
227
+ const allModules = computed(() => {
228
+ const modules = [];
229
+ if (Array.isArray(navSections)) {
230
+ navSections.forEach(section => {
231
+ if (section && Array.isArray(section.items)) {
232
+ section.items.forEach(item => {
233
+ modules.push({
234
+ ...item,
235
+ sectionTitle: section.title || 'Other'
236
+ });
237
+ });
238
+ }
239
+ });
240
+ }
241
+ return modules;
242
+ });
243
+
244
+ const filteredModules = computed(() => {
245
+ const mods = allModules.value;
246
+ if (!paletteQuery.value) return mods;
247
+ const query = paletteQuery.value.toLowerCase();
248
+ return mods.filter(m =>
249
+ (m.label && m.label.toLowerCase().includes(query)) ||
250
+ (m.sectionTitle && m.sectionTitle.toLowerCase().includes(query))
251
+ );
252
+ });
253
+
254
+ // Tab methods
255
+ const openTab = (item) => {
256
+ if (!item || !item.id) return;
257
+ const existing = tabs.value.find(t => t.id === item.id);
258
+ if (!existing) {
259
+ tabs.value.push({
260
+ id: item.id,
261
+ label: item.label,
262
+ icon: item.icon,
263
+ path: item.path
264
+ });
265
+ }
266
+ activeTabId.value = item.id;
267
+ saveTabsToStorage();
268
+ closePalette();
269
+ };
270
+
271
+ const closeTab = (id) => {
272
+ const index = tabs.value.findIndex(t => t.id === id);
273
+ if (index === -1) return;
274
+
275
+ const wasActive = activeTabId.value === id;
276
+ tabs.value.splice(index, 1);
277
+
278
+ if (wasActive && tabs.value.length > 0) {
279
+ const nextTab = tabs.value[Math.min(index, tabs.value.length - 1)];
280
+ activeTabId.value = nextTab.id;
281
+ } else if (tabs.value.length === 0) {
282
+ activeTabId.value = null;
283
+ }
284
+ saveTabsToStorage();
285
+ };
286
+
287
+ // Palette methods
288
+ const togglePalette = () => {
289
+ showPalette.value = !showPalette.value;
290
+ if (showPalette.value) {
291
+ paletteQuery.value = '';
292
+ paletteCursor.value = 0;
293
+ nextTick(() => {
294
+ if (paletteInput.value) paletteInput.value.focus();
295
+ });
296
+ }
297
+ };
298
+
299
+ const closePalette = () => {
300
+ showPalette.value = false;
301
+ };
302
+
303
+ const navigatePalette = (dir) => {
304
+ const len = filteredModules.value.length;
305
+ if (len === 0) return;
306
+ paletteCursor.value = (paletteCursor.value + dir + len) % len;
307
+
308
+ nextTick(() => {
309
+ const items = document.querySelectorAll('.palette-item');
310
+ const el = items[paletteCursor.value];
311
+ if (el) el.scrollIntoView({ block: 'nearest' });
312
+ });
313
+ };
314
+
315
+ const selectPaletteItem = () => {
316
+ const item = filteredModules.value[paletteCursor.value];
317
+ if (item) openTab(item);
318
+ };
319
+
320
+ const selectModule = (item) => {
321
+ openTab(item);
322
+ };
323
+
324
+ // Keyboard events
325
+ const handleKeydown = (e) => {
326
+ if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
327
+ e.preventDefault();
328
+ togglePalette();
329
+ }
330
+ };
331
+
332
+ const handleMessage = (e) => {
333
+ if (e.data && e.data.type === 'keydown' && e.data.ctrlK) {
334
+ togglePalette();
335
+ }
336
+ };
337
+
338
+ onMounted(() => {
339
+ window.addEventListener('keydown', handleKeydown);
340
+ window.addEventListener('message', handleMessage);
341
+
342
+ // Load saved tabs from localStorage
343
+ const savedState = loadTabsFromStorage();
344
+ if (savedState && savedState.tabs.length > 0) {
345
+ tabs.value = savedState.tabs;
346
+ activeTabId.value = savedState.activeTabId;
347
+ } else {
348
+ // Open default tab if no saved state
349
+ if (allModules.value.length > 0) {
350
+ const defaultModule = allModules.value.find(m => m.id === 'overview') || allModules.value[0];
351
+ if (defaultModule) openTab(defaultModule);
352
+ }
353
+ }
354
+ });
355
+
356
+ // Watch for activeTabId changes (when user clicks on tabs)
357
+ watch(activeTabId, () => {
358
+ saveTabsToStorage();
359
+ });
360
+
361
+ onUnmounted(() => {
362
+ window.removeEventListener('keydown', handleKeydown);
363
+ window.removeEventListener('message', handleMessage);
364
+ });
365
+
366
+ return {
367
+ baseUrl,
368
+ adminBase,
369
+ navSections,
370
+ tabs,
371
+ activeTabId,
372
+ openTab,
373
+ closeTab,
374
+ showPalette,
375
+ paletteQuery,
376
+ paletteCursor,
377
+ paletteInput,
378
+ filteredModules,
379
+ togglePalette,
380
+ closePalette,
381
+ navigatePalette,
382
+ selectPaletteItem,
383
+ selectModule
384
+ };
385
+ }
386
+ }).mount('#app');
387
+ </script>
388
+ <script>
389
+ window.addEventListener("keydown", (e) => {
390
+ if ((e.ctrlKey || e.metaKey) && e.key === "k") {
391
+ e.preventDefault();
392
+ window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
393
+ }
394
+ });
395
+ </script>
396
+ </body>
397
+ </html>