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
@@ -9,6 +9,50 @@ module.exports = components + `
9
9
  // Set route prefix to admin path
10
10
  m.route.prefix = window.__ADMIN_PATH__ || '/_admin';
11
11
 
12
+ // Title formatting and synchronization helper
13
+ function formatAdminTitle(pageTitle, baseTitle, template) {
14
+ baseTitle = baseTitle || 'Admin Panel';
15
+ if (!pageTitle || pageTitle === baseTitle) {
16
+ return baseTitle;
17
+ }
18
+ if (typeof template === 'function') {
19
+ return template(pageTitle, baseTitle);
20
+ }
21
+ if (typeof template === 'string' && template.trim()) {
22
+ var formatted = template;
23
+ if (formatted.indexOf('%s') !== -1) {
24
+ formatted = formatted.replace(/%s/g, pageTitle);
25
+ }
26
+ if (formatted.indexOf('%b') !== -1) {
27
+ formatted = formatted.replace(/%b/g, baseTitle);
28
+ }
29
+ return formatted;
30
+ }
31
+ return pageTitle + ' | ' + baseTitle;
32
+ }
33
+
34
+ function setAdminPageTitle(pageTitle, options) {
35
+ try {
36
+ var config = window.__ADMIN_CONFIG__ || {};
37
+ var settings = config.settings || {};
38
+ var baseTitle = settings.title || 'Admin Panel';
39
+ var template = (options && options.template) || settings.titleTemplate || '%s | %b';
40
+
41
+ if (options && options.raw) {
42
+ document.title = pageTitle || baseTitle;
43
+ return;
44
+ }
45
+
46
+ document.title = formatAdminTitle(pageTitle, baseTitle, template);
47
+ } catch (e) {
48
+ if (pageTitle) {
49
+ document.title = pageTitle;
50
+ }
51
+ }
52
+ }
53
+ window.formatAdminTitle = formatAdminTitle;
54
+ window.setAdminPageTitle = setAdminPageTitle;
55
+
12
56
  // Helper to check auth status
13
57
  async function checkAuth() {
14
58
  if (state.user) return true;
@@ -45,7 +89,7 @@ function getUserManagementModel() {
45
89
  async function guardUserManagementRoutes() {
46
90
  var isAuth = await checkAuth();
47
91
  if (!isAuth) {
48
- redirectToLogin();
92
+ redirectToLogin('expired');
49
93
  return false;
50
94
  }
51
95
  if (!getUserManagementModel()) {
@@ -98,13 +142,20 @@ const UserSessionsPage = {
98
142
  m('td.px-3.py-2.text-gray-700.dark:text-slate-300', r.created_at ? new Date(r.created_at).toLocaleString() : ''),
99
143
  m('td.px-3.py-2', m('button.text-red-600.dark:text-red-400.text-xs', {
100
144
  onclick: async function () {
101
- if (!confirm('Revoke this session?')) return;
145
+ var ok = await confirmAction({
146
+ title: 'Revoke Session',
147
+ message: 'Revoke this session?',
148
+ danger: true,
149
+ confirmText: 'Revoke'
150
+ });
151
+ if (!ok) return;
102
152
  try {
103
153
  await api.delete('/users/sessions/' + encodeURIComponent(r.token));
104
154
  vnode.state.rows = vnode.state.rows.filter(function (x) { return x.token !== r.token; });
155
+ notifySuccess('Session revoked successfully');
105
156
  m.redraw();
106
157
  } catch (err) {
107
- alert(err.message || err);
158
+ notifyError(err.message || String(err));
108
159
  }
109
160
  },
110
161
  }, 'Revoke')),
@@ -122,12 +173,15 @@ var routes = {
122
173
  onmatch: async () => {
123
174
  const needsSetup = await checkSetupNeeded();
124
175
  if (needsSetup) {
176
+ setAdminPageTitle('Setup');
125
177
  return SetupForm;
126
178
  }
127
179
  const isAuth = await checkAuth();
128
180
  if (!isAuth) {
181
+ setAdminPageTitle('Sign In');
129
182
  return LoginForm;
130
183
  }
184
+ setAdminPageTitle('Dashboard');
131
185
  return Dashboard;
132
186
  }
133
187
  },
@@ -138,6 +192,7 @@ var routes = {
138
192
  m.route.set(consumeIntendedRoute('/'));
139
193
  return;
140
194
  }
195
+ setAdminPageTitle('Sign In');
141
196
  return LoginForm;
142
197
  }
143
198
  },
@@ -148,6 +203,7 @@ var routes = {
148
203
  m.route.set('/login');
149
204
  return;
150
205
  }
206
+ setAdminPageTitle('Setup');
151
207
  return SetupForm;
152
208
  }
153
209
  },
@@ -155,9 +211,10 @@ var routes = {
155
211
  onmatch: async () => {
156
212
  const isAuth = await checkAuth();
157
213
  if (!isAuth) {
158
- redirectToLogin();
214
+ redirectToLogin('expired');
159
215
  return;
160
216
  }
217
+ setAdminPageTitle('Settings');
161
218
  return SettingsPage;
162
219
  }
163
220
  },
@@ -165,9 +222,10 @@ var routes = {
165
222
  onmatch: async () => {
166
223
  const isAuth = await checkAuth();
167
224
  if (!isAuth) {
168
- redirectToLogin();
225
+ redirectToLogin('expired');
169
226
  return;
170
227
  }
228
+ setAdminPageTitle('Profile');
171
229
  return ProfilePage;
172
230
  }
173
231
  },
@@ -175,15 +233,17 @@ var routes = {
175
233
  onmatch: async () => {
176
234
  const isAuth = await checkAuth();
177
235
  if (!isAuth) {
178
- redirectToLogin();
236
+ redirectToLogin('expired');
179
237
  return;
180
238
  }
239
+ setAdminPageTitle('Staff Users');
181
240
  return AdminUsersPage;
182
241
  }
183
242
  },
184
243
  '/users/sessions': {
185
244
  onmatch: async () => {
186
245
  if (!(await guardUserManagementRoutes())) return;
246
+ setAdminPageTitle('Active Sessions');
187
247
  return UserSessionsPage;
188
248
  }
189
249
  },
@@ -213,9 +273,11 @@ var routes = {
213
273
  onmatch: async () => {
214
274
  const isAuth = await checkAuth();
215
275
  if (!isAuth) {
216
- redirectToLogin();
276
+ redirectToLogin('expired');
217
277
  return;
218
278
  }
279
+ const model = m.route.param('model');
280
+ setAdminPageTitle(model);
219
281
  return RecordList;
220
282
  }
221
283
  },
@@ -223,9 +285,11 @@ var routes = {
223
285
  onmatch: async () => {
224
286
  const isAuth = await checkAuth();
225
287
  if (!isAuth) {
226
- redirectToLogin();
288
+ redirectToLogin('expired');
227
289
  return;
228
290
  }
291
+ const model = m.route.param('model');
292
+ setAdminPageTitle('New ' + model);
229
293
  return RecordForm;
230
294
  }
231
295
  },
@@ -233,9 +297,11 @@ var routes = {
233
297
  onmatch: async () => {
234
298
  const isAuth = await checkAuth();
235
299
  if (!isAuth) {
236
- redirectToLogin();
300
+ redirectToLogin('expired');
237
301
  return;
238
302
  }
303
+ const model = m.route.param('model');
304
+ setAdminPageTitle('Edit ' + model);
239
305
  return RecordForm;
240
306
  }
241
307
  },
@@ -243,14 +309,21 @@ var routes = {
243
309
 
244
310
  function wrapCustomPageWithLayout(page, rawComp) {
245
311
  if (!rawComp) return createCustomPage(page);
246
- if (page.layout === false) return rawComp;
247
312
 
248
313
  return {
249
314
  oninit: function(vnode) {
315
+ if (page) {
316
+ if (page.documentTitle) {
317
+ document.title = page.documentTitle;
318
+ } else if (page.title || page.id) {
319
+ setAdminPageTitle(page.title || page.id);
320
+ }
321
+ }
250
322
  try {
251
- vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
252
- if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
253
- return vnode.state.instance.oninit.call(this, vnode);
323
+ const instance = typeof rawComp === 'function' ? rawComp.call(rawComp, vnode) : rawComp;
324
+ vnode.state.instance = instance;
325
+ if (instance && typeof instance.oninit === 'function') {
326
+ return instance.oninit.call(instance, vnode);
254
327
  }
255
328
  } catch (err) {
256
329
  console.error('[Admin Panel] Error in oninit for custom page "' + page.id + '":', err);
@@ -260,7 +333,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
260
333
  oncreate: function(vnode) {
261
334
  try {
262
335
  const comp = vnode.state.instance || rawComp;
263
- if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
336
+ if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(comp, vnode);
264
337
  } catch (err) {
265
338
  console.error('[Admin Panel] Error in oncreate for custom page "' + page.id + '":', err);
266
339
  }
@@ -268,7 +341,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
268
341
  onupdate: function(vnode) {
269
342
  try {
270
343
  const comp = vnode.state.instance || rawComp;
271
- if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
344
+ if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(comp, vnode);
272
345
  } catch (err) {
273
346
  console.error('[Admin Panel] Error in onupdate for custom page "' + page.id + '":', err);
274
347
  }
@@ -276,7 +349,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
276
349
  onbeforeremove: function(vnode) {
277
350
  try {
278
351
  const comp = vnode.state.instance || rawComp;
279
- if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
352
+ if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(comp, vnode);
280
353
  } catch (err) {
281
354
  console.error('[Admin Panel] Error in onbeforeremove for custom page "' + page.id + '":', err);
282
355
  }
@@ -284,7 +357,7 @@ function wrapCustomPageWithLayout(page, rawComp) {
284
357
  onremove: function(vnode) {
285
358
  try {
286
359
  const comp = vnode.state.instance || rawComp;
287
- if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
360
+ if (comp && typeof comp.onremove === 'function') return comp.onremove.call(comp, vnode);
288
361
  } catch (err) {
289
362
  console.error('[Admin Panel] Error in onremove for custom page "' + page.id + '":', err);
290
363
  }
@@ -292,32 +365,47 @@ function wrapCustomPageWithLayout(page, rawComp) {
292
365
  view: function(vnode) {
293
366
  if (vnode.state.renderError) {
294
367
  var err = vnode.state.renderError;
295
- var breadcrumbs = [{ label: page.title, href: page.path }];
296
- return m(Layout, [
297
- m(Breadcrumb, { items: breadcrumbs }),
298
- m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
299
- m('h3.text-lg.font-semibold.text-red-800', 'Sayfa Yüklenirken Hata Oluştu'),
300
- m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
301
- ])
302
- ]);
368
+ return typeof renderCustomPageError === 'function'
369
+ ? renderCustomPageError(page, err)
370
+ : m(Layout, [
371
+ m(Breadcrumb, { items: [{ label: page.title, href: page.path }] }),
372
+ m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
373
+ m('h3.text-lg.font-semibold.text-red-800', 'Custom Page Component Error'),
374
+ m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
375
+ ])
376
+ ]);
303
377
  }
304
378
  const comp = vnode.state.instance || rawComp;
305
379
  var content;
306
380
  try {
307
381
  if (comp && typeof comp.view === 'function') {
308
- content = comp.view.call(this, vnode);
382
+ content = comp.view.call(comp, vnode);
309
383
  } else if (typeof comp === 'function') {
310
- content = comp.call(this, vnode);
384
+ content = comp.call(comp, vnode);
311
385
  } else {
312
386
  content = comp;
313
387
  }
314
388
  } catch (err) {
315
389
  console.error('[Admin Panel] Error rendering custom page "' + page.id + '":', err);
316
- content = m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
317
- m('h3.text-lg.font-semibold.text-red-800', 'Sayfa Çizilirken Hata Oluştu'),
318
- m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
390
+ return typeof renderCustomPageError === 'function'
391
+ ? renderCustomPageError(page, err)
392
+ : m(Layout, [
393
+ m(Breadcrumb, { items: [{ label: page.title, href: page.path }] }),
394
+ m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
395
+ m('h3.text-lg.font-semibold.text-red-800', 'Custom Page Component Error'),
396
+ m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
397
+ ])
398
+ ]);
399
+ }
400
+
401
+ if (page.layout === false) {
402
+ return m('div.custom-canvas-page', [
403
+ content,
404
+ typeof ToastContainer !== 'undefined' ? m(ToastContainer) : null,
405
+ typeof ConfirmModalContainer !== 'undefined' ? m(ConfirmModalContainer) : null,
319
406
  ]);
320
407
  }
408
+
321
409
  if (content && (content.tag === Layout || (content.tag && content.tag.name === 'Layout'))) {
322
410
  return content;
323
411
  }
@@ -339,9 +427,14 @@ if (config && config.pages) {
339
427
  onmatch: async () => {
340
428
  const isAuth = await checkAuth();
341
429
  if (!isAuth) {
342
- redirectToLogin();
430
+ redirectToLogin('expired');
343
431
  return;
344
432
  }
433
+ if (page.documentTitle) {
434
+ document.title = page.documentTitle;
435
+ } else {
436
+ setAdminPageTitle(page.title || page.id);
437
+ }
345
438
  const comp = (window.__customPages && window.__customPages[page.id])
346
439
  ? wrapCustomPageWithLayout(page, window.__customPages[page.id])
347
440
  : createCustomPage(page);
@@ -16,11 +16,12 @@ const api = {
16
16
  if (!response.ok) {
17
17
  const error = await response.json().catch(() => ({ error: 'Request failed' }));
18
18
  if (response.status === 401 && path.indexOf('/auth/') !== 0) {
19
- redirectToLogin();
19
+ redirectToLogin('expired');
20
20
  }
21
- const err = new Error(error.error || 'Request failed');
21
+ const err = new Error(error.error || error.message || 'Request failed');
22
22
  if (error.fields) err.fields = error.fields;
23
23
  err.response = error;
24
+ err.status = response.status;
24
25
  throw err;
25
26
  }
26
27
 
@@ -90,6 +91,8 @@ const state = {
90
91
  needsSetup: false,
91
92
  loading: false,
92
93
  error: null,
94
+ notice: null,
95
+ toasts: [],
93
96
  models: [],
94
97
  currentModel: null,
95
98
  currentModelMeta: null, // Full model metadata with columns
@@ -118,14 +121,19 @@ var INTENDED_ROUTE_KEY = 'webspresso.admin.intendedRoute';
118
121
 
119
122
  function isSafeIntendedRoute(path) {
120
123
  if (!path || typeof path !== 'string') return false;
121
- if (path.charAt(0) !== '/') return false;
122
- if (path === '/login' || path === '/setup') return false;
124
+ var p = path.trim();
125
+ if (p.charAt(0) !== '/' || p.indexOf('//') === 0 || p.indexOf('/\\') === 0) return false;
126
+ if (p === '/login' || p === '/setup' || p.indexOf('/login?') === 0 || p.indexOf('/setup?') === 0) return false;
127
+ if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(p)) return false;
123
128
  return true;
124
129
  }
125
130
 
126
- function saveIntendedRoute() {
131
+ function saveIntendedRoute(explicitPath) {
127
132
  try {
128
- var path = m.route.get();
133
+ var path = explicitPath;
134
+ if (!path && typeof m !== 'undefined' && m.route && typeof m.route.get === 'function') {
135
+ path = m.route.get();
136
+ }
129
137
  if (!isSafeIntendedRoute(path)) return;
130
138
  sessionStorage.setItem(INTENDED_ROUTE_KEY, path);
131
139
  } catch (e) {}
@@ -139,17 +147,314 @@ function clearIntendedRoute() {
139
147
 
140
148
  function consumeIntendedRoute(defaultPath) {
141
149
  defaultPath = defaultPath || '/';
150
+
151
+ // 1. Try route parameter or query string returnTo
152
+ try {
153
+ if (typeof m !== 'undefined' && m.route && typeof m.route.param === 'function') {
154
+ var param = m.route.param('returnTo');
155
+ if (param && isSafeIntendedRoute(param)) {
156
+ clearIntendedRoute();
157
+ return param;
158
+ }
159
+ }
160
+ } catch (e) {}
161
+
162
+ try {
163
+ if (typeof window !== 'undefined' && window.location && window.location.search) {
164
+ var match = window.location.search.match(/[?&]returnTo=([^&]+)/);
165
+ if (match && match[1]) {
166
+ var decoded = decodeURIComponent(match[1]);
167
+ if (isSafeIntendedRoute(decoded)) {
168
+ clearIntendedRoute();
169
+ return decoded;
170
+ }
171
+ }
172
+ }
173
+ } catch (e) {}
174
+
175
+ // 2. Fall back to sessionStorage
142
176
  try {
143
177
  var path = sessionStorage.getItem(INTENDED_ROUTE_KEY);
144
178
  sessionStorage.removeItem(INTENDED_ROUTE_KEY);
145
179
  if (isSafeIntendedRoute(path)) return path;
146
180
  } catch (e) {}
181
+
147
182
  return defaultPath;
148
183
  }
149
184
 
150
- function redirectToLogin() {
151
- saveIntendedRoute();
152
- m.route.set('/login');
185
+ function redirectToLogin(reason) {
186
+ var currentRoute = '';
187
+ try {
188
+ if (typeof m !== 'undefined' && m.route && typeof m.route.get === 'function') {
189
+ currentRoute = m.route.get();
190
+ }
191
+ } catch (e) {}
192
+
193
+ if (isSafeIntendedRoute(currentRoute)) {
194
+ saveIntendedRoute(currentRoute);
195
+ }
196
+
197
+ var params = [];
198
+ if (isSafeIntendedRoute(currentRoute)) {
199
+ params.push('returnTo=' + encodeURIComponent(currentRoute));
200
+ }
201
+ if (reason) {
202
+ params.push('reason=' + encodeURIComponent(reason));
203
+ }
204
+
205
+ var target = '/login' + (params.length > 0 ? '?' + params.join('&') : '');
206
+ try {
207
+ if (typeof m !== 'undefined' && m.route && typeof m.route.set === 'function') {
208
+ m.route.set(target);
209
+ }
210
+ } catch (e) {}
211
+ }
212
+
213
+ // ==========================================
214
+ // Toast Notification & Confirm Modal System
215
+ // ==========================================
216
+ var toastState = (typeof window !== 'undefined' && window.toastState) ? window.toastState : {
217
+ items: [],
218
+ add: function(type, message, options) {
219
+ options = options || {};
220
+ if (typeof type === 'object') {
221
+ options = type;
222
+ type = options.type || 'info';
223
+ message = options.message || '';
224
+ }
225
+ var id = 'toast_' + Math.random().toString(36).substring(2, 9);
226
+ var duration = options.duration !== undefined ? options.duration : 4000;
227
+ var item = {
228
+ id: id,
229
+ type: type || 'info',
230
+ message: message,
231
+ title: options.title || '',
232
+ duration: duration,
233
+ };
234
+ toastState.items.push(item);
235
+ if (typeof state !== 'undefined') {
236
+ state.toasts = toastState.items;
237
+ }
238
+ if (item.duration > 0) {
239
+ setTimeout(function() {
240
+ toastState.remove(id);
241
+ }, item.duration);
242
+ }
243
+ try {
244
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
245
+ m.redraw();
246
+ }
247
+ } catch (e) {}
248
+ return id;
249
+ },
250
+ remove: function(id) {
251
+ toastState.items = toastState.items.filter(function(t) { return t.id !== id; });
252
+ if (typeof state !== 'undefined') {
253
+ state.toasts = toastState.items;
254
+ }
255
+ try {
256
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
257
+ m.redraw();
258
+ }
259
+ } catch (e) {}
260
+ },
261
+ clear: function() {
262
+ toastState.items = [];
263
+ if (typeof state !== 'undefined') {
264
+ state.toasts = [];
265
+ }
266
+ try {
267
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
268
+ m.redraw();
269
+ }
270
+ } catch (e) {}
271
+ },
272
+ };
273
+
274
+ function normalizeToastOptions(options) {
275
+ if (typeof options === 'number') {
276
+ return { duration: options };
277
+ }
278
+ return options || {};
279
+ }
280
+
281
+ var notify = function(message, type, options) {
282
+ var opts = normalizeToastOptions(options);
283
+ if (typeof type === 'object') {
284
+ opts = type;
285
+ type = opts.type || 'info';
286
+ }
287
+ return toastState.add(type || 'info', message, opts);
288
+ };
289
+ notify.success = function(message, options) { return toastState.add('success', message, normalizeToastOptions(options)); };
290
+ notify.error = function(message, options) { return toastState.add('error', message, normalizeToastOptions(options)); };
291
+ notify.warning = function(message, options) { return toastState.add('warning', message, normalizeToastOptions(options)); };
292
+ notify.info = function(message, options) { return toastState.add('info', message, normalizeToastOptions(options)); };
293
+ notify.remove = function(id) { return toastState.remove(id); };
294
+ notify.clear = function() { return toastState.clear(); };
295
+
296
+ var ToastContainer = {
297
+ view: function() {
298
+ var items = (typeof toastState !== 'undefined' && toastState.items) || (typeof state !== 'undefined' && state.toasts) || [];
299
+ if (!items || items.length === 0) return null;
300
+ return m('div.fixed.top-4.right-4.z-50.flex.flex-col.gap-2.5.max-w-sm.w-full.pointer-events-none.px-4.sm:px-0',
301
+ items.map(function(t) {
302
+ var typeConfig = {
303
+ success: {
304
+ border: 'border-emerald-200 dark:border-emerald-800/80',
305
+ bg: 'bg-white dark:bg-slate-900',
306
+ iconColor: 'text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950/60',
307
+ symbol: '✓',
308
+ },
309
+ error: {
310
+ border: 'border-red-200 dark:border-red-800/80',
311
+ bg: 'bg-white dark:bg-slate-900',
312
+ iconColor: 'text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/60',
313
+ symbol: '!',
314
+ },
315
+ warning: {
316
+ border: 'border-amber-200 dark:border-amber-800/80',
317
+ bg: 'bg-white dark:bg-slate-900',
318
+ iconColor: 'text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-950/60',
319
+ symbol: '⚠',
320
+ },
321
+ info: {
322
+ border: 'border-blue-200 dark:border-blue-800/80',
323
+ bg: 'bg-white dark:bg-slate-900',
324
+ iconColor: 'text-blue-600 dark:text-blue-400 bg-blue-50 dark:bg-blue-950/60',
325
+ symbol: 'ℹ',
326
+ },
327
+ };
328
+ var cfg = typeConfig[t.type] || typeConfig.info;
329
+
330
+ return m('div.pointer-events-auto.shadow-xl.rounded-xl.border.p-4.flex.items-start.gap-3.transition-all.transform', {
331
+ key: t.id,
332
+ class: cfg.border + ' ' + cfg.bg
333
+ }, [
334
+ m('div.w-7.h-7.rounded-lg.flex.items-center.justify-center.text-xs.font-bold.flex-shrink-0', {
335
+ class: cfg.iconColor
336
+ }, cfg.symbol),
337
+ m('div.flex-1.min-w-0', [
338
+ t.title ? m('h4.text-sm.font-semibold.text-gray-900.dark:text-slate-100', t.title) : null,
339
+ m('p.text-xs.text-gray-600.dark:text-slate-300.break-words', t.message)
340
+ ]),
341
+ m('button.text-gray-400.hover:text-gray-600.dark:hover:text-slate-200.p-1.text-xs', {
342
+ onclick: function() { toastState.remove(t.id); }
343
+ }, '✕')
344
+ ]);
345
+ })
346
+ );
347
+ }
348
+ };
349
+
350
+ var confirmState = (typeof window !== 'undefined' && window.confirmState) ? window.confirmState : {
351
+ current: null,
352
+ open: function(options) {
353
+ options = options || {};
354
+ if (typeof options === 'string') {
355
+ options = { message: options };
356
+ }
357
+ return new Promise(function(resolve) {
358
+ confirmState.current = {
359
+ title: options.title || 'Confirmation',
360
+ message: options.message || 'Are you sure you want to proceed?',
361
+ confirmText: options.confirmText || 'Confirm',
362
+ cancelText: options.cancelText || 'Cancel',
363
+ confirmColor: options.confirmColor || (options.danger ? 'red' : 'blue'),
364
+ danger: options.danger || false,
365
+ resolve: resolve,
366
+ };
367
+ try {
368
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
369
+ m.redraw();
370
+ }
371
+ } catch (e) {}
372
+ });
373
+ },
374
+ close: function(result) {
375
+ if (confirmState.current && confirmState.current.resolve) {
376
+ confirmState.current.resolve(Boolean(result));
377
+ }
378
+ confirmState.current = null;
379
+ try {
380
+ if (typeof m !== 'undefined' && typeof m.redraw === 'function') {
381
+ m.redraw();
382
+ }
383
+ } catch (e) {}
384
+ }
385
+ };
386
+
387
+ var confirmModal = function(options) {
388
+ return confirmState.open(options);
389
+ };
390
+
391
+ var ConfirmModalContainer = {
392
+ view: function() {
393
+ var cur = (typeof confirmState !== 'undefined' && confirmState.current) || null;
394
+ if (!cur) return null;
395
+ var isDanger = cur.danger || cur.confirmColor === 'red';
396
+
397
+ return m('div.fixed.inset-0.z-50.flex.items-center.justify-center.p-4', [
398
+ m('div.fixed.inset-0.bg-black/50.backdrop-blur-sm', {
399
+ onclick: function() { confirmState.close(false); }
400
+ }),
401
+ m('div.relative.bg-white.dark:bg-slate-900.rounded-xl.shadow-2xl.border.border-gray-200.dark:border-slate-700.max-w-md.w-full.p-6.z-10', [
402
+ m('div.flex.items-start.gap-4', [
403
+ m('div.w-10.h-10.rounded-full.flex.items-center.justify-center.flex-shrink-0', {
404
+ class: isDanger ? 'bg-red-100 dark:bg-red-950/60 text-red-600 dark:text-red-400' : 'bg-blue-100 dark:bg-blue-950/60 text-blue-600 dark:text-blue-400'
405
+ }, isDanger ? '⚠' : '?'),
406
+ m('div.flex-1', [
407
+ m('h3.text-base.font-bold.text-gray-900.dark:text-slate-100', cur.title),
408
+ m('p.text-xs.text-gray-500.dark:text-slate-400.mt-1.5', cur.message)
409
+ ])
410
+ ]),
411
+ m('div.mt-6.flex.items-center.justify-end.gap-3', [
412
+ m('button.px-4.py-2.text-xs.font-medium.text-gray-700.dark:text-slate-300.bg-gray-100.dark:bg-slate-800.hover:bg-gray-200.dark:hover:bg-slate-700.rounded-lg', {
413
+ onclick: function() { confirmState.close(false); }
414
+ }, cur.cancelText),
415
+ m('button.px-4.py-2.text-xs.font-medium.text-white.rounded-lg.shadow-sm', {
416
+ class: isDanger ? 'bg-red-600 hover:bg-red-700' : 'bg-blue-600 hover:bg-blue-700',
417
+ onclick: function() { confirmState.close(true); }
418
+ }, cur.confirmText)
419
+ ])
420
+ ])
421
+ ]);
422
+ }
423
+ };
424
+
425
+ // Convenience notification & confirmation helpers
426
+ function notifySuccess(msg, optionsOrTtl) {
427
+ return notify.success(msg, optionsOrTtl);
428
+ }
429
+
430
+ function notifyError(msg, optionsOrTtl) {
431
+ return notify.error(msg, optionsOrTtl);
432
+ }
433
+
434
+ function notifyWarning(msg, optionsOrTtl) {
435
+ return notify.warning(msg, optionsOrTtl);
436
+ }
437
+
438
+ function notifyInfo(msg, optionsOrTtl) {
439
+ return notify.info(msg, optionsOrTtl);
440
+ }
441
+
442
+ async function confirmAction(optionsOrMsg) {
443
+ return await confirmModal(optionsOrMsg);
444
+ }
445
+
446
+ // Global window bindings
447
+ if (typeof window !== 'undefined') {
448
+ window.toastState = toastState;
449
+ window.confirmState = confirmState;
450
+ window.notify = notify;
451
+ window.toast = notify;
452
+ window.confirmModal = confirmModal;
453
+ window.ToastContainer = ToastContainer;
454
+ window.ConfirmModalContainer = ConfirmModalContainer;
455
+ window.Admin = window.Admin || {};
456
+ window.Admin.toast = notify;
457
+ window.Admin.confirm = confirmModal;
153
458
  }
154
459
 
155
460
  // Breadcrumb Component