webspresso 0.0.97 → 0.0.99

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 (136) hide show
  1. package/.agents/ADMIN.md +163 -5
  2. package/.agents/AGENTS.md +1 -1
  3. package/.agents/ARCHITECTURE.md +10 -0
  4. package/.agents/AUTH.md +169 -0
  5. package/.agents/CLI.md +195 -1
  6. package/.agents/ORM.md +88 -4
  7. package/.agents/PLUGINS.md +221 -9
  8. package/.agents/QUEUE.md +127 -0
  9. package/.agents/ROUTING.md +127 -7
  10. package/AGENTS.md +1 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/analytics-prune.js +57 -0
  14. package/bin/commands/api.js +207 -15
  15. package/bin/commands/build.js +99 -0
  16. package/bin/commands/dev.js +64 -35
  17. package/bin/commands/doctor.js +26 -0
  18. package/bin/commands/email-prune.js +47 -0
  19. package/bin/commands/make-job.js +86 -0
  20. package/bin/commands/make-model.js +250 -0
  21. package/bin/commands/new.js +45 -28
  22. package/bin/commands/openapi.js +102 -0
  23. package/bin/commands/page.js +6 -3
  24. package/bin/commands/queue-prune.js +59 -0
  25. package/bin/commands/routes.js +52 -0
  26. package/bin/commands/service.js +6 -3
  27. package/bin/commands/typegen.js +106 -0
  28. package/bin/commands/upload-prune.js +79 -0
  29. package/bin/webspresso.js +22 -0
  30. package/core/applySchema.js +13 -6
  31. package/core/auth/hash.js +21 -12
  32. package/core/auth/index.js +60 -0
  33. package/core/auth/manager.js +134 -0
  34. package/core/auth/middleware.js +88 -0
  35. package/core/auth/oauth.js +517 -0
  36. package/core/auth/qrcode.js +499 -0
  37. package/core/auth/totp.js +504 -0
  38. package/core/compression/middleware.js +23 -17
  39. package/core/openapi/build-from-api-routes.js +160 -29
  40. package/core/orm/cache/fingerprint.js +15 -1
  41. package/core/orm/cache/memory-provider.js +27 -2
  42. package/core/orm/eager-loader.js +144 -10
  43. package/core/orm/index.js +20 -7
  44. package/core/orm/model.js +53 -7
  45. package/core/orm/query-builder.js +90 -4
  46. package/core/orm/repository.js +44 -3
  47. package/core/orm/search.js +213 -0
  48. package/core/queue/adapters/database.js +94 -0
  49. package/core/queue/adapters/memory.js +62 -0
  50. package/core/queue/adapters/redis.js +80 -0
  51. package/core/queue/cron.js +331 -0
  52. package/core/queue/index.js +8 -1
  53. package/core/queue/manager.js +205 -1
  54. package/core/queue/scheduler.js +359 -0
  55. package/core/sse/index.js +19 -0
  56. package/core/sse/stream.js +430 -0
  57. package/core/ssr/stream.js +11 -0
  58. package/index.d.ts +1188 -10
  59. package/index.js +88 -96
  60. package/package.json +1 -1
  61. package/plugins/admin-panel/api.js +279 -17
  62. package/plugins/admin-panel/app.js +114 -22
  63. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  64. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  65. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
  66. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  67. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  68. package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
  69. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  70. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  71. package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
  72. package/plugins/admin-panel/core/admin-module.js +13 -7
  73. package/plugins/admin-panel/core/api-extensions.js +30 -2
  74. package/plugins/admin-panel/core/registry.js +96 -16
  75. package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
  76. package/plugins/admin-panel/field-renderers/index.js +2 -0
  77. package/plugins/admin-panel/index.js +96 -0
  78. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  79. package/plugins/admin-panel/modules/admin-users.js +16 -1
  80. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  81. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  82. package/plugins/admin-panel/modules/dashboard.js +283 -0
  83. package/plugins/admin-panel/modules/menu.js +56 -18
  84. package/plugins/admin-panel/modules/user-management.js +24 -6
  85. package/plugins/api-key/crypto.js +101 -0
  86. package/plugins/api-key/index.js +288 -0
  87. package/plugins/api-key/middleware.js +461 -0
  88. package/plugins/api-key/migration-template.js +48 -0
  89. package/plugins/api-key/model.js +50 -0
  90. package/plugins/api-key/scopes.js +125 -0
  91. package/plugins/audit-log/admin-component.js +487 -59
  92. package/plugins/audit-log/api-handlers.js +148 -0
  93. package/plugins/audit-log/index.js +153 -3
  94. package/plugins/audit-log/purge.js +60 -10
  95. package/plugins/dashboard/app.js +152 -27
  96. package/plugins/dashboard/index.js +143 -39
  97. package/plugins/dashboard/styles.js +78 -0
  98. package/plugins/data-exchange/export-xlsx.js +9 -2
  99. package/plugins/data-exchange/import.js +9 -2
  100. package/plugins/data-exchange/index.js +6 -3
  101. package/plugins/email/index.js +132 -2
  102. package/plugins/email/purge.js +60 -10
  103. package/plugins/file-manager/admin-component.js +20 -4
  104. package/plugins/file-manager/index.js +21 -4
  105. package/plugins/index.js +10 -0
  106. package/plugins/oauth/index.js +338 -0
  107. package/plugins/orm-cache-admin/index.js +4 -2
  108. package/plugins/queue/index.js +15 -0
  109. package/plugins/schema-explorer/admin-component.js +714 -0
  110. package/plugins/schema-explorer/relations-graph.js +205 -0
  111. package/plugins/schema-explorer.js +248 -69
  112. package/plugins/site-analytics/admin-component.js +23 -0
  113. package/plugins/site-analytics/api-handlers.js +42 -0
  114. package/plugins/site-analytics/index.js +147 -2
  115. package/plugins/site-analytics/purge.js +168 -0
  116. package/plugins/swagger.js +261 -13
  117. package/plugins/upload/index.js +26 -0
  118. package/plugins/upload/orphan-pruner.js +292 -0
  119. package/src/api/api-loader.js +4 -1
  120. package/src/bundle/index.js +517 -0
  121. package/src/bundle/manifest.js +165 -0
  122. package/src/dev/banner.js +296 -0
  123. package/src/dev/colors.js +70 -0
  124. package/src/dev/index.js +62 -0
  125. package/src/dev/profiler.js +354 -0
  126. package/src/dev/typegen.js +497 -0
  127. package/src/dev-reload/index.js +611 -0
  128. package/src/discovery/file-route-parser.js +44 -1
  129. package/src/discovery/scan-routes.js +2 -2
  130. package/src/file-router.js +347 -162
  131. package/src/helpers.js +967 -98
  132. package/src/njk-frontmatter.js +12 -0
  133. package/src/pages/page-loader.js +61 -11
  134. package/src/routing/cascading.js +378 -0
  135. package/src/routing/middleware-resolver.js +92 -0
  136. package/src/server.js +635 -63
@@ -10,6 +10,8 @@ const state = {
10
10
  filter: 'all',
11
11
  search: '',
12
12
  routes: window.__DASHBOARD_DATA__.routes || [],
13
+ models: window.__DASHBOARD_DATA__.models || [],
14
+ services: window.__DASHBOARD_DATA__.services || [],
13
15
  plugins: window.__DASHBOARD_DATA__.plugins || [],
14
16
  config: window.__DASHBOARD_DATA__.config || {}
15
17
  };
@@ -29,18 +31,27 @@ const Icons = {
29
31
  // Components
30
32
  const Header = {
31
33
  view: () => m('.header', [
32
- m('h1', [
33
- Icons.logo(),
34
- 'Webspresso Dashboard'
34
+ m('div', [
35
+ m('h1', [
36
+ Icons.logo(),
37
+ 'Webspresso Dashboard'
38
+ ]),
39
+ m('span.dev-badge', 'development')
35
40
  ]),
36
- m('span.dev-badge', 'development')
41
+ m('.header-actions', [
42
+ m('a.action-link', { href: '/_admin', target: '_blank', rel: 'noopener' }, '⚙️ Admin Panel ↗'),
43
+ m('a.action-link', { href: '/_swagger', target: '_blank', rel: 'noopener' }, '📖 API Docs ↗'),
44
+ m('a.action-link', { href: '/_schema', target: '_blank', rel: 'noopener' }, '🗂️ Schema Explorer ↗')
45
+ ])
37
46
  ])
38
47
  };
39
48
 
40
49
  const Tabs = {
41
50
  view: () => m('.tabs', [
42
- ['routes', 'Routes'],
43
- ['plugins', 'Plugins'],
51
+ ['routes', 'Routes (' + state.routes.length + ')'],
52
+ ['models', 'Models (' + state.models.length + ')'],
53
+ ['services', 'Services (' + state.services.length + ')'],
54
+ ['plugins', 'Plugins (' + state.plugins.length + ')'],
44
55
  ['config', 'Config']
45
56
  ].map(([id, label]) =>
46
57
  m('button.tab', {
@@ -72,32 +83,36 @@ const FilterBar = {
72
83
  };
73
84
 
74
85
  const MethodBadge = {
75
- view: (vnode) => m('span.method-badge.method-' + vnode.attrs.method,
76
- vnode.attrs.method.toUpperCase()
86
+ view: (vnode) => m('span.method-badge.method-' + (vnode.attrs.method || 'get').toLowerCase(),
87
+ (vnode.attrs.method || 'GET').toUpperCase()
77
88
  )
78
89
  };
79
90
 
80
91
  const TypeBadge = {
81
- view: (vnode) => m('span.type-badge.type-' + vnode.attrs.type,
82
- vnode.attrs.type.toUpperCase()
83
- )
92
+ view: (vnode) => {
93
+ const rawType = vnode.attrs.type || 'page';
94
+ const label = rawType === 'page' ? 'SSR' : rawType.toUpperCase();
95
+ return m('span.type-badge.type-' + (rawType === 'page' ? 'ssr' : rawType), label);
96
+ }
84
97
  };
85
98
 
86
99
  const RoutesTable = {
87
100
  view: () => {
88
101
  let routes = state.routes;
89
102
 
90
- // Apply filter
91
- if (state.filter !== 'all') {
92
- routes = routes.filter(r => r.type === state.filter);
103
+ // Apply filter: SSR matches both 'ssr' and 'page'
104
+ if (state.filter === 'ssr') {
105
+ routes = routes.filter(r => r.type === 'ssr' || r.type === 'page');
106
+ } else if (state.filter === 'api') {
107
+ routes = routes.filter(r => r.type === 'api');
93
108
  }
94
109
 
95
- // Apply search
110
+ // Apply search: checks path or pattern or file
96
111
  if (state.search) {
97
112
  const search = state.search.toLowerCase();
98
113
  routes = routes.filter(r =>
99
- r.pattern.toLowerCase().includes(search) ||
100
- r.file.toLowerCase().includes(search)
114
+ ((r.path || r.pattern || '')).toLowerCase().includes(search) ||
115
+ ((r.file || '')).toLowerCase().includes(search)
101
116
  );
102
117
  }
103
118
 
@@ -121,17 +136,18 @@ const RoutesTable = {
121
136
  ])
122
137
  ),
123
138
  m('tbody',
124
- routes.map(route =>
125
- m('tr', [
139
+ routes.map(route => {
140
+ const displayPath = route.path || route.pattern || '/';
141
+ return m('tr', [
126
142
  m('td', m(MethodBadge, { method: route.method })),
127
143
  m('td', [
128
- m('span.code', route.pattern),
144
+ m('span.code', displayPath),
129
145
  route.isDynamic ? m('span.dynamic-indicator', '⚡ dynamic') : null
130
146
  ]),
131
- m('td', m('span.file-path', route.file)),
147
+ m('td', m('span.file-path', route.file || 'custom')),
132
148
  m('td', m(TypeBadge, { type: route.type }))
133
- ])
134
- )
149
+ ]);
150
+ })
135
151
  )
136
152
  ])
137
153
  );
@@ -140,7 +156,7 @@ const RoutesTable = {
140
156
 
141
157
  const StatsGrid = {
142
158
  view: () => {
143
- const ssrCount = state.routes.filter(r => r.type === 'ssr').length;
159
+ const ssrCount = state.routes.filter(r => r.type === 'ssr' || r.type === 'page').length;
144
160
  const apiCount = state.routes.filter(r => r.type === 'api').length;
145
161
  const dynamicCount = state.routes.filter(r => r.isDynamic).length;
146
162
 
@@ -173,6 +189,116 @@ const RoutesView = {
173
189
  ])
174
190
  };
175
191
 
192
+ const ModelsView = {
193
+ view: () => {
194
+ const models = state.models || [];
195
+ if (models.length === 0) {
196
+ return m('.table-container',
197
+ m('.empty-state', [
198
+ Icons.empty(),
199
+ m('p', 'No ORM models registered in core/orm')
200
+ ])
201
+ );
202
+ }
203
+
204
+ return m('div', [
205
+ m('.stats-grid', [
206
+ m('.card', [
207
+ m('.card-title', 'Total Models'),
208
+ m('.card-value', models.length)
209
+ ]),
210
+ m('.card', [
211
+ m('.card-title', 'Cached Models'),
212
+ m('.card-value', models.filter(m => Boolean(m.cache)).length)
213
+ ]),
214
+ m('.card', [
215
+ m('.card-title', 'Soft-Delete Enabled'),
216
+ m('.card-value', models.filter(m => Boolean(m.softDelete)).length)
217
+ ])
218
+ ]),
219
+ m('.table-container',
220
+ m('table', [
221
+ m('thead',
222
+ m('tr', [
223
+ m('th', 'Model'),
224
+ m('th', 'Table'),
225
+ m('th', 'Primary Key'),
226
+ m('th', 'Columns'),
227
+ m('th', 'Soft Delete'),
228
+ m('th', 'Cache')
229
+ ])
230
+ ),
231
+ m('tbody',
232
+ models.map(mod =>
233
+ m('tr', [
234
+ m('td', m('strong', mod.name)),
235
+ m('td', m('span.code-badge', mod.table)),
236
+ m('td', m('span.badge.badge-blue', mod.primaryKey || 'id')),
237
+ m('td', (mod.columnCount || 0) + ' columns'),
238
+ m('td', mod.softDelete ? m('span.badge.badge-green', 'Enabled') : m('span.badge.badge-gray', 'Disabled')),
239
+ m('td', mod.cache ? m('span.badge.badge-purple', String(mod.cache)) : m('span.badge.badge-gray', 'None'))
240
+ ])
241
+ )
242
+ )
243
+ ])
244
+ )
245
+ ]);
246
+ }
247
+ };
248
+
249
+ const ServicesView = {
250
+ view: () => {
251
+ const services = state.services || [];
252
+ if (services.length === 0) {
253
+ return m('.table-container',
254
+ m('.empty-state', [
255
+ Icons.empty(),
256
+ m('p', 'No services registered in services/ or ServiceRegistry')
257
+ ])
258
+ );
259
+ }
260
+
261
+ return m('div', [
262
+ m('.stats-grid', [
263
+ m('.card', [
264
+ m('.card-title', 'Total Services'),
265
+ m('.card-value', services.length)
266
+ ]),
267
+ m('.card', [
268
+ m('.card-title', 'ACID Transactions'),
269
+ m('.card-value', services.filter(s => s.transaction).length)
270
+ ]),
271
+ m('.card', [
272
+ m('.card-title', 'Auth Protected'),
273
+ m('.card-value', services.filter(s => s.auth).length)
274
+ ])
275
+ ]),
276
+ m('.table-container',
277
+ m('table', [
278
+ m('thead',
279
+ m('tr', [
280
+ m('th', 'Service Name'),
281
+ m('th', 'Transaction (ACID)'),
282
+ m('th', 'Auth Guard'),
283
+ m('th', 'Schema Validation')
284
+ ])
285
+ ),
286
+ m('tbody',
287
+ services.map(srv =>
288
+ m('tr', [
289
+ m('td', m('span.code-badge', srv.name)),
290
+ m('td', srv.transaction ? m('span.badge.badge-green', 'Active') : m('span.badge.badge-gray', 'No')),
291
+ m('td', srv.auth ? m('span.badge.badge-orange', String(srv.auth)) : m('span.badge.badge-gray', 'Public')),
292
+ m('td', srv.hasSchema ? m('span.badge.badge-blue', 'Zod Schema') : m('span.badge.badge-gray', 'None'))
293
+ ])
294
+ )
295
+ )
296
+ ])
297
+ )
298
+ ]);
299
+ }
300
+ };
301
+
176
302
  const PluginsView = {
177
303
  view: () => {
178
304
  if (state.plugins.length === 0) {
@@ -254,6 +380,8 @@ const App = {
254
380
  m(Header),
255
381
  m(Tabs),
256
382
  state.activeTab === 'routes' ? m(RoutesView) : null,
383
+ state.activeTab === 'models' ? m(ModelsView) : null,
384
+ state.activeTab === 'services' ? m(ServicesView) : null,
257
385
  state.activeTab === 'plugins' ? m(PluginsView) : null,
258
386
  state.activeTab === 'config' ? m(ConfigView) : null
259
387
  ])
@@ -262,6 +390,3 @@ const App = {
262
390
  // Mount app
263
391
  m.mount(document.getElementById('app'), App);
264
392
  `;
265
-
266
-
267
-
@@ -1,29 +1,92 @@
1
+ 'use strict';
2
+
1
3
  /**
2
4
  * Webspresso Dashboard Plugin
3
- * Development dashboard for monitoring routes, plugins, and configuration
5
+ * Development dashboard for monitoring routes, ORM models, services, plugins, and configuration
4
6
  * Uses Mithril.js for the SPA interface
7
+ * @module plugins/dashboard
5
8
  */
6
9
 
7
10
  const styles = require('./styles');
8
11
  const appScript = require('./app');
12
+ const { getAllModels } = require('../../core/orm/model');
13
+ const { getServiceRegistry, hasServiceRegistry, hasDb, getDb } = require('../../src/app-context');
9
14
 
10
15
  /**
11
16
  * Filter sensitive environment variables
17
+ * @param {Object} env
18
+ * @returns {Object}
12
19
  */
13
20
  function filterSensitiveEnv(env) {
14
21
  const sensitiveKeys = ['SECRET', 'PASSWORD', 'KEY', 'TOKEN', 'CREDENTIAL', 'PRIVATE'];
15
22
  const filtered = {};
16
-
23
+
17
24
  for (const [key, value] of Object.entries(env)) {
18
- const isSensitive = sensitiveKeys.some(s => key.toUpperCase().includes(s));
25
+ const isSensitive = sensitiveKeys.some((s) => key.toUpperCase().includes(s));
19
26
  filtered[key] = isSensitive ? '••••••••' : value;
20
27
  }
21
-
28
+
22
29
  return filtered;
23
30
  }
24
31
 
32
+ /**
33
+ * Safely collects all registered ORM models
34
+ * @returns {Array<Object>}
35
+ */
36
+ function collectModels() {
37
+ try {
38
+ const models = getAllModels();
39
+ const result = [];
40
+ for (const [name, model] of models.entries()) {
41
+ result.push({
42
+ name,
43
+ table: model.table || name.toLowerCase(),
44
+ primaryKey: model.primaryKey || 'id',
45
+ columns: model.schema && model.schema.shape ? Object.keys(model.schema.shape) : [],
46
+ columnCount: model.schema && model.schema.shape ? Object.keys(model.schema.shape).length : 0,
47
+ relations: Object.keys(model.relations || {}),
48
+ scopes: Object.keys(model.scopes || {}),
49
+ softDelete: Boolean(model.scopes?.softDelete || model.scopes?.notDeleted || model.softDelete || model.paranoid),
50
+ cache: model.cache || false,
51
+ });
52
+ }
53
+ return result;
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Safely collects all registered business services
61
+ * @returns {Array<Object>}
62
+ */
63
+ function collectServices() {
64
+ try {
65
+ if (!hasServiceRegistry()) return [];
66
+ const registry = getServiceRegistry();
67
+ const result = [];
68
+ if (registry && registry.services) {
69
+ for (const [name, def] of registry.services.entries()) {
70
+ result.push({
71
+ name,
72
+ transaction: Boolean(def.transaction),
73
+ auth: def.auth !== undefined ? def.auth : false,
74
+ timeout: def.timeout || null,
75
+ hasSchema: Boolean(def.schema),
76
+ cache: def.cache ? true : false,
77
+ });
78
+ }
79
+ }
80
+ return result;
81
+ } catch {
82
+ return [];
83
+ }
84
+ }
85
+
25
86
  /**
26
87
  * Generate the dashboard HTML
88
+ * @param {Object} data
89
+ * @returns {string}
27
90
  */
28
91
  function generateDashboardHtml(data) {
29
92
  return `<!DOCTYPE html>
@@ -48,25 +111,25 @@ function generateDashboardHtml(data) {
48
111
  /**
49
112
  * Create the dashboard plugin
50
113
  * @param {Object} options - Plugin options
51
- * @param {string} options.path - Dashboard path (default: '/_webspresso')
52
- * @param {boolean} options.enabled - Force enable/disable (default: auto based on NODE_ENV)
114
+ * @param {string} [options.path='/_webspresso'] - Dashboard path
115
+ * @param {boolean} [options.enabled] - Force enable/disable (default: auto based on NODE_ENV)
53
116
  */
54
117
  function dashboardPlugin(options = {}) {
55
118
  const {
56
119
  path: dashboardPath = '/_webspresso',
57
- enabled
120
+ enabled,
58
121
  } = options;
59
-
122
+
60
123
  // Determine if enabled (default: only in development)
61
- const isEnabled = enabled !== undefined
62
- ? enabled
124
+ const isEnabled = enabled !== undefined
125
+ ? enabled
63
126
  : process.env.NODE_ENV !== 'production';
64
-
127
+
65
128
  return {
66
129
  name: 'dashboard',
67
130
  version: '1.0.0',
68
- description: 'Development dashboard for monitoring routes and configuration',
69
-
131
+ description: 'Development dashboard for monitoring routes, models, services, and configuration',
132
+
70
133
  /**
71
134
  * Called after routes are mounted
72
135
  */
@@ -75,68 +138,109 @@ function dashboardPlugin(options = {}) {
75
138
  if (!isEnabled) {
76
139
  return;
77
140
  }
78
-
79
- const { app, routes, options: appOptions } = ctx;
80
-
141
+
142
+ const { app, routes = [], options: appOptions = {} } = ctx;
143
+
81
144
  // Collect plugin info
82
- const pluginManager = ctx.pluginManager || (appOptions && appOptions.pluginManager);
145
+ const pluginManager = ctx.pluginManager || appOptions.pluginManager;
83
146
  const plugins = [];
84
-
147
+
85
148
  // Get plugins from plugin manager if available
86
149
  if (pluginManager && pluginManager.plugins) {
87
150
  for (const [name, plugin] of pluginManager.plugins) {
88
151
  plugins.push({
89
152
  name: plugin.name || name,
90
153
  version: plugin.version || '0.0.0',
91
- description: plugin.description || ''
154
+ description: plugin.description || '',
92
155
  });
93
156
  }
94
157
  }
95
-
158
+
159
+ // Collect models and services
160
+ const models = collectModels();
161
+ const services = collectServices();
162
+
96
163
  // Build config data
97
164
  const config = {
98
165
  env: filterSensitiveEnv({
99
166
  NODE_ENV: process.env.NODE_ENV || 'development',
100
167
  PORT: process.env.PORT || '3000',
101
- BASE_URL: process.env.BASE_URL || 'http://localhost:3000'
168
+ BASE_URL: process.env.BASE_URL || 'http://localhost:3000',
102
169
  }),
103
170
  i18n: {
104
171
  defaultLocale: process.env.DEFAULT_LOCALE || 'en',
105
- supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(',')
172
+ supportedLocales: (process.env.SUPPORTED_LOCALES || 'en').split(','),
106
173
  },
107
174
  server: {
108
175
  port: process.env.PORT || '3000',
109
- baseUrl: process.env.BASE_URL || 'http://localhost:3000'
110
- }
176
+ baseUrl: process.env.BASE_URL || 'http://localhost:3000',
177
+ },
178
+ database: {
179
+ connected: hasDb(),
180
+ dialect: hasDb() ? (getDb().client || getDb().knex?.client?.config?.client || 'knex') : 'none',
181
+ },
111
182
  };
112
-
113
- // Dashboard HTML endpoint
183
+
184
+ const overview = {
185
+ routesCount: routes.length,
186
+ modelsCount: models.length,
187
+ servicesCount: services.length,
188
+ pluginsCount: plugins.length,
189
+ uptime: process.uptime(),
190
+ memoryUsage: process.memoryUsage(),
191
+ };
192
+
193
+ // 1. Dashboard HTML endpoint
114
194
  ctx.addRoute('get', dashboardPath, (req, res) => {
115
- const data = { routes, plugins, config };
195
+ const data = { routes, plugins, config, models, services, overview };
116
196
  res.type('text/html');
117
197
  res.send(generateDashboardHtml(data));
118
198
  });
119
-
120
- // JSON API endpoints
121
- ctx.addRoute('get', dashboardPath + '/api/routes', (req, res) => {
199
+
200
+ // 2. JSON API endpoints
201
+ ctx.addRoute('get', `${dashboardPath}/api/routes`, (req, res) => {
122
202
  res.json(routes);
123
203
  });
124
-
125
- ctx.addRoute('get', dashboardPath + '/api/plugins', (req, res) => {
204
+
205
+ ctx.addRoute('get', `${dashboardPath}/api/plugins`, (req, res) => {
126
206
  res.json(plugins);
127
207
  });
128
-
129
- ctx.addRoute('get', dashboardPath + '/api/config', (req, res) => {
208
+
209
+ ctx.addRoute('get', `${dashboardPath}/api/config`, (req, res) => {
130
210
  res.json(config);
131
211
  });
132
-
212
+
213
+ ctx.addRoute('get', `${dashboardPath}/api/models`, (req, res) => {
214
+ res.json(collectModels());
215
+ });
216
+
217
+ ctx.addRoute('get', `${dashboardPath}/api/services`, (req, res) => {
218
+ res.json(collectServices());
219
+ });
220
+
221
+ ctx.addRoute('get', `${dashboardPath}/api/overview`, (req, res) => {
222
+ res.json({
223
+ routesCount: (ctx.routes || []).length,
224
+ modelsCount: collectModels().length,
225
+ servicesCount: collectServices().length,
226
+ pluginsCount: plugins.length,
227
+ nodeVersion: process.version,
228
+ platform: process.platform,
229
+ uptime: process.uptime(),
230
+ memoryUsage: process.memoryUsage(),
231
+ });
232
+ });
233
+
133
234
  // Log dashboard URL
134
235
  console.log(`\n📊 Dashboard available at: http://localhost:${process.env.PORT || 3000}${dashboardPath}\n`);
135
- }
236
+ },
136
237
  };
137
238
  }
138
239
 
139
240
  module.exports = dashboardPlugin;
140
-
141
-
142
-
241
+ module.exports.dashboardPlugin = dashboardPlugin;
242
+ module.exports.devToolbarPlugin = dashboardPlugin;
243
+ module.exports.devtoolsPlugin = dashboardPlugin;
244
+ module.exports.generateDashboardHtml = generateDashboardHtml;
245
+ module.exports.collectModels = collectModels;
246
+ module.exports.collectServices = collectServices;
@@ -378,6 +378,84 @@ tr:hover td {
378
378
  animation: spin 0.8s linear infinite;
379
379
  margin-right: 12px;
380
380
  }
381
+
382
+ /* Header Actions */
383
+ .header-actions {
384
+ display: flex;
385
+ align-items: center;
386
+ gap: 8px;
387
+ }
388
+
389
+ .action-link {
390
+ display: inline-flex;
391
+ align-items: center;
392
+ gap: 6px;
393
+ padding: 6px 12px;
394
+ border-radius: 6px;
395
+ font-size: 13px;
396
+ font-weight: 500;
397
+ text-decoration: none;
398
+ background: var(--bg-tertiary);
399
+ border: 1px solid var(--border);
400
+ color: var(--text-primary);
401
+ transition: all 0.15s ease;
402
+ }
403
+
404
+ .action-link:hover {
405
+ background: var(--border);
406
+ border-color: var(--accent);
407
+ color: var(--text-primary);
408
+ }
409
+
410
+ /* Badges */
411
+ .badge {
412
+ display: inline-block;
413
+ padding: 2px 8px;
414
+ border-radius: 10px;
415
+ font-size: 11px;
416
+ font-weight: 600;
417
+ text-transform: uppercase;
418
+ }
419
+
420
+ .badge-green {
421
+ background: rgba(63, 185, 80, 0.15);
422
+ color: var(--green);
423
+ border: 1px solid rgba(63, 185, 80, 0.3);
424
+ }
425
+
426
+ .badge-blue {
427
+ background: rgba(88, 166, 255, 0.15);
428
+ color: var(--accent);
429
+ border: 1px solid rgba(88, 166, 255, 0.3);
430
+ }
431
+
432
+ .badge-purple {
433
+ background: rgba(163, 113, 247, 0.15);
434
+ color: var(--purple);
435
+ border: 1px solid rgba(163, 113, 247, 0.3);
436
+ }
437
+
438
+ .badge-orange {
439
+ background: rgba(210, 153, 34, 0.15);
440
+ color: var(--orange);
441
+ border: 1px solid rgba(210, 153, 34, 0.3);
442
+ }
443
+
444
+ .badge-gray {
445
+ background: rgba(110, 118, 129, 0.15);
446
+ color: var(--text-muted);
447
+ border: 1px solid rgba(110, 118, 129, 0.3);
448
+ }
449
+
450
+ .code-badge {
451
+ font-family: 'SF Mono', SFMono-Regular, ui-monospace, monospace;
452
+ font-size: 12px;
453
+ background: var(--bg-tertiary);
454
+ padding: 2px 6px;
455
+ border-radius: 4px;
456
+ border: 1px solid var(--border);
457
+ color: var(--accent);
458
+ }
381
459
  `;
382
460
 
383
461
 
@@ -3,7 +3,13 @@
3
3
  * @module plugins/data-exchange/export-xlsx
4
4
  */
5
5
 
6
- const ExcelJS = require('exceljs');
6
+ let ExcelJS = null;
7
+ function getExcelJS() {
8
+ if (!ExcelJS) {
9
+ ExcelJS = require('exceljs');
10
+ }
11
+ return ExcelJS;
12
+ }
7
13
  const { sanitizeForOutput } = require('../../core/orm/utils');
8
14
  const { resolveExportRecords } = require('./record-selection');
9
15
 
@@ -42,7 +48,8 @@ async function buildXlsxBuffer(model, records) {
42
48
  const clean = sanitizeForOutput(records, model);
43
49
  const columns = getModelExportColumns(model, clean);
44
50
 
45
- const wb = new ExcelJS.Workbook();
51
+ const ExcelLib = getExcelJS();
52
+ const wb = new ExcelLib.Workbook();
46
53
  const ws = wb.addWorksheet('Export', {
47
54
  views: [{ state: 'frozen', ySplit: 1 }],
48
55
  });
@@ -4,7 +4,13 @@
4
4
  */
5
5
 
6
6
  const multer = require('multer');
7
- const ExcelJS = require('exceljs');
7
+ let ExcelJS = null;
8
+ function getExcelJS() {
9
+ if (!ExcelJS) {
10
+ ExcelJS = require('exceljs');
11
+ }
12
+ return ExcelJS;
13
+ }
8
14
  const { parse: parseCsv } = require('csv-parse/sync');
9
15
  const { buildHeaderMapping, dataRowsToObjects } = require('./parse-table');
10
16
 
@@ -132,7 +138,8 @@ function parseCsvToRows(buffer) {
132
138
  }
133
139
 
134
140
  async function parseXlsxToRows(buffer) {
135
- const wb = new ExcelJS.Workbook();
141
+ const ExcelLib = getExcelJS();
142
+ const wb = new ExcelLib.Workbook();
136
143
  await wb.xlsx.load(buffer);
137
144
  const ws = wb.worksheets[0];
138
145
  if (!ws) return [];
@@ -76,14 +76,17 @@ function dataExchangePlugin(options = {}) {
76
76
  models: '*',
77
77
  confirm: false,
78
78
  handler: async (records, modelName, { db: d }) => {
79
+ const targetModel = typeof modelName === 'object' && modelName !== null
80
+ ? (modelName.name || modelName.modelName || modelName.model || String(modelName))
81
+ : String(modelName);
79
82
  const { getModel } = require('../../core/orm/model');
80
- const model = d.getModel ? d.getModel(modelName) : getModel(modelName);
83
+ const model = d.getModel ? d.getModel(targetModel) : getModel(targetModel);
81
84
  const pk = model?.primaryKey || 'id';
82
85
  const ids = records.map((r) => r[pk]).join(',');
83
86
  return {
84
87
  download: true,
85
- url: `${adminPath}/api/data-exchange/export/${modelName}?ids=${encodeURIComponent(ids)}`,
86
- filename: `${modelName}_export.xlsx`,
88
+ url: `${adminPath}/api/data-exchange/export/${targetModel}?ids=${encodeURIComponent(ids)}`,
89
+ filename: `${targetModel}_export.xlsx`,
87
90
  };
88
91
  },
89
92
  });