webspresso 0.0.96 → 0.0.98

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 (121) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/.agents/skills/webspresso-admin-and-plugins/SKILL.md +122 -0
  7. package/.agents/skills/webspresso-framework/SKILL.md +56 -0
  8. package/.agents/skills/webspresso-orm-and-database/SKILL.md +119 -0
  9. package/.agents/skills/webspresso-routing-and-api/SKILL.md +114 -0
  10. package/.agents/skills/webspresso-services/SKILL.md +101 -0
  11. package/bin/commands/add-esbuild.js +80 -0
  12. package/bin/commands/add-tailwind.js +76 -31
  13. package/bin/commands/admin-password.js +2 -1
  14. package/bin/commands/admin-setup.js +1 -1
  15. package/bin/commands/agents-init.js +51 -11
  16. package/bin/commands/analytics-prune.js +57 -0
  17. package/bin/commands/api.js +207 -15
  18. package/bin/commands/audit-prune.js +1 -1
  19. package/bin/commands/build.js +99 -0
  20. package/bin/commands/dev.js +64 -35
  21. package/bin/commands/doctor.js +26 -0
  22. package/bin/commands/email-prune.js +47 -0
  23. package/bin/commands/favicon-generate.js +1 -1
  24. package/bin/commands/make-job.js +86 -0
  25. package/bin/commands/make-model.js +250 -0
  26. package/bin/commands/new.js +45 -28
  27. package/bin/commands/openapi.js +102 -0
  28. package/bin/commands/orm-map.js +5 -4
  29. package/bin/commands/page.js +6 -3
  30. package/bin/commands/polar-migrate.js +1 -1
  31. package/bin/commands/queue-prune.js +59 -0
  32. package/bin/commands/routes.js +52 -0
  33. package/bin/commands/seed.js +1 -1
  34. package/bin/commands/service.js +6 -3
  35. package/bin/commands/typegen.js +106 -0
  36. package/bin/commands/upload-prune.js +79 -0
  37. package/bin/webspresso.js +22 -0
  38. package/core/applySchema.js +13 -6
  39. package/core/auth/hash.js +31 -17
  40. package/core/compression/middleware.js +23 -17
  41. package/core/openapi/build-from-api-routes.js +160 -29
  42. package/core/orm/cache/fingerprint.js +15 -1
  43. package/core/orm/cache/memory-provider.js +27 -2
  44. package/core/orm/index.js +12 -7
  45. package/core/orm/model.js +49 -7
  46. package/core/queue/adapters/database.js +94 -0
  47. package/core/queue/adapters/memory.js +62 -0
  48. package/core/queue/adapters/redis.js +80 -0
  49. package/core/queue/manager.js +123 -0
  50. package/core/ssr/stream.js +11 -0
  51. package/index.d.ts +660 -10
  52. package/index.js +44 -96
  53. package/package.json +32 -12
  54. package/plugins/admin-panel/api.js +279 -17
  55. package/plugins/admin-panel/app.js +124 -31
  56. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  57. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  58. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  59. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  60. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  61. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  62. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  63. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  64. package/plugins/admin-panel/core/admin-module.js +13 -7
  65. package/plugins/admin-panel/core/api-extensions.js +34 -2
  66. package/plugins/admin-panel/core/registry.js +96 -16
  67. package/plugins/admin-panel/index.js +96 -0
  68. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  69. package/plugins/admin-panel/modules/admin-users.js +16 -1
  70. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  71. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  72. package/plugins/admin-panel/modules/dashboard.js +283 -0
  73. package/plugins/admin-panel/modules/menu.js +56 -18
  74. package/plugins/admin-panel/modules/user-management.js +24 -6
  75. package/plugins/audit-log/admin-component.js +11 -0
  76. package/plugins/audit-log/api-handlers.js +34 -0
  77. package/plugins/audit-log/index.js +129 -3
  78. package/plugins/audit-log/purge.js +60 -10
  79. package/plugins/basic-auth/index.js +3 -1
  80. package/plugins/content/client/inline-edit.js +12 -12
  81. package/plugins/cors.js +6 -7
  82. package/plugins/dashboard/app.js +152 -27
  83. package/plugins/dashboard/index.js +143 -39
  84. package/plugins/dashboard/styles.js +78 -0
  85. package/plugins/data-exchange/export-xlsx.js +9 -2
  86. package/plugins/data-exchange/import.js +9 -2
  87. package/plugins/data-exchange/index.js +6 -3
  88. package/plugins/email/index.js +132 -2
  89. package/plugins/email/purge.js +60 -10
  90. package/plugins/file-manager/core.js +1 -1
  91. package/plugins/file-manager/security.js +33 -23
  92. package/plugins/index.js +49 -87
  93. package/plugins/mcp/transports/sse.js +4 -2
  94. package/plugins/orm-cache-admin/index.js +4 -2
  95. package/plugins/queue/index.js +11 -0
  96. package/plugins/schema-explorer/admin-component.js +714 -0
  97. package/plugins/schema-explorer/relations-graph.js +205 -0
  98. package/plugins/schema-explorer.js +248 -69
  99. package/plugins/site-analytics/admin-component.js +30 -8
  100. package/plugins/site-analytics/api-handlers.js +42 -0
  101. package/plugins/site-analytics/index.js +147 -2
  102. package/plugins/site-analytics/purge.js +168 -0
  103. package/plugins/swagger.js +261 -13
  104. package/plugins/upload/index.js +26 -0
  105. package/plugins/upload/orphan-pruner.js +292 -0
  106. package/src/api/api-loader.js +3 -1
  107. package/src/bundle/index.js +517 -0
  108. package/src/bundle/manifest.js +165 -0
  109. package/src/dev/banner.js +296 -0
  110. package/src/dev/colors.js +70 -0
  111. package/src/dev/index.js +62 -0
  112. package/src/dev/profiler.js +354 -0
  113. package/src/dev/typegen.js +497 -0
  114. package/src/dev-reload/index.js +611 -0
  115. package/src/file-router.js +213 -63
  116. package/src/helpers.js +958 -98
  117. package/src/njk-frontmatter.js +12 -0
  118. package/src/pages/page-loader.js +61 -11
  119. package/src/server.js +629 -63
  120. package/src/services/discovery.js +6 -1
  121. package/src/services/index.js +6 -13
@@ -0,0 +1,714 @@
1
+ /**
2
+ * Schema Explorer Admin SPA Component (Mithril.js)
3
+ * Clean, modern, zero-CDN interface for exploring database tables, columns, constraints, and relationships.
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ /**
9
+ * Generates the Mithril client-side component code as a string
10
+ * @param {Object} [options]
11
+ * @param {string} [options.apiPrefix='/schema']
12
+ * @returns {string} JavaScript client code
13
+ */
14
+ function generateSchemaExplorerAdminComponent(options = {}) {
15
+ const { apiPrefix = '/schema' } = options;
16
+
17
+ return `
18
+ (function() {
19
+
20
+ function getSchemaApi(method, path) {
21
+ var adminPath = window.__ADMIN_PATH__ || '/_admin';
22
+ var url = adminPath + '/api${apiPrefix}' + (path || '');
23
+ return fetch(url, {
24
+ method: method || 'GET',
25
+ credentials: 'include',
26
+ headers: { 'Accept': 'application/json' }
27
+ }).then(function(res) {
28
+ if (!res.ok) {
29
+ return res.json().catch(function() { return { error: res.statusText }; }).then(function(err) {
30
+ throw new Error(err.error || 'Failed to load schema data (' + res.status + ')');
31
+ });
32
+ }
33
+ return res.json();
34
+ });
35
+ }
36
+
37
+ function copyTextToClipboard(text, onSuccess) {
38
+ if (navigator.clipboard && navigator.clipboard.writeText) {
39
+ navigator.clipboard.writeText(text).then(function() {
40
+ if (typeof onSuccess === 'function') onSuccess();
41
+ }).catch(function() {
42
+ fallbackCopy(text, onSuccess);
43
+ });
44
+ } else {
45
+ fallbackCopy(text, onSuccess);
46
+ }
47
+ }
48
+
49
+ function fallbackCopy(text, onSuccess) {
50
+ var ta = document.createElement('textarea');
51
+ ta.value = text;
52
+ ta.style.position = 'fixed';
53
+ ta.style.opacity = '0';
54
+ document.body.appendChild(ta);
55
+ ta.select();
56
+ try {
57
+ document.execCommand('copy');
58
+ if (typeof onSuccess === 'function') onSuccess();
59
+ } catch (e) {
60
+ console.error('Copy failed:', e);
61
+ }
62
+ document.body.removeChild(ta);
63
+ }
64
+
65
+ function downloadFile(content, fileName, contentType) {
66
+ var blob = new Blob([content], { type: contentType || 'text/plain;charset=utf-8' });
67
+ var url = URL.createObjectURL(blob);
68
+ var a = document.createElement('a');
69
+ a.href = url;
70
+ a.download = fileName;
71
+ document.body.appendChild(a);
72
+ a.click();
73
+ setTimeout(function() {
74
+ document.body.removeChild(a);
75
+ URL.revokeObjectURL(url);
76
+ }, 100);
77
+ }
78
+
79
+ var SchemaExplorerPage = {
80
+ oninit: function(vnode) {
81
+ vnode.state.loading = true;
82
+ vnode.state.error = null;
83
+ vnode.state.models = [];
84
+ vnode.state.relationships = [];
85
+ vnode.state.mermaid = '';
86
+ vnode.state.activeTab = 'details'; // 'details' | 'graph' | 'mermaid'
87
+ vnode.state.selectedModelName = null;
88
+ vnode.state.searchQuery = '';
89
+ vnode.state.selectedGraphModel = null;
90
+ vnode.state.copiedMermaid = false;
91
+ vnode.state.copiedJson = false;
92
+
93
+ this.loadData(vnode);
94
+ },
95
+
96
+ loadData: function(vnode) {
97
+ vnode.state.loading = true;
98
+ vnode.state.error = null;
99
+
100
+ getSchemaApi('GET', '')
101
+ .then(function(data) {
102
+ vnode.state.models = data.models || [];
103
+ vnode.state.relationships = data.relationships || [];
104
+ vnode.state.mermaid = data.mermaid || '';
105
+
106
+ // Auto-select first model if none selected
107
+ if (!vnode.state.selectedModelName && vnode.state.models.length > 0) {
108
+ vnode.state.selectedModelName = vnode.state.models[0].name;
109
+ }
110
+
111
+ vnode.state.loading = false;
112
+ m.redraw();
113
+ })
114
+ .catch(function(err) {
115
+ vnode.state.error = err.message || String(err);
116
+ vnode.state.loading = false;
117
+ m.redraw();
118
+ });
119
+ },
120
+
121
+ selectModel: function(vnode, name) {
122
+ vnode.state.selectedModelName = name;
123
+ vnode.state.activeTab = 'details';
124
+ m.redraw();
125
+ },
126
+
127
+ copyMermaid: function(vnode) {
128
+ var self = this;
129
+ copyTextToClipboard(vnode.state.mermaid, function() {
130
+ vnode.state.copiedMermaid = true;
131
+ m.redraw();
132
+ setTimeout(function() {
133
+ vnode.state.copiedMermaid = false;
134
+ m.redraw();
135
+ }, 2000);
136
+ });
137
+ },
138
+
139
+ downloadJson: function(vnode) {
140
+ var payload = JSON.stringify({
141
+ models: vnode.state.models,
142
+ relationships: vnode.state.relationships,
143
+ generatedAt: new Date().toISOString()
144
+ }, null, 2);
145
+ downloadFile(payload, 'database-schema.json', 'application/json');
146
+ },
147
+
148
+ view: function(vnode) {
149
+ var s = vnode.state;
150
+ var self = this;
151
+
152
+ // Filter models by search query
153
+ var q = (s.searchQuery || '').toLowerCase().trim();
154
+ var filteredModels = s.models.filter(function(m) {
155
+ if (!q) return true;
156
+ if (m.name.toLowerCase().indexOf(q) !== -1) return true;
157
+ if (m.table.toLowerCase().indexOf(q) !== -1) return true;
158
+ if (m.columns && m.columns.some(function(c) { return c.name.toLowerCase().indexOf(q) !== -1; })) return true;
159
+ return false;
160
+ });
161
+
162
+ // Active selected model
163
+ var selectedModel = s.models.find(function(m) { return m.name === s.selectedModelName; }) || filteredModels[0] || null;
164
+
165
+ // Compute stats
166
+ var totalColumns = s.models.reduce(function(acc, m) { return acc + (m.columns ? m.columns.length : 0); }, 0);
167
+ var totalRelations = s.relationships.length;
168
+
169
+ return m('div.space-y-6', [
170
+ // 1. Top Header Banner
171
+ m('div.bg-gradient-to-r.from-slate-900.via-indigo-950.to-slate-900.text-white.p-6.rounded-2xl.shadow-lg.border.border-slate-800', [
172
+ m('div.flex.flex-col.md:flex-row.md:items-center.justify-between.gap-4', [
173
+ m('div.flex.items-center.space-x-4', [
174
+ m('div.w-12.h-12.rounded-xl.bg-indigo-600.bg-opacity-80.flex.items-center.justify-center.text-2xl.shadow-inner.flex-shrink-0', '🗄️'),
175
+ m('div', [
176
+ m('h1.text-2xl.font-extrabold.tracking-tight', 'Database Schema & Relations Explorer'),
177
+ m('p.text-sm.text-slate-300.mt-1', 'Explore database tables, column constraints, and relationships interactively.'),
178
+ ]),
179
+ ]),
180
+ m('div.flex.flex-wrap.items-center.gap-2', [
181
+ m('button.inline-flex.items-center.px-3.py-2.rounded-lg.text-xs.font-semibold.bg-slate-800.hover:bg-slate-700.text-slate-200.border.border-slate-700.transition-colors.shadow-sm', {
182
+ onclick: function() { self.loadData(vnode); },
183
+ title: 'Refresh Schema'
184
+ }, [
185
+ m('span.mr-1.5', '🔄'),
186
+ 'Refresh'
187
+ ]),
188
+ m('button.inline-flex.items-center.px-3.py-2.rounded-lg.text-xs.font-semibold.bg-indigo-600.hover:bg-indigo-500.text-white.transition-colors.shadow-sm', {
189
+ onclick: function() { self.copyMermaid(vnode); },
190
+ title: 'Copy Mermaid ER Code'
191
+ }, [
192
+ m('span.mr-1.5', s.copiedMermaid ? '✓' : '📋'),
193
+ s.copiedMermaid ? 'Copied!' : 'Copy Mermaid'
194
+ ]),
195
+ m('button.inline-flex.items-center.px-3.py-2.rounded-lg.text-xs.font-semibold.bg-slate-800.hover:bg-slate-700.text-slate-200.border.border-slate-700.transition-colors.shadow-sm', {
196
+ onclick: function() { self.downloadJson(vnode); },
197
+ title: 'Download Schema as JSON'
198
+ }, [
199
+ m('span.mr-1.5', '💾'),
200
+ 'Download JSON'
201
+ ]),
202
+ ]),
203
+ ]),
204
+
205
+ // Stat Counter Badges
206
+ m('div.grid.grid-cols-2.sm:grid-cols-4.gap-3.mt-6.pt-4.border-t.border-slate-800/80', [
207
+ renderStatPill('📦 Models', s.models.length + ' Tables'),
208
+ renderStatPill('📑 Total Columns', totalColumns + ' Columns'),
209
+ renderStatPill('🔗 Relationships', totalRelations + ' Connections'),
210
+ renderStatPill('⚡ Status', s.loading ? 'Loading…' : 'Active & Synced'),
211
+ ]),
212
+ ]),
213
+
214
+ // Loading & Error States
215
+ s.loading && m('div.flex.items-center.justify-center.py-16.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700', [
216
+ m('div.text-center', [
217
+ m('div.inline-block.animate-spin.rounded-full.h-8.w-8.border-4.border-indigo-500.border-t-transparent.mb-3'),
218
+ m('p.text-sm.text-gray-500.dark:text-slate-400', 'Scanning schemas and relationships…'),
219
+ ])
220
+ ]),
221
+
222
+ s.error && m('div.p-4.rounded-xl.bg-red-50.dark:bg-red-950/40.border.border-red-200.dark:border-red-800.text-red-700.dark:text-red-300.flex.items-start.space-x-3', [
223
+ m('span.text-xl', '⚠️'),
224
+ m('div', [
225
+ m('h4.font-bold.text-sm', 'Schema Loading Error'),
226
+ m('p.text-xs.mt-1', s.error),
227
+ m('button.mt-2.text-xs.underline.font-semibold', { onclick: function() { self.loadData(vnode); } }, 'Retry'),
228
+ ])
229
+ ]),
230
+
231
+ !s.loading && !s.error && m('div.space-y-6', [
232
+ // 2. Tab Navigation
233
+ m('div.border-b.border-gray-200.dark:border-slate-700.flex.space-x-4', [
234
+ renderTabButton('details', '📋 Table & Column Details', s.activeTab, function() { s.activeTab = 'details'; }),
235
+ renderTabButton('graph', '🌐 Relationship Map (ER)', s.activeTab, function() { s.activeTab = 'graph'; }),
236
+ renderTabButton('mermaid', '🧜‍♂️ Mermaid ER Code', s.activeTab, function() { s.activeTab = 'mermaid'; }),
237
+ ]),
238
+
239
+ // 3. Tab Contents
240
+ s.activeTab === 'details' && renderDetailsTab(vnode, filteredModels, selectedModel, self),
241
+ s.activeTab === 'graph' && renderGraphTab(vnode, self),
242
+ s.activeTab === 'mermaid' && renderMermaidTab(vnode, self),
243
+ ])
244
+ ]);
245
+ }
246
+ };
247
+
248
+ function renderStatPill(label, val) {
249
+ return m('div.bg-slate-800/60.rounded-xl.p-3.border.border-slate-700/50', [
250
+ m('div.text-xs.text-slate-400.font-medium', label),
251
+ m('div.text-base.font-bold.text-white.mt-0.5', val),
252
+ ]);
253
+ }
254
+
255
+ function renderTabButton(id, title, activeTab, onClick) {
256
+ var isActive = activeTab === id;
257
+ return m('button.py-3.px-4.border-b-2.text-sm.font-medium.transition-colors.flex.items-center.space-x-2', {
258
+ class: isActive
259
+ ? 'border-indigo-600 text-indigo-600 dark:text-indigo-400 font-semibold'
260
+ : 'border-transparent text-gray-500 hover:text-gray-700 dark:text-slate-400 dark:hover:text-slate-200',
261
+ onclick: onClick
262
+ }, title);
263
+ }
264
+
265
+ /** TAB 1: Table & Column Details */
266
+ function renderDetailsTab(vnode, filteredModels, selectedModel, self) {
267
+ var s = vnode.state;
268
+
269
+ return m('div.grid.grid-cols-1.lg:grid-cols-12.gap-6', [
270
+ // Left: Table List
271
+ m('div.lg:col-span-4.space-y-4', [
272
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.p-4', [
273
+ m('div.relative.mb-3', [
274
+ m('input.w-full.bg-gray-50.dark:bg-slate-900.border.border-gray-200.dark:border-slate-700.rounded-lg.px-3.py-2.text-sm.text-gray-900.dark:text-slate-100.placeholder-gray-400.focus:outline-none.focus:ring-2.focus:ring-indigo-500', {
275
+ placeholder: '🔍 Search tables or columns…',
276
+ value: s.searchQuery,
277
+ oninput: function(e) { s.searchQuery = e.target.value; }
278
+ }),
279
+ s.searchQuery && m('button.absolute.right-2.top-2.text-gray-400.hover:text-gray-600.text-xs', {
280
+ onclick: function() { s.searchQuery = ''; }
281
+ }, '✕')
282
+ ]),
283
+
284
+ m('div.text-xs.font-semibold.uppercase.tracking-wider.text-gray-400.dark:text-slate-500.mb-2',
285
+ 'Tables (' + filteredModels.length + ')'
286
+ ),
287
+
288
+ m('div.space-y-1.5.max-h-[600px].overflow-y-auto.pr-1', filteredModels.map(function(model) {
289
+ var isSelected = selectedModel && selectedModel.name === model.name;
290
+ var colCount = (model.columns || []).length;
291
+ var outCount = (model.outgoingRelations || []).length;
292
+ var inCount = (model.incomingRelations || []).length;
293
+
294
+ return m('div.p-3.rounded-xl.cursor-pointer.transition-all.border', {
295
+ class: isSelected
296
+ ? 'bg-indigo-50.dark:bg-indigo-950/40.border-indigo-400.dark:border-indigo-600.shadow-sm'
297
+ : 'bg-white.dark:bg-slate-800.border-gray-200.dark:border-slate-700/60.hover:border-indigo-300.hover:bg-gray-50.dark:hover:bg-slate-700/50',
298
+ onclick: function() { self.selectModel(vnode, model.name); }
299
+ }, [
300
+ m('div.flex.items-center.justify-between', [
301
+ m('span.font-bold.text-sm', {
302
+ class: isSelected ? 'text-indigo-900 dark:text-indigo-200' : 'text-gray-900 dark:text-slate-100'
303
+ }, model.name),
304
+ m('span.text-xs.font-mono.px-2.py-0.5.rounded-md.bg-gray-100.dark:bg-slate-700.text-gray-600.dark:text-slate-300',
305
+ model.table
306
+ ),
307
+ ]),
308
+ m('div.flex.items-center.space-x-3.mt-2.text-xs.text-gray-500.dark:text-slate-400', [
309
+ m('span', colCount + ' cols'),
310
+ (outCount + inCount > 0) && m('span.text-indigo-600.dark:text-indigo-400.font-medium',
311
+ '🔗 ' + (outCount + inCount) + ' relations'
312
+ ),
313
+ model.scopes && model.scopes.softDelete && m('span.text-amber-600.dark:text-amber-400', '🗑️ SoftDel'),
314
+ ])
315
+ ]);
316
+ }))
317
+ ])
318
+ ]),
319
+
320
+ // Right: Selected Model Schema Details
321
+ m('div.lg:col-span-8.space-y-6', [
322
+ !selectedModel ? m('div.p-12.text-center.bg-white.dark:bg-slate-800.rounded-xl.border.border-gray-200.dark:border-slate-700', [
323
+ m('p.text-gray-500.dark:text-slate-400', 'Select a table on the left to view details.')
324
+ ]) : [
325
+ // Model Header Card
326
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.p-6', [
327
+ m('div.flex.flex-col.sm:flex-row.sm:items-center.justify-between.gap-4', [
328
+ m('div', [
329
+ m('div.flex.items-center.space-x-3', [
330
+ m('h2.text-xl.font-extrabold.text-gray-900.dark:text-slate-100', selectedModel.name),
331
+ m('span.text-xs.font-mono.px-2.5.py-1.rounded-lg.bg-slate-100.dark:bg-slate-700.text-slate-700.dark:text-slate-200.border.border-slate-200.dark:border-slate-600',
332
+ 'table: ' + selectedModel.table
333
+ ),
334
+ ]),
335
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1',
336
+ 'Primary Key: ' + (selectedModel.primaryKey || 'id') + ' • Total ' + (selectedModel.columns || []).length + ' Columns'
337
+ ),
338
+ ]),
339
+
340
+ // Scopes Badges
341
+ m('div.flex.flex-wrap.gap-2', [
342
+ selectedModel.scopes && selectedModel.scopes.timestamps
343
+ ? m('span.text-xs.font-medium.px-2.5.py-1.rounded-md.bg-emerald-50.dark:bg-emerald-950/40.text-emerald-700.dark:text-emerald-300.border.border-emerald-200.dark:border-emerald-800', '⏱️ Timestamps')
344
+ : null,
345
+ selectedModel.scopes && selectedModel.scopes.softDelete
346
+ ? m('span.text-xs.font-medium.px-2.5.py-1.rounded-md.bg-amber-50.dark:bg-amber-950/40.text-amber-700.dark:text-amber-300.border.border-amber-200.dark:border-amber-800', '🗑️ Soft Delete')
347
+ : null,
348
+ selectedModel.scopes && selectedModel.scopes.tenant
349
+ ? m('span.text-xs.font-medium.px-2.5.py-1.rounded-md.bg-purple-50.dark:bg-purple-950/40.text-purple-700.dark:text-purple-300.border.border-purple-200.dark:border-purple-800', '🏢 Multi-Tenant')
350
+ : null,
351
+ ])
352
+ ]),
353
+ ]),
354
+
355
+ // Columns Breakdown
356
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.overflow-hidden', [
357
+ m('div.px-6.py-4.border-b.border-gray-200.dark:border-slate-700.flex.items-center.justify-between', [
358
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'Column Definitions'),
359
+ m('span.text-xs.text-gray-500.dark:text-slate-400', (selectedModel.columns || []).length + ' fields')
360
+ ]),
361
+
362
+ m('div.overflow-x-auto', [
363
+ m('table.min-w-full.divide-y.divide-gray-200.dark:divide-slate-700.text-left.text-sm', [
364
+ m('thead.bg-gray-50.dark:bg-slate-900/60', [
365
+ m('tr', [
366
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Column'),
367
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Data Type'),
368
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Nullable'),
369
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Constraints'),
370
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Default'),
371
+ ])
372
+ ]),
373
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700/60', (selectedModel.columns || []).map(function(col) {
374
+ var isPk = col.primary || col.name === selectedModel.primaryKey;
375
+ var isFk = col.references || (selectedModel.outgoingRelations || []).some(function(r) { return r.foreignKey === col.name; });
376
+
377
+ return m('tr.hover:bg-gray-50.dark:hover:bg-slate-700/30.transition-colors', [
378
+ // Name + Icon
379
+ m('td.px-4.py-3.font-mono.text-sm.font-semibold.text-gray-900.dark:text-slate-100.flex.items-center.space-x-2', [
380
+ isPk && m('span.text-amber-500', { title: 'Primary Key' }, '🔑'),
381
+ !isPk && isFk && m('span.text-indigo-500', { title: 'Foreign Key' }, '🔗'),
382
+ m('span', col.name)
383
+ ]),
384
+
385
+ // Type pill
386
+ m('td.px-4.py-3', [
387
+ renderTypeBadge(col)
388
+ ]),
389
+
390
+ // Nullable
391
+ m('td.px-4.py-3.text-xs', [
392
+ col.nullable
393
+ ? m('span.px-2.py-0.5.rounded.bg-amber-50.dark:bg-amber-950/40.text-amber-700.dark:text-amber-300.font-medium', 'NULL')
394
+ : m('span.px-2.py-0.5.rounded.bg-gray-100.dark:bg-slate-700.text-gray-700.dark:text-slate-300.font-medium', 'NOT NULL')
395
+ ]),
396
+
397
+ // Constraints
398
+ m('td.px-4.py-3', [
399
+ m('div.flex.flex-wrap.gap-1.5', [
400
+ isPk && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-amber-100.dark:bg-amber-900/60.text-amber-800.dark:text-amber-200', 'PK'),
401
+ col.unique && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-purple-100.dark:bg-purple-900/60.text-purple-800.dark:text-purple-200', 'UNIQUE'),
402
+ col.index && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-slate-100.dark:bg-slate-700.text-slate-700.dark:text-slate-300', 'INDEX'),
403
+ col.autoIncrement && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-blue-100.dark:bg-blue-900/60.text-blue-800.dark:text-blue-200', 'AUTO_INC'),
404
+ col.references && m('span.text-2xs.font-bold.px-2.py-0.5.rounded.bg-cyan-100.dark:bg-cyan-900/60.text-cyan-800.dark:text-cyan-200', 'FK ➔ ' + col.references),
405
+ !isPk && !col.unique && !col.index && !col.references && !col.autoIncrement && m('span.text-xs.text-gray-400', '—')
406
+ ])
407
+ ]),
408
+
409
+ // Default
410
+ m('td.px-4.py-3.font-mono.text-xs.text-gray-600.dark:text-slate-400',
411
+ col.default !== undefined && col.default !== null ? String(col.default) : '—'
412
+ )
413
+ ]);
414
+ }))
415
+ ])
416
+ ])
417
+ ]),
418
+
419
+ // Relationships Card
420
+ renderModelRelationsSection(vnode, selectedModel, self)
421
+ ]
422
+ ])
423
+ ]);
424
+ }
425
+
426
+ function renderTypeBadge(col) {
427
+ var type = col.type || 'string';
428
+ var label = type;
429
+ if (col.maxLength) label += '(' + col.maxLength + ')';
430
+ else if (col.precision) label += '(' + col.precision + (col.scale ? ',' + col.scale : '') + ')';
431
+ else if (col.enumValues && col.enumValues.length) label += '[' + col.enumValues.slice(0, 3).join(',') + '…]';
432
+
433
+ var bg = 'bg-slate-100 text-slate-800 dark:bg-slate-700 dark:text-slate-200';
434
+ if (type === 'integer' || type === 'bigint' || type === 'float' || type === 'decimal') {
435
+ bg = 'bg-blue-50 text-blue-700 dark:bg-blue-950/60 dark:text-blue-300 border border-blue-200 dark:border-blue-800';
436
+ } else if (type === 'string' || type === 'text') {
437
+ bg = 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/60 dark:text-emerald-300 border border-emerald-200 dark:border-emerald-800';
438
+ } else if (type === 'boolean') {
439
+ bg = 'bg-amber-50 text-amber-700 dark:bg-amber-950/60 dark:text-amber-300 border border-amber-200 dark:border-amber-800';
440
+ } else if (type === 'timestamp' || type === 'date') {
441
+ bg = 'bg-purple-50 text-purple-700 dark:bg-purple-950/60 dark:text-purple-300 border border-purple-200 dark:border-purple-800';
442
+ } else if (type === 'json' || type === 'jsonb') {
443
+ bg = 'bg-pink-50 text-pink-700 dark:bg-pink-950/60 dark:text-pink-300 border border-pink-200 dark:border-pink-800';
444
+ }
445
+
446
+ return m('span.px-2.py-0.5.rounded-md.font-mono.text-xs.font-medium', { class: bg }, label);
447
+ }
448
+
449
+ function renderModelRelationsSection(vnode, selectedModel, self) {
450
+ var outgoing = selectedModel.outgoingRelations || [];
451
+ var incoming = selectedModel.incomingRelations || [];
452
+
453
+ return m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.p-6.space-y-6', [
454
+ m('div.flex.items-center.justify-between.border-b.border-gray-200.dark:border-slate-700.pb-4', [
455
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'Model Relationships'),
456
+ m('span.text-xs.text-gray-500.dark:text-slate-400', (outgoing.length + incoming.length) + ' total connections')
457
+ ]),
458
+
459
+ outgoing.length === 0 && incoming.length === 0 && m('p.text-sm.text-gray-500.dark:text-slate-400.py-4',
460
+ 'No relationships defined or detected for this model.'
461
+ ),
462
+
463
+ // Outgoing Relations
464
+ outgoing.length > 0 && m('div.space-y-3', [
465
+ m('h4.text-xs.font-bold.uppercase.tracking-wider.text-indigo-600.dark:text-indigo-400',
466
+ '↗ Outgoing Relations (' + selectedModel.name + ' references)'
467
+ ),
468
+ m('div.grid.grid-cols-1.md:grid-cols-2.gap-3', outgoing.map(function(rel) {
469
+ return m('div.p-4.rounded-xl.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900/40.space-y-2', [
470
+ m('div.flex.items-center.justify-between', [
471
+ m('span.font-bold.text-sm.text-gray-900.dark:text-slate-100', rel.name),
472
+ renderRelationTypeBadge(rel.type),
473
+ ]),
474
+ m('div.text-xs.text-gray-600.dark:text-slate-300.font-mono', [
475
+ m('span.text-gray-500', rel.foreignKey || 'id'),
476
+ m('span.mx-1.5.text-indigo-500', '➔'),
477
+ m('span.font-semibold', (rel.targetModel || rel.targetTable) + '.' + (rel.localKey || 'id')),
478
+ ]),
479
+ rel.targetModel && m('button.text-xs.text-indigo-600.dark:text-indigo-400.font-semibold.hover:underline.pt-1', {
480
+ onclick: function() { self.selectModel(vnode, rel.targetModel); }
481
+ }, 'Go to Table: ' + rel.targetModel + ' ➔')
482
+ ]);
483
+ }))
484
+ ]),
485
+
486
+ // Incoming Relations
487
+ incoming.length > 0 && m('div.space-y-3.pt-2', [
488
+ m('h4.text-xs.font-bold.uppercase.tracking-wider.text-emerald-600.dark:text-emerald-400',
489
+ '↙ Incoming Relations (Other models referencing ' + selectedModel.name + ')'
490
+ ),
491
+ m('div.grid.grid-cols-1.md:grid-cols-2.gap-3', incoming.map(function(rel) {
492
+ return m('div.p-4.rounded-xl.border.border-gray-200.dark:border-slate-700.bg-gray-50.dark:bg-slate-900/40.space-y-2', [
493
+ m('div.flex.items-center.justify-between', [
494
+ m('span.font-bold.text-sm.text-gray-900.dark:text-slate-100', rel.sourceModel),
495
+ renderRelationTypeBadge(rel.type),
496
+ ]),
497
+ m('div.text-xs.text-gray-600.dark:text-slate-300.font-mono', [
498
+ m('span.font-semibold', rel.sourceTable + '.' + (rel.foreignKey || 'id')),
499
+ m('span.mx-1.5.text-emerald-500', '➔'),
500
+ m('span.text-gray-500', selectedModel.table + '.' + (rel.localKey || 'id')),
501
+ ]),
502
+ m('button.text-xs.text-emerald-600.dark:text-emerald-400.font-semibold.hover:underline.pt-1', {
503
+ onclick: function() { self.selectModel(vnode, rel.sourceModel); }
504
+ }, 'Go to Table: ' + rel.sourceModel + ' ➔')
505
+ ]);
506
+ }))
507
+ ])
508
+ ]);
509
+ }
510
+
511
+ function renderRelationTypeBadge(type) {
512
+ var t = type || 'belongsTo';
513
+ var bg = 'bg-indigo-100 text-indigo-800 dark:bg-indigo-950/60 dark:text-indigo-300';
514
+ if (t === 'hasMany') bg = 'bg-emerald-100 text-emerald-800 dark:bg-emerald-950/60 dark:text-emerald-300';
515
+ else if (t === 'hasOne') bg = 'bg-purple-100 text-purple-800 dark:bg-purple-950/60 dark:text-purple-300';
516
+ else if (t === 'manyToMany') bg = 'bg-amber-100 text-amber-800 dark:bg-amber-950/60 dark:text-amber-300';
517
+
518
+ return m('span.px-2.py-0.5.rounded-md.text-2xs.font-bold.uppercase.tracking-wide', { class: bg }, t);
519
+ }
520
+
521
+ /** TAB 2: Visual Relationship Map (ER) */
522
+ function renderGraphTab(vnode, self) {
523
+ var s = vnode.state;
524
+ var sel = s.selectedGraphModel;
525
+
526
+ return m('div.space-y-6', [
527
+ // Info Banner
528
+ m('div.p-4.rounded-xl.bg-indigo-50.dark:bg-indigo-950/30.border.border-indigo-100.dark:border-indigo-900.flex.items-center.justify-between', [
529
+ m('div.text-xs.text-indigo-900.dark:text-indigo-300', [
530
+ m('span.font-bold', '💡 Tip: '),
531
+ 'Click on any table card to highlight all its direct relationships.'
532
+ ]),
533
+ sel && m('button.text-xs.font-bold.text-indigo-600.dark:text-indigo-400.hover:underline', {
534
+ onclick: function() { s.selectedGraphModel = null; }
535
+ }, 'Clear Selection ✕')
536
+ ]),
537
+
538
+ // Visual Cards Grid
539
+ m('div.grid.grid-cols-1.md:grid-cols-2.xl:grid-cols-3.gap-5', s.models.map(function(model) {
540
+ var isDirectlySelected = sel === model.name;
541
+ var isConnected = false;
542
+ if (sel) {
543
+ var activeModel = s.models.find(function(m) { return m.name === sel; });
544
+ if (activeModel && activeModel.connectedModels && activeModel.connectedModels.indexOf(model.name) !== -1) {
545
+ isConnected = true;
546
+ }
547
+ }
548
+
549
+ var cardBorder = 'border-gray-200 dark:border-slate-700';
550
+ var cardOpacity = 'opacity-100';
551
+ if (sel) {
552
+ if (isDirectlySelected) {
553
+ cardBorder = 'border-indigo-500 ring-2 ring-indigo-500 shadow-md';
554
+ } else if (isConnected) {
555
+ cardBorder = 'border-emerald-500 ring-2 ring-emerald-400/60 shadow-sm';
556
+ } else {
557
+ cardOpacity = 'opacity-40 hover:opacity-100';
558
+ }
559
+ }
560
+
561
+ var outRels = model.outgoingRelations || [];
562
+ var inRels = model.incomingRelations || [];
563
+
564
+ return m('div.rounded-2xl.border.bg-white.dark:bg-slate-800.shadow-sm.p-5.transition-all.cursor-pointer', {
565
+ class: cardBorder + ' ' + cardOpacity,
566
+ onclick: function() {
567
+ s.selectedGraphModel = (s.selectedGraphModel === model.name) ? null : model.name;
568
+ }
569
+ }, [
570
+ // Card Header
571
+ m('div.flex.items-center.justify-between.border-b.border-gray-100.dark:border-slate-700/80.pb-3.mb-3', [
572
+ m('div', [
573
+ m('div.font-extrabold.text-base.text-gray-900.dark:text-slate-100', model.name),
574
+ m('div.text-xs.font-mono.text-gray-500.dark:text-slate-400', model.table),
575
+ ]),
576
+ m('span.text-xs.font-bold.px-2.py-0.5.rounded.bg-amber-100.dark:bg-amber-950/60.text-amber-800.dark:text-amber-200',
577
+ 'PK: ' + (model.primaryKey || 'id')
578
+ )
579
+ ]),
580
+
581
+ // Column preview (top 6 columns)
582
+ m('div.space-y-1.text-xs.font-mono.py-1', (model.columns || []).slice(0, 6).map(function(c) {
583
+ var isFk = c.references || outRels.some(function(r) { return r.foreignKey === c.name; });
584
+ var isPk = c.primary || c.name === model.primaryKey;
585
+ return m('div.flex.items-center.justify-between.py-0.5.px-1.5.rounded', {
586
+ class: isPk ? 'bg-amber-50/50 dark:bg-amber-950/20' : (isFk ? 'bg-indigo-50/50 dark:bg-indigo-950/20' : '')
587
+ }, [
588
+ m('span.truncate', [
589
+ isPk ? '🔑 ' : (isFk ? '🔗 ' : '• '),
590
+ c.name
591
+ ]),
592
+ m('span.text-gray-400.text-2xs', c.type)
593
+ ]);
594
+ })),
595
+
596
+ (model.columns || []).length > 6 && m('div.text-2xs.text-gray-400.text-center.pt-1',
597
+ '+ ' + ((model.columns || []).length - 6) + ' more columns…'
598
+ ),
599
+
600
+ // Relations footer
601
+ m('div.mt-4.pt-3.border-t.border-gray-100.dark:border-slate-700/80.flex.flex-col.gap-2', [
602
+ m('div.flex.items-center.justify-between.text-xs', [
603
+ m('span.text-gray-500.dark:text-slate-400', 'Relations:'),
604
+ m('span.font-semibold.text-indigo-600.dark:text-indigo-400', (outRels.length + inRels.length) + ' connections')
605
+ ]),
606
+
607
+ m('div.flex.flex-wrap.gap-1', [
608
+ outRels.map(function(r) {
609
+ return m('span.text-2xs.font-semibold.px-2.py-0.5.rounded.bg-indigo-50.dark:bg-indigo-950/60.text-indigo-700.dark:text-indigo-300',
610
+ '➔ ' + (r.targetModel || r.targetTable)
611
+ );
612
+ }),
613
+ inRels.map(function(r) {
614
+ return m('span.text-2xs.font-semibold.px-2.py-0.5.rounded.bg-emerald-50.dark:bg-emerald-950/60.text-emerald-700.dark:text-emerald-300',
615
+ '⬅ ' + r.sourceModel
616
+ );
617
+ }),
618
+ ]),
619
+
620
+ m('button.mt-1.w-full.py-1.5.rounded-lg.bg-gray-100.dark:bg-slate-700.hover:bg-indigo-50.dark:hover:bg-indigo-950/40.text-gray-700.dark:text-slate-200.hover:text-indigo-600.text-xs.font-semibold.transition-colors', {
621
+ onclick: function(e) {
622
+ e.stopPropagation();
623
+ self.selectModel(vnode, model.name);
624
+ }
625
+ }, 'View Details ➔')
626
+ ])
627
+ ]);
628
+ })),
629
+
630
+ // Full Relationships Matrix / Table
631
+ m('div.bg-white.dark:bg-slate-800.rounded-xl.shadow-sm.border.border-gray-200.dark:border-slate-700.overflow-hidden', [
632
+ m('div.px-6.py-4.border-b.border-gray-200.dark:border-slate-700', [
633
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', 'All Relationships Matrix'),
634
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-0.5', 'All model connections and foreign key mappings detected in database.')
635
+ ]),
636
+
637
+ m('div.overflow-x-auto', [
638
+ m('table.min-w-full.divide-y.divide-gray-200.dark:divide-slate-700.text-left.text-sm', [
639
+ m('thead.bg-gray-50.dark:bg-slate-900/60', [
640
+ m('tr', [
641
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Source Model / Table'),
642
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Relation'),
643
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Type'),
644
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Target Model / Table'),
645
+ m('th.px-4.py-3.text-xs.font-semibold.text-gray-500.dark:text-slate-400.uppercase', 'Key Mapping'),
646
+ ])
647
+ ]),
648
+ m('tbody.divide-y.divide-gray-100.dark:divide-slate-700/60', s.relationships.map(function(r) {
649
+ return m('tr.hover:bg-gray-50.dark:hover:bg-slate-700/30.transition-colors', [
650
+ m('td.px-4.py-3', [
651
+ m('button.font-bold.text-indigo-600.dark:text-indigo-400.hover:underline', {
652
+ onclick: function() { self.selectModel(vnode, r.sourceModel); }
653
+ }, r.sourceModel),
654
+ m('span.text-xs.font-mono.text-gray-400.ml-1.5', '(' + r.sourceTable + ')')
655
+ ]),
656
+ m('td.px-4.py-3.font-mono.text-xs.font-semibold', r.name),
657
+ m('td.px-4.py-3', renderRelationTypeBadge(r.type)),
658
+ m('td.px-4.py-3', [
659
+ r.targetModel ? m('button.font-bold.text-emerald-600.dark:text-emerald-400.hover:underline', {
660
+ onclick: function() { self.selectModel(vnode, r.targetModel); }
661
+ }, r.targetModel) : m('span.text-gray-500', r.targetTable || '—'),
662
+ r.targetTable && m('span.text-xs.font-mono.text-gray-400.ml-1.5', '(' + r.targetTable + ')')
663
+ ]),
664
+ m('td.px-4.py-3.font-mono.text-xs.text-gray-600.dark:text-slate-300', [
665
+ m('span', r.sourceTable + '.' + (r.foreignKey || 'id')),
666
+ m('span.text-indigo-500.mx-1', '➔'),
667
+ m('span', (r.targetTable || r.targetModel) + '.' + (r.localKey || 'id')),
668
+ ])
669
+ ]);
670
+ }))
671
+ ])
672
+ ])
673
+ ])
674
+ ]);
675
+ }
676
+
677
+ /** TAB 3: Mermaid ER Diagram Code */
678
+ function renderMermaidTab(vnode, self) {
679
+ var s = vnode.state;
680
+
681
+ return m('div.space-y-4', [
682
+ m('div.bg-slate-900.text-white.p-6.rounded-2xl.border.border-slate-800.shadow-md.space-y-4', [
683
+ m('div.flex.items-center.justify-between', [
684
+ m('div', [
685
+ m('h3.text-lg.font-bold', 'Mermaid.js Entity-Relationship Diagram'),
686
+ m('p.text-xs.text-slate-400.mt-1',
687
+ 'Copy and paste this code block into GitHub READMEs, Markdown documentation, or the mermaid.live editor.'
688
+ ),
689
+ ]),
690
+ m('button.px-4.py-2.rounded-lg.bg-indigo-600.hover:bg-indigo-500.text-white.text-xs.font-bold.transition-colors.shadow-sm', {
691
+ onclick: function() { self.copyMermaid(vnode); }
692
+ }, [
693
+ m('span.mr-1.5', s.copiedMermaid ? '✓' : '📋'),
694
+ s.copiedMermaid ? 'Copied!' : 'Copy Code'
695
+ ])
696
+ ]),
697
+
698
+ m('div.bg-slate-950.rounded-xl.p-4.border.border-slate-800.overflow-x-auto', [
699
+ m('pre.font-mono.text-xs.text-emerald-400.leading-relaxed', s.mermaid || '// No schema data available yet')
700
+ ])
701
+ ])
702
+ ]);
703
+ }
704
+
705
+ window.__customPages = window.__customPages || {};
706
+ window.__customPages['schema-explorer'] = SchemaExplorerPage;
707
+
708
+ })();
709
+ `;
710
+ }
711
+
712
+ module.exports = {
713
+ generateSchemaExplorerAdminComponent,
714
+ };