@intranefr/superbackend 1.4.3 → 1.5.0

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 (65) hide show
  1. package/.env.example +6 -1
  2. package/README.md +5 -5
  3. package/index.js +23 -5
  4. package/package.json +5 -2
  5. package/public/sdk/ui-components.iife.js +191 -0
  6. package/sdk/error-tracking/browser/package.json +4 -3
  7. package/sdk/error-tracking/browser/src/embed.js +29 -0
  8. package/sdk/ui-components/browser/src/index.js +228 -0
  9. package/src/controllers/admin.controller.js +139 -1
  10. package/src/controllers/adminHeadless.controller.js +82 -0
  11. package/src/controllers/adminMigration.controller.js +5 -1
  12. package/src/controllers/adminScripts.controller.js +229 -0
  13. package/src/controllers/adminTerminals.controller.js +39 -0
  14. package/src/controllers/adminUiComponents.controller.js +315 -0
  15. package/src/controllers/adminUiComponentsAi.controller.js +34 -0
  16. package/src/controllers/orgAdmin.controller.js +286 -0
  17. package/src/controllers/uiComponentsPublic.controller.js +118 -0
  18. package/src/middleware/auth.js +7 -0
  19. package/src/middleware.js +119 -0
  20. package/src/models/HeadlessModelDefinition.js +10 -0
  21. package/src/models/ScriptDefinition.js +42 -0
  22. package/src/models/ScriptRun.js +22 -0
  23. package/src/models/UiComponent.js +29 -0
  24. package/src/models/UiComponentProject.js +26 -0
  25. package/src/models/UiComponentProjectComponent.js +18 -0
  26. package/src/routes/admin.routes.js +2 -0
  27. package/src/routes/adminHeadless.routes.js +6 -0
  28. package/src/routes/adminScripts.routes.js +21 -0
  29. package/src/routes/adminTerminals.routes.js +13 -0
  30. package/src/routes/adminUiComponents.routes.js +29 -0
  31. package/src/routes/llmUi.routes.js +26 -0
  32. package/src/routes/orgAdmin.routes.js +5 -0
  33. package/src/routes/uiComponentsPublic.routes.js +9 -0
  34. package/src/services/consoleOverride.service.js +291 -0
  35. package/src/services/email.service.js +17 -1
  36. package/src/services/headlessExternalModels.service.js +292 -0
  37. package/src/services/headlessModels.service.js +26 -6
  38. package/src/services/scriptsRunner.service.js +259 -0
  39. package/src/services/terminals.service.js +152 -0
  40. package/src/services/terminalsWs.service.js +100 -0
  41. package/src/services/uiComponentsAi.service.js +312 -0
  42. package/src/services/uiComponentsCrypto.service.js +39 -0
  43. package/src/services/webhook.service.js +2 -2
  44. package/src/services/workflow.service.js +1 -1
  45. package/src/utils/encryption.js +5 -3
  46. package/views/admin-coolify-deploy.ejs +1 -1
  47. package/views/admin-dashboard-home.ejs +1 -1
  48. package/views/admin-dashboard.ejs +1 -1
  49. package/views/admin-errors.ejs +2 -2
  50. package/views/admin-global-settings.ejs +3 -3
  51. package/views/admin-headless.ejs +294 -24
  52. package/views/admin-json-configs.ejs +8 -1
  53. package/views/admin-llm.ejs +2 -2
  54. package/views/admin-organizations.ejs +365 -9
  55. package/views/admin-scripts.ejs +497 -0
  56. package/views/admin-seo-config.ejs +1 -1
  57. package/views/admin-terminals.ejs +328 -0
  58. package/views/admin-test.ejs +3 -3
  59. package/views/admin-ui-components.ejs +709 -0
  60. package/views/admin-users.ejs +440 -4
  61. package/views/admin-webhooks.ejs +1 -1
  62. package/views/admin-workflows.ejs +1 -1
  63. package/views/partials/admin-assets-script.ejs +3 -3
  64. package/views/partials/dashboard/nav-items.ejs +3 -0
  65. package/views/partials/dashboard/palette.ejs +1 -1
@@ -33,6 +33,7 @@
33
33
  <p class="text-sm text-gray-600 mt-1">Browse orgs, members and invites</p>
34
34
  </div>
35
35
  <div class="flex items-center gap-4">
36
+ <button id="btn-create-org" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 font-medium">Create Organization</button>
36
37
  </div>
37
38
  </div>
38
39
  </div>
@@ -90,6 +91,7 @@
90
91
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Name</th>
91
92
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Slug</th>
92
93
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
94
+ <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
93
95
  </tr>
94
96
  </thead>
95
97
  <tbody id="orgs-tbody" class="bg-white divide-y divide-gray-200"></tbody>
@@ -275,6 +277,78 @@
275
277
  </div>
276
278
  </div>
277
279
 
280
+ <!-- Create Organization Modal -->
281
+ <div id="modal-create-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
282
+ <div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
283
+ <div class="px-6 py-4 border-b border-gray-200">
284
+ <h3 class="text-lg font-medium text-gray-900">Create New Organization</h3>
285
+ </div>
286
+ <div class="px-6 py-4">
287
+ <div class="space-y-4">
288
+ <div>
289
+ <label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
290
+ <input id="create-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
291
+ <div class="text-xs text-gray-500 mt-1">2-100 characters. Slug will be auto-generated.</div>
292
+ </div>
293
+ <div>
294
+ <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
295
+ <textarea id="create-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
296
+ <div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
297
+ </div>
298
+ <div>
299
+ <label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
300
+ <input id="create-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="Leave empty to use first admin">
301
+ <div class="text-xs text-gray-500 mt-1">Optional. Leave empty to assign to first admin user.</div>
302
+ </div>
303
+ </div>
304
+ </div>
305
+ <div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
306
+ <button id="btn-create-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
307
+ <button id="btn-create-org-submit" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">Create</button>
308
+ </div>
309
+ </div>
310
+ </div>
311
+
312
+ <!-- Edit Organization Modal -->
313
+ <div id="modal-edit-org" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden items-center justify-center z-50">
314
+ <div class="bg-white rounded-lg shadow-xl max-w-md w-full mx-4">
315
+ <div class="px-6 py-4 border-b border-gray-200">
316
+ <h3 class="text-lg font-medium text-gray-900">Edit Organization</h3>
317
+ </div>
318
+ <div class="px-6 py-4">
319
+ <div class="space-y-4">
320
+ <input id="edit-org-id" type="hidden">
321
+ <div>
322
+ <label class="block text-sm font-medium text-gray-700 mb-1">Name *</label>
323
+ <input id="edit-org-name" type="text" class="w-full border rounded px-3 py-2" placeholder="Organization name" maxlength="100">
324
+ <div class="text-xs text-gray-500 mt-1">2-100 characters.</div>
325
+ </div>
326
+ <div>
327
+ <label class="block text-sm font-medium text-gray-700 mb-1">Description</label>
328
+ <textarea id="edit-org-description" class="w-full border rounded px-3 py-2" rows="3" placeholder="Optional description" maxlength="500"></textarea>
329
+ <div class="text-xs text-gray-500 mt-1">Maximum 500 characters.</div>
330
+ </div>
331
+ <div>
332
+ <label class="block text-sm font-medium text-gray-700 mb-1">Owner User ID</label>
333
+ <input id="edit-org-owner" type="text" class="w-full border rounded px-3 py-2" placeholder="User ID">
334
+ <div class="text-xs text-gray-500 mt-1">Current owner will be replaced.</div>
335
+ </div>
336
+ <div>
337
+ <label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
338
+ <select id="edit-org-status" class="w-full border rounded px-3 py-2">
339
+ <option value="active">Active</option>
340
+ <option value="disabled">Disabled</option>
341
+ </select>
342
+ </div>
343
+ </div>
344
+ </div>
345
+ <div class="px-6 py-4 border-t border-gray-200 flex justify-end gap-3">
346
+ <button id="btn-edit-org-cancel" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Cancel</button>
347
+ <button id="btn-edit-org-submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Save Changes</button>
348
+ </div>
349
+ </div>
350
+ </div>
351
+
278
352
  <!-- Toast Container -->
279
353
  <div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
280
354
 
@@ -400,7 +474,10 @@
400
474
  limit,
401
475
  offset: state.orgs.offset,
402
476
  })}`;
403
- const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
477
+ const res = await fetch(url, {
478
+ headers: { 'Accept': 'application/json' },
479
+ credentials: 'same-origin'
480
+ });
404
481
  const data = await res.json();
405
482
 
406
483
  if (!res.ok) {
@@ -434,7 +511,7 @@
434
511
  if (orgs.length === 0) {
435
512
  tbody.innerHTML = `
436
513
  <tr>
437
- <td class="px-4 py-6 text-sm text-gray-600" colspan="3">No organizations found.</td>
514
+ <td class="px-4 py-6 text-sm text-gray-600" colspan="4">No organizations found.</td>
438
515
  </tr>
439
516
  `;
440
517
  return;
@@ -445,13 +522,29 @@
445
522
  const rowClass = isSelected ? 'bg-blue-50' : 'bg-white';
446
523
 
447
524
  return `
448
- <tr class="${rowClass} hover:bg-gray-50 cursor-pointer" data-org-id="${escapeHtml(o?._id)}">
449
- <td class="px-4 py-3 text-sm text-gray-900">
525
+ <tr class="${rowClass} hover:bg-gray-50" data-org-id="${escapeHtml(o?._id)}">
526
+ <td class="px-4 py-3 text-sm text-gray-900 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
450
527
  <div class="font-medium">${escapeHtml(o?.name)}</div>
451
528
  <div class="text-xs text-gray-500">${escapeHtml(o?.ownerUserId)}</div>
452
529
  </td>
453
- <td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(o?.slug)}</td>
454
- <td class="px-4 py-3 text-sm text-gray-700">${escapeHtml(o?.status)}</td>
530
+ <td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">${escapeHtml(o?.slug)}</td>
531
+ <td class="px-4 py-3 text-sm text-gray-700 cursor-pointer" onclick="selectOrg('${escapeHtml(o?._id)}')">
532
+ <span class="px-2 py-1 text-xs rounded-full ${
533
+ o?.status === 'active'
534
+ ? 'bg-green-100 text-green-800'
535
+ : 'bg-red-100 text-red-800'
536
+ }">
537
+ ${escapeHtml(o?.status)}
538
+ </span>
539
+ </td>
540
+ <td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
541
+ <button class="text-blue-600 hover:text-blue-800 mr-2" data-edit="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}" data-description="${escapeHtml(o?.description || '')}" data-owner="${escapeHtml(o?.ownerUserId)}" data-status="${escapeHtml(o?.status)}">Edit</button>
542
+ ${o?.status === 'active'
543
+ ? `<button class="text-orange-600 hover:text-orange-800 mr-2" data-disable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Disable</button>`
544
+ : `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Enable</button>`
545
+ }
546
+ <button class="text-red-600 hover:text-red-800" data-delete="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Delete</button>
547
+ </td>
455
548
  </tr>
456
549
  `;
457
550
  }).join('');
@@ -511,7 +604,10 @@
511
604
  }
512
605
 
513
606
  try {
514
- const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, { headers: { 'Accept': 'application/json' } });
607
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
608
+ headers: { 'Accept': 'application/json' },
609
+ credentials: 'same-origin'
610
+ });
515
611
  const data = await res.json();
516
612
 
517
613
  if (!res.ok) {
@@ -562,7 +658,10 @@
562
658
  limit: state.members.limit,
563
659
  offset: state.members.offset,
564
660
  })}`;
565
- const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
661
+ const res = await fetch(url, {
662
+ headers: { 'Accept': 'application/json' },
663
+ credentials: 'same-origin'
664
+ });
566
665
  const data = await res.json();
567
666
 
568
667
  if (!res.ok) {
@@ -655,6 +754,7 @@
655
754
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
656
755
  method: 'PATCH',
657
756
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
757
+ credentials: 'same-origin',
658
758
  body: JSON.stringify({ role }),
659
759
  });
660
760
  const data = await res.json();
@@ -680,6 +780,7 @@
680
780
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members/${encodeURIComponent(memberId)}`, {
681
781
  method: 'DELETE',
682
782
  headers: { 'Accept': 'application/json' },
783
+ credentials: 'same-origin'
683
784
  });
684
785
  const data = await res.json();
685
786
  if (!res.ok) {
@@ -722,7 +823,10 @@
722
823
  limit: state.invites.limit,
723
824
  offset: state.invites.offset,
724
825
  })}`;
725
- const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
826
+ const res = await fetch(url, {
827
+ headers: { 'Accept': 'application/json' },
828
+ credentials: 'same-origin'
829
+ });
726
830
  const data = await res.json();
727
831
 
728
832
  if (!res.ok) {
@@ -804,6 +908,7 @@
804
908
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}`, {
805
909
  method: 'DELETE',
806
910
  headers: { 'Accept': 'application/json' },
911
+ credentials: 'same-origin'
807
912
  });
808
913
  const data = await res.json();
809
914
  if (!res.ok) {
@@ -828,6 +933,7 @@
828
933
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites/${encodeURIComponent(inviteId)}/resend`, {
829
934
  method: 'POST',
830
935
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
936
+ credentials: 'same-origin',
831
937
  body: JSON.stringify({}),
832
938
  });
833
939
  const data = await res.json();
@@ -866,6 +972,7 @@
866
972
  const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/invites`, {
867
973
  method: 'POST',
868
974
  headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
975
+ credentials: 'same-origin',
869
976
  body: JSON.stringify({ email, role, expiresInDays }),
870
977
  });
871
978
  const data = await res.json();
@@ -911,6 +1018,39 @@
911
1018
  const nextOrgsBtn = document.getElementById('btn-orgs-next');
912
1019
  if (nextOrgsBtn) nextOrgsBtn.onclick = () => { state.orgs.offset = Math.max(0, state.orgs.offset + state.orgs.limit); loadOrgs(); };
913
1020
 
1021
+ // CRUD event handlers
1022
+ const createOrgBtn = document.getElementById('btn-create-org');
1023
+ if (createOrgBtn) createOrgBtn.onclick = () => openCreateOrgModal();
1024
+
1025
+ const createOrgCancelBtn = document.getElementById('btn-create-org-cancel');
1026
+ if (createOrgCancelBtn) createOrgCancelBtn.onclick = () => closeCreateOrgModal();
1027
+
1028
+ const createOrgSubmitBtn = document.getElementById('btn-create-org-submit');
1029
+ if (createOrgSubmitBtn) createOrgSubmitBtn.onclick = () => createOrganization();
1030
+
1031
+ const editOrgCancelBtn = document.getElementById('btn-edit-org-cancel');
1032
+ if (editOrgCancelBtn) editOrgCancelBtn.onclick = () => closeEditOrgModal();
1033
+
1034
+ const editOrgSubmitBtn = document.getElementById('btn-edit-org-submit');
1035
+ if (editOrgSubmitBtn) editOrgSubmitBtn.onclick = () => updateOrganization();
1036
+
1037
+ // Organization action buttons (will be added dynamically when table loads)
1038
+ document.addEventListener('click', (e) => {
1039
+ if (e.target.matches('[data-edit]')) {
1040
+ const btn = e.target;
1041
+ openEditOrgModal(btn.dataset.edit, btn.dataset.name, btn.dataset.description, btn.dataset.owner, btn.dataset.status);
1042
+ } else if (e.target.matches('[data-disable]')) {
1043
+ const btn = e.target;
1044
+ disableOrganization(btn.dataset.disable, btn.dataset.name);
1045
+ } else if (e.target.matches('[data-enable]')) {
1046
+ const btn = e.target;
1047
+ enableOrganization(btn.dataset.enable, btn.dataset.name);
1048
+ } else if (e.target.matches('[data-delete]')) {
1049
+ const btn = e.target;
1050
+ deleteOrganization(btn.dataset.delete, btn.dataset.name);
1051
+ }
1052
+ });
1053
+
914
1054
  const selectedRefreshBtn = document.getElementById('btn-selected-refresh');
915
1055
  if (selectedRefreshBtn) selectedRefreshBtn.onclick = () => Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
916
1056
 
@@ -969,6 +1109,222 @@
969
1109
  setSelectedControlsEnabled(false);
970
1110
  }
971
1111
 
1112
+ // Modal functions
1113
+ function openCreateOrgModal() {
1114
+ document.getElementById('modal-create-org').classList.remove('hidden');
1115
+ document.getElementById('modal-create-org').classList.add('flex');
1116
+ // Clear form
1117
+ document.getElementById('create-org-name').value = '';
1118
+ document.getElementById('create-org-description').value = '';
1119
+ document.getElementById('create-org-owner').value = '';
1120
+ }
1121
+
1122
+ function closeCreateOrgModal() {
1123
+ document.getElementById('modal-create-org').classList.add('hidden');
1124
+ document.getElementById('modal-create-org').classList.remove('flex');
1125
+ }
1126
+
1127
+ function openEditOrgModal(orgId, name, description, owner, status) {
1128
+ document.getElementById('modal-edit-org').classList.remove('hidden');
1129
+ document.getElementById('modal-edit-org').classList.add('flex');
1130
+ // Populate form
1131
+ document.getElementById('edit-org-id').value = orgId;
1132
+ document.getElementById('edit-org-name').value = name || '';
1133
+ document.getElementById('edit-org-description').value = description || '';
1134
+ document.getElementById('edit-org-owner').value = owner || '';
1135
+ document.getElementById('edit-org-status').value = status || 'active';
1136
+ }
1137
+
1138
+ function closeEditOrgModal() {
1139
+ document.getElementById('modal-edit-org').classList.add('hidden');
1140
+ document.getElementById('modal-edit-org').classList.remove('flex');
1141
+ }
1142
+
1143
+ // CRUD operations
1144
+ async function createOrganization() {
1145
+ const name = document.getElementById('create-org-name').value.trim();
1146
+ const description = document.getElementById('create-org-description').value.trim();
1147
+ const ownerUserId = document.getElementById('create-org-owner').value.trim();
1148
+
1149
+ if (!name) {
1150
+ showToast('Organization name is required', 'error');
1151
+ return;
1152
+ }
1153
+
1154
+ if (name.length < 2) {
1155
+ showToast('Name must be at least 2 characters', 'error');
1156
+ return;
1157
+ }
1158
+
1159
+ try {
1160
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}`, {
1161
+ method: 'POST',
1162
+ headers: { 'Content-Type': 'application/json' },
1163
+ credentials: 'same-origin',
1164
+ body: JSON.stringify({
1165
+ name,
1166
+ description: description || undefined,
1167
+ ownerUserId: ownerUserId || undefined
1168
+ }),
1169
+ });
1170
+
1171
+ const data = await res.json();
1172
+ if (!res.ok) {
1173
+ showToast(data?.error || 'Failed to create organization', 'error');
1174
+ return;
1175
+ }
1176
+
1177
+ showToast('Organization created successfully', 'success');
1178
+ closeCreateOrgModal();
1179
+ await loadOrgs();
1180
+ } catch (e) {
1181
+ showToast(e.message || 'Failed to create organization', 'error');
1182
+ }
1183
+ }
1184
+
1185
+ async function updateOrganization() {
1186
+ const orgId = document.getElementById('edit-org-id').value;
1187
+ const name = document.getElementById('edit-org-name').value.trim();
1188
+ const description = document.getElementById('edit-org-description').value.trim();
1189
+ const ownerUserId = document.getElementById('edit-org-owner').value.trim();
1190
+ const status = document.getElementById('edit-org-status').value;
1191
+
1192
+ if (!name) {
1193
+ showToast('Organization name is required', 'error');
1194
+ return;
1195
+ }
1196
+
1197
+ if (name.length < 2) {
1198
+ showToast('Name must be at least 2 characters', 'error');
1199
+ return;
1200
+ }
1201
+
1202
+ try {
1203
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
1204
+ method: 'PUT',
1205
+ headers: { 'Content-Type': 'application/json' },
1206
+ credentials: 'same-origin',
1207
+ body: JSON.stringify({
1208
+ name,
1209
+ description: description || undefined,
1210
+ ownerUserId: ownerUserId || undefined,
1211
+ status
1212
+ }),
1213
+ });
1214
+
1215
+ const data = await res.json();
1216
+ if (!res.ok) {
1217
+ showToast(data?.error || 'Failed to update organization', 'error');
1218
+ return;
1219
+ }
1220
+
1221
+ showToast('Organization updated successfully', 'success');
1222
+ closeEditOrgModal();
1223
+ await loadOrgs();
1224
+ if (state.orgs.selectedOrgId === orgId) {
1225
+ await loadSelectedOrg();
1226
+ }
1227
+ } catch (e) {
1228
+ showToast(e.message || 'Failed to update organization', 'error');
1229
+ }
1230
+ }
1231
+
1232
+ async function disableOrganization(orgId, orgName) {
1233
+ if (!confirm(`Are you sure you want to disable "${orgName}"?`)) return;
1234
+
1235
+ try {
1236
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/disable`, {
1237
+ method: 'PATCH',
1238
+ credentials: 'same-origin'
1239
+ });
1240
+
1241
+ const data = await res.json();
1242
+ if (!res.ok) {
1243
+ showToast(data?.error || 'Failed to disable organization', 'error');
1244
+ return;
1245
+ }
1246
+
1247
+ showToast('Organization disabled successfully', 'success');
1248
+ await loadOrgs();
1249
+ if (state.orgs.selectedOrgId === orgId) {
1250
+ await loadSelectedOrg();
1251
+ }
1252
+ } catch (e) {
1253
+ showToast(e.message || 'Failed to disable organization', 'error');
1254
+ }
1255
+ }
1256
+
1257
+ async function enableOrganization(orgId, orgName) {
1258
+ if (!confirm(`Are you sure you want to enable "${orgName}"?`)) return;
1259
+
1260
+ try {
1261
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/enable`, {
1262
+ method: 'PATCH',
1263
+ credentials: 'same-origin'
1264
+ });
1265
+
1266
+ const data = await res.json();
1267
+ if (!res.ok) {
1268
+ showToast(data?.error || 'Failed to enable organization', 'error');
1269
+ return;
1270
+ }
1271
+
1272
+ showToast('Organization enabled successfully', 'success');
1273
+ await loadOrgs();
1274
+ if (state.orgs.selectedOrgId === orgId) {
1275
+ await loadSelectedOrg();
1276
+ }
1277
+ } catch (e) {
1278
+ showToast(e.message || 'Failed to enable organization', 'error');
1279
+ }
1280
+ }
1281
+
1282
+ async function deleteOrganization(orgId, orgName) {
1283
+ const confirm1 = confirm(`Are you sure you want to delete "${orgName}"?`);
1284
+ if (!confirm1) return;
1285
+
1286
+ const confirm2 = confirm(
1287
+ 'WARNING: This will permanently delete the organization and ALL its data including:\n' +
1288
+ '• All organization members\n' +
1289
+ '• All pending invites\n' +
1290
+ '• All associated assets and files\n' +
1291
+ '• All notifications and activity\n\n' +
1292
+ 'This action cannot be undone. Continue?'
1293
+ );
1294
+ if (!confirm2) return;
1295
+
1296
+ try {
1297
+ showToast('Deleting organization and cleaning up data...', 'success');
1298
+
1299
+ const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}`, {
1300
+ method: 'DELETE',
1301
+ credentials: 'same-origin'
1302
+ });
1303
+
1304
+ const data = await res.json();
1305
+ if (!res.ok) {
1306
+ showToast(data?.error || 'Failed to delete organization', 'error');
1307
+ return;
1308
+ }
1309
+
1310
+ showToast(`Organization "${orgName}" deleted permanently`, 'success');
1311
+
1312
+ // Clear selection if this org was selected
1313
+ if (state.orgs.selectedOrgId === orgId) {
1314
+ state.orgs.selectedOrgId = null;
1315
+ setSelectedControlsEnabled(false);
1316
+ document.getElementById('selected-org-subtitle').textContent = 'Select an org from the list';
1317
+ document.getElementById('kpi-members').textContent = '-';
1318
+ document.getElementById('kpi-invites').textContent = '-';
1319
+ document.getElementById('kpi-org-status').textContent = '-';
1320
+ }
1321
+
1322
+ await loadOrgs();
1323
+ } catch (e) {
1324
+ showToast(e.message || 'Failed to delete organization', 'error');
1325
+ }
1326
+ }
1327
+
972
1328
  bindEvents();
973
1329
  loadOrgs();
974
1330
  </script>