@intranefr/superbackend 1.7.7 → 1.7.9

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 (119) hide show
  1. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl +0 -0
  2. package/.beads/.br_history/issues.20260314_212352_900045509.jsonl.meta.json +1 -0
  3. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl +1 -0
  4. package/.beads/.br_history/issues.20260314_212353_087140743.jsonl.meta.json +1 -0
  5. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl +2 -0
  6. package/.beads/.br_history/issues.20260314_212353_285881504.jsonl.meta.json +1 -0
  7. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl +3 -0
  8. package/.beads/.br_history/issues.20260314_212353_473915419.jsonl.meta.json +1 -0
  9. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl +4 -0
  10. package/.beads/.br_history/issues.20260314_212353_659476307.jsonl.meta.json +1 -0
  11. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl +5 -0
  12. package/.beads/.br_history/issues.20260314_212353_869998925.jsonl.meta.json +1 -0
  13. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl +6 -0
  14. package/.beads/.br_history/issues.20260314_212354_054785029.jsonl.meta.json +1 -0
  15. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl +7 -0
  16. package/.beads/.br_history/issues.20260314_213336_175893691.jsonl.meta.json +1 -0
  17. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl +7 -0
  18. package/.beads/.br_history/issues.20260314_213336_338509797.jsonl.meta.json +1 -0
  19. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl +7 -0
  20. package/.beads/.br_history/issues.20260314_213336_515443192.jsonl.meta.json +1 -0
  21. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl +7 -0
  22. package/.beads/.br_history/issues.20260314_213336_676417592.jsonl.meta.json +1 -0
  23. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl +7 -0
  24. package/.beads/.br_history/issues.20260314_213336_839182422.jsonl.meta.json +1 -0
  25. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl +7 -0
  26. package/.beads/.br_history/issues.20260314_213337_004349113.jsonl.meta.json +1 -0
  27. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl +7 -0
  28. package/.beads/.br_history/issues.20260314_213337_179824080.jsonl.meta.json +1 -0
  29. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl +7 -0
  30. package/.beads/.br_history/issues.20260314_213701_705075332.jsonl.meta.json +1 -0
  31. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl +8 -0
  32. package/.beads/.br_history/issues.20260314_213706_783128702.jsonl.meta.json +1 -0
  33. package/.beads/config.yaml +4 -0
  34. package/.beads/issues.jsonl +8 -0
  35. package/.beads/metadata.json +4 -0
  36. package/.env.example +8 -0
  37. package/autochangelog/.env.example +36 -0
  38. package/autochangelog/README.md +412 -0
  39. package/autochangelog/config/database.js +27 -0
  40. package/autochangelog/package.json +47 -0
  41. package/autochangelog/public/landing.html +581 -0
  42. package/autochangelog/server.js +104 -0
  43. package/autochangelog/src/app.js +181 -0
  44. package/autochangelog/src/config/database.js +26 -0
  45. package/autochangelog/src/controllers/auth.js +488 -0
  46. package/autochangelog/src/controllers/changelog.js +682 -0
  47. package/autochangelog/src/controllers/project.js +580 -0
  48. package/autochangelog/src/controllers/repository.js +780 -0
  49. package/autochangelog/src/middleware/auth.js +386 -0
  50. package/autochangelog/src/models/Changelog.js +443 -0
  51. package/autochangelog/src/models/Project.js +226 -0
  52. package/autochangelog/src/models/Repository.js +366 -0
  53. package/autochangelog/src/models/User.js +223 -0
  54. package/autochangelog/src/routes/auth.routes.js +32 -0
  55. package/autochangelog/src/routes/changelog.routes.js +42 -0
  56. package/autochangelog/src/routes/github-auth.routes.js +102 -0
  57. package/autochangelog/src/routes/project.routes.js +50 -0
  58. package/autochangelog/src/routes/repository.routes.js +54 -0
  59. package/autochangelog/src/services/changelog.js +722 -0
  60. package/autochangelog/src/services/github.js +243 -0
  61. package/autochangelog/utils/logger.js +77 -0
  62. package/autochangelog/views/404.ejs +18 -0
  63. package/autochangelog/views/dashboard.ejs +596 -0
  64. package/autochangelog/views/index.ejs +231 -0
  65. package/autochangelog/views/layouts/main.ejs +44 -0
  66. package/autochangelog/views/login.ejs +104 -0
  67. package/autochangelog/views/partials/footer.ejs +20 -0
  68. package/autochangelog/views/partials/navbar.ejs +51 -0
  69. package/autochangelog/views/register.ejs +109 -0
  70. package/autochangelog-cli/README.md +266 -0
  71. package/autochangelog-cli/bin/autochangelog +120 -0
  72. package/autochangelog-cli/package.json +46 -0
  73. package/autochangelog-cli/src/cli/commands/auth.js +291 -0
  74. package/autochangelog-cli/src/cli/commands/changelog.js +619 -0
  75. package/autochangelog-cli/src/cli/commands/project.js +427 -0
  76. package/autochangelog-cli/src/cli/commands/repo.js +557 -0
  77. package/autochangelog-cli/src/cli/commands/stats.js +706 -0
  78. package/autochangelog-cli/src/cli/utils/config.js +277 -0
  79. package/autochangelog-cli/src/cli/utils/errors.js +307 -0
  80. package/autochangelog-cli/src/cli/utils/logger.js +75 -0
  81. package/autochangelog-cli/src/cli/utils/output.js +357 -0
  82. package/package.json +8 -3
  83. package/plugins/supercli/README.md +108 -0
  84. package/plugins/supercli/plugin.json +123 -0
  85. package/server.js +1 -1
  86. package/src/cli/api.js +380 -0
  87. package/src/cli/direct/agent-utils.js +61 -0
  88. package/src/cli/direct/cli-utils.js +112 -0
  89. package/src/cli/direct/data-seeding.js +307 -0
  90. package/src/cli/direct/db-admin.js +84 -0
  91. package/src/cli/direct/db-advanced.js +372 -0
  92. package/src/cli/direct/db-utils.js +558 -0
  93. package/src/cli/direct/help.js +195 -0
  94. package/src/cli/direct/migration.js +107 -0
  95. package/src/cli/direct/rbac-advanced.js +132 -0
  96. package/src/cli/direct/resources-additional.js +400 -0
  97. package/src/cli/direct/resources-cms-advanced.js +173 -0
  98. package/src/cli/direct/resources-cms.js +247 -0
  99. package/src/cli/direct/resources-core.js +253 -0
  100. package/src/cli/direct/resources-execution.js +367 -0
  101. package/src/cli/direct/resources-health.js +152 -0
  102. package/src/cli/direct/resources-integrations.js +182 -0
  103. package/src/cli/direct/resources-logs.js +204 -0
  104. package/src/cli/direct/resources-org-rbac.js +187 -0
  105. package/src/cli/direct/resources-system.js +236 -0
  106. package/src/cli/direct.js +556 -0
  107. package/src/controllers/admin.controller.js +4 -0
  108. package/src/controllers/auth.controller.js +148 -1
  109. package/src/controllers/waitingList.controller.js +130 -1
  110. package/src/models/RbacRole.js +1 -1
  111. package/src/models/User.js +39 -5
  112. package/src/routes/auth.routes.js +6 -0
  113. package/src/routes/waitingList.routes.js +12 -2
  114. package/src/routes/waitingListAdmin.routes.js +3 -0
  115. package/src/services/email.service.js +1 -0
  116. package/src/services/github.service.js +255 -0
  117. package/src/services/rateLimiter.service.js +29 -1
  118. package/src/services/waitingListJson.service.js +32 -3
  119. package/views/admin-waiting-list.ejs +386 -3
@@ -71,18 +71,43 @@
71
71
  </div>
72
72
 
73
73
  <!-- Entries table (admin) -->
74
- <div class="bg-white rounded-lg shadow p-6 mb-8">
74
+ <div class="bg-white rounded-lg shadow p-6 mb-8 relative">
75
75
  <div class="flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
76
76
  <div>
77
77
  <h2 class="text-xl font-bold text-gray-900">Entries</h2>
78
78
  <p id="entries-subtitle" class="text-sm text-gray-600 mt-1">Loading...</p>
79
79
  </div>
80
80
  <div class="flex gap-2">
81
+ <button id="btn-export-csv" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 flex items-center gap-2">
82
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
83
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
84
+ </svg>
85
+ Export CSV
86
+ </button>
81
87
  <button id="btn-copy-visible" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Copy visible</button>
82
88
  <button id="btn-refresh-entries" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Refresh</button>
83
89
  </div>
84
90
  </div>
85
91
 
92
+ <!-- Loading Overlay -->
93
+ <div id="entries-loading" class="hidden absolute inset-0 bg-white bg-opacity-75 flex items-center justify-center z-10 rounded-lg">
94
+ <div class="flex items-center gap-2 text-gray-600">
95
+ <svg class="animate-spin h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
96
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
97
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
98
+ </svg>
99
+ <span>Loading...</span>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Type Filter Cards -->
104
+ <div class="mt-6">
105
+ <h3 class="text-sm font-medium text-gray-700 mb-3">Filter by Type</h3>
106
+ <div id="type-filter-cards" class="flex flex-wrap gap-3">
107
+ <!-- Cards will be rendered here -->
108
+ </div>
109
+ </div>
110
+
86
111
  <div class="grid grid-cols-1 sm:grid-cols-4 gap-4 mt-4">
87
112
  <div>
88
113
  <label class="block text-sm font-medium text-gray-700 mb-1">Status</label>
@@ -111,6 +136,13 @@
111
136
  <button id="btn-entries-apply" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">Apply</button>
112
137
  <button id="btn-entries-reset" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Reset</button>
113
138
  <div class="flex-1"></div>
139
+ <button id="btn-bulk-remove" class="bg-red-500 text-white px-4 py-2 rounded hover:bg-red-600 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2" disabled>
140
+ <svg id="bulk-remove-spinner" class="animate-spin h-4 w-4 hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
141
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
142
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
143
+ </svg>
144
+ Delete Selected (<span id="selected-count">0</span>)
145
+ </button>
114
146
  <button id="btn-entries-prev" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Prev</button>
115
147
  <button id="btn-entries-next" class="bg-gray-100 text-gray-800 px-4 py-2 rounded hover:bg-gray-200">Next</button>
116
148
  </div>
@@ -119,6 +151,9 @@
119
151
  <table class="min-w-full divide-y divide-gray-200">
120
152
  <thead class="bg-gray-50">
121
153
  <tr>
154
+ <th class="px-4 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
155
+ <input type="checkbox" id="select-all-entries" class="rounded border-gray-300 text-blue-600 focus:ring-blue-500">
156
+ </th>
122
157
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created</th>
123
158
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Email</th>
124
159
  <th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Type</th>
@@ -166,6 +201,48 @@
166
201
  <pre id="subscribe-response" class="bg-gray-100 p-4 rounded overflow-auto max-h-96"></pre>
167
202
  </div>
168
203
  </div>
204
+
205
+ <!-- Integration -->
206
+ <div class="bg-white rounded-lg shadow p-6">
207
+ <details class="group">
208
+ <summary class="flex items-center justify-between cursor-pointer list-none">
209
+ <div>
210
+ <h2 class="text-xl font-bold text-gray-900">Integration</h2>
211
+ <p class="text-sm text-gray-600 mt-1">cURL example for external apps</p>
212
+ </div>
213
+ <span class="transition-transform group-open:rotate-180">
214
+ <svg class="w-6 h-6 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
215
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"></path>
216
+ </svg>
217
+ </span>
218
+ </summary>
219
+
220
+ <div class="mt-6">
221
+ <div class="bg-gray-900 rounded-lg p-4 overflow-x-auto">
222
+ <div class="flex items-center justify-between mb-2">
223
+ <span class="text-xs font-mono text-gray-400">bash</span>
224
+ <button id="btn-copy-curl" class="text-xs bg-gray-700 hover:bg-gray-600 text-white px-3 py-1 rounded flex items-center gap-1">
225
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
226
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
227
+ </svg>
228
+ Copy
229
+ </button>
230
+ </div>
231
+ <pre id="curl-example" class="text-green-400 font-mono text-sm whitespace-pre-wrap break-all"></pre>
232
+ </div>
233
+
234
+ <div class="mt-4 text-sm text-gray-600">
235
+ <p class="mb-2"><strong class="text-gray-900">Endpoint:</strong> <code class="bg-gray-100 px-2 py-1 rounded">POST /api/waiting-list/subscribe</code></p>
236
+ <p class="mb-2"><strong class="text-gray-900">Fields:</strong></p>
237
+ <ul class="list-disc list-inside space-y-1 ml-2">
238
+ <li><code class="bg-gray-100 px-2 py-0.5 rounded text-sm">email</code> (required) - User email address</li>
239
+ <li><code class="bg-gray-100 px-2 py-0.5 rounded text-sm">type</code> (required) - Interest type (e.g., buyer, seller, both)</li>
240
+ <li><code class="bg-gray-100 px-2 py-0.5 rounded text-sm">referralSource</code> (optional) - Traffic source (default: website)</li>
241
+ </ul>
242
+ </div>
243
+ </div>
244
+ </details>
245
+ </div>
169
246
  </div>
170
247
  </div>
171
248
 
@@ -178,6 +255,27 @@
178
255
  const WAITING_LIST_SUBSCRIBE_PATH = '/api/waiting-list/subscribe';
179
256
  const WAITING_LIST_ADMIN_LIST_PATH = '/api/admin/waiting-list';
180
257
 
258
+ // Generate cURL example with all fields
259
+ function generateCurlExample() {
260
+ const baseUrl = window.location.origin;
261
+ const curlCommand = `curl -X POST ${baseUrl}/api/waiting-list/subscribe \\
262
+ -H "Content-Type: application/json" \\
263
+ -d '{
264
+ "email": "user@example.com",
265
+ "type": "buyer",
266
+ "referralSource": "website"
267
+ }'`;
268
+ return curlCommand;
269
+ }
270
+
271
+ // Render cURL example on page load
272
+ function renderCurlExample() {
273
+ const curlEl = document.getElementById('curl-example');
274
+ if (curlEl) {
275
+ curlEl.textContent = generateCurlExample();
276
+ }
277
+ }
278
+
181
279
  function showToast(message, type = 'success') {
182
280
  const container = document.getElementById('toast-container');
183
281
  const toast = document.createElement('div');
@@ -272,8 +370,175 @@
272
370
 
273
371
  let entriesState = { offset: 0, total: 0, limit: 50 };
274
372
  let lastEntries = [];
373
+ let selectedType = null; // Track selected type filter
374
+ let selectedEntryIds = new Set(); // Track selected entry IDs for bulk delete
375
+ let isLoading = false;
376
+
377
+ function showLoading(show) {
378
+ const loadingEl = document.getElementById('entries-loading');
379
+ if (loadingEl) {
380
+ loadingEl.classList.toggle('hidden', !show);
381
+ }
382
+ isLoading = show;
383
+ }
384
+
385
+ async function updateBulkDeleteButton() {
386
+ const btn = document.getElementById('btn-bulk-remove');
387
+ const countEl = document.getElementById('selected-count');
388
+ if (btn && countEl) {
389
+ const count = selectedEntryIds.size;
390
+ countEl.textContent = count;
391
+ btn.disabled = count === 0 || isLoading;
392
+ }
393
+ }
394
+
395
+ function toggleEntrySelection(entryId, isChecked) {
396
+ if (isChecked) {
397
+ selectedEntryIds.add(entryId);
398
+ } else {
399
+ selectedEntryIds.delete(entryId);
400
+ }
401
+ updateBulkDeleteButton();
402
+ }
403
+
404
+ function toggleSelectAll(isChecked) {
405
+ if (isChecked) {
406
+ // Select all visible entries
407
+ lastEntries.forEach(entry => {
408
+ if (entry && entry.id) {
409
+ selectedEntryIds.add(entry.id);
410
+ }
411
+ });
412
+ } else {
413
+ // Deselect only visible entries
414
+ lastEntries.forEach(entry => {
415
+ if (entry && entry.id) {
416
+ selectedEntryIds.delete(entry.id);
417
+ }
418
+ });
419
+ }
420
+ updateBulkDeleteButton();
421
+
422
+ // Update individual checkboxes to match select-all state
423
+ document.querySelectorAll('.entry-checkbox').forEach(cb => {
424
+ cb.checked = isChecked;
425
+ });
426
+ }
427
+
428
+ async function loadTypes() {
429
+ try {
430
+ const res = await fetch(`${API_BASE}/api/admin/waiting-list/types`, {
431
+ headers: { 'Accept': 'application/json' }
432
+ });
433
+ const data = await res.json();
434
+
435
+ if (!res.ok) {
436
+ console.error('Failed to load types:', data?.error);
437
+ return;
438
+ }
439
+
440
+ const container = document.getElementById('type-filter-cards');
441
+ if (!container) return;
442
+
443
+ const types = data?.types || [];
444
+
445
+ // Render ALL card
446
+ let html = `
447
+ <button
448
+ class="type-filter-card px-4 py-3 rounded-lg border-2 transition-all ${
449
+ selectedType === null
450
+ ? 'border-blue-500 bg-blue-50 shadow-md'
451
+ : 'border-gray-200 bg-white hover:border-gray-300'
452
+ }"
453
+ data-type=""
454
+ >
455
+ <div class="text-sm font-medium text-gray-600">ALL</div>
456
+ <div class="text-2xl font-bold text-gray-900 mt-1">${data?.total || 0}</div>
457
+ </button>
458
+ `;
459
+
460
+ // Render type cards
461
+ types.forEach(({ type, count }) => {
462
+ const isSelected = selectedType === type;
463
+ const safeType = String(type)
464
+ .replaceAll('&', '&amp;')
465
+ .replaceAll('<', '&lt;')
466
+ .replaceAll('>', '&gt;')
467
+ .replaceAll('"', '&quot;')
468
+ .replaceAll("'", '&#39;');
469
+
470
+ html += `
471
+ <button
472
+ class="type-filter-card px-4 py-3 rounded-lg border-2 transition-all ${
473
+ isSelected
474
+ ? 'border-blue-500 bg-blue-50 shadow-md'
475
+ : 'border-gray-200 bg-white hover:border-gray-300'
476
+ }"
477
+ data-type="${safeType}"
478
+ >
479
+ <div class="text-sm font-medium text-gray-600">${safeType}</div>
480
+ <div class="text-2xl font-bold text-gray-900 mt-1">${count}</div>
481
+ </button>
482
+ `;
483
+ });
484
+
485
+ container.innerHTML = html;
486
+
487
+ // Bind click events
488
+ container.querySelectorAll('.type-filter-card').forEach((card) => {
489
+ card.addEventListener('click', () => handleTypeFilterClick(card));
490
+ });
491
+ } catch (e) {
492
+ console.error('Error loading types:', e);
493
+ }
494
+ }
495
+
496
+ function handleTypeFilterClick(card) {
497
+ const type = card.getAttribute('data-type');
498
+ selectedType = type || null;
499
+
500
+ // Update UI - highlight selected card
501
+ document.querySelectorAll('.type-filter-card').forEach((c) => {
502
+ c.classList.remove('border-blue-500', 'bg-blue-50', 'shadow-md');
503
+ c.classList.add('border-gray-200', 'hover:border-gray-300');
504
+ });
505
+
506
+ card.classList.remove('border-gray-200', 'hover:border-gray-300');
507
+ card.classList.add('border-blue-500', 'bg-blue-50', 'shadow-md');
508
+
509
+ // Update hidden input and reload entries
510
+ const typeInput = document.getElementById('entries-type');
511
+ if (typeInput) typeInput.value = selectedType || '';
512
+
513
+ // Reset pagination and reload
514
+ entriesState.offset = 0;
515
+ loadEntries();
516
+ }
517
+
518
+ function exportCsv() {
519
+ // Get current filter values
520
+ const status = document.getElementById('entries-status')?.value?.trim();
521
+ const type = document.getElementById('entries-type')?.value?.trim();
522
+ const email = document.getElementById('entries-email')?.value?.trim();
523
+
524
+ // Build query string with current filters
525
+ const params = new URLSearchParams();
526
+ if (status) params.set('status', status);
527
+ if (type) params.set('type', type);
528
+ if (email) params.set('email', email);
529
+
530
+ // Create download link and trigger it
531
+ const downloadUrl = `${API_BASE}/api/admin/waiting-list/export-csv?${params.toString()}`;
532
+
533
+ // Open in new tab to trigger download
534
+ window.open(downloadUrl, '_blank');
535
+
536
+ showToast('CSV export started', 'success');
537
+ }
275
538
 
276
539
  async function loadEntries() {
540
+ if (isLoading) return; // Prevent concurrent loads
541
+
277
542
  const status = document.getElementById('entries-status')?.value?.trim();
278
543
  const type = document.getElementById('entries-type')?.value?.trim();
279
544
  const email = document.getElementById('entries-email')?.value?.trim();
@@ -281,16 +546,20 @@
281
546
 
282
547
  const subtitle = document.getElementById('entries-subtitle');
283
548
  const tbody = document.getElementById('entries-tbody');
549
+
550
+ showLoading(true);
284
551
  if (subtitle) subtitle.textContent = 'Loading...';
285
552
  if (tbody) tbody.innerHTML = '';
286
553
 
287
554
  try {
555
+ // Add timestamp to bypass browser cache
288
556
  const url = `${API_BASE}${WAITING_LIST_ADMIN_LIST_PATH}${qs({
289
557
  status: status || undefined,
290
558
  type: type || undefined,
291
559
  email: email || undefined,
292
560
  limit,
293
561
  offset: entriesState.offset,
562
+ _t: Date.now() // Cache buster
294
563
  })}`;
295
564
 
296
565
  const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
@@ -332,9 +601,19 @@
332
601
  const statusCell = escapeHtml(e?.status);
333
602
  const refCell = escapeHtml(e?.referralSource || '');
334
603
  const rawEmail = String(e?.email || '');
604
+ const entryId = e?.id || '';
605
+ const isChecked = selectedEntryIds.has(entryId) ? 'checked' : '';
335
606
 
336
607
  return `
337
- <tr>
608
+ <tr class="${isChecked ? 'bg-blue-50' : ''}">
609
+ <td class="px-4 py-3 text-sm text-center">
610
+ <input
611
+ type="checkbox"
612
+ class="entry-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500"
613
+ data-entry-id="${escapeHtml(entryId)}"
614
+ ${isChecked}
615
+ >
616
+ </td>
338
617
  <td class="px-4 py-3 text-sm text-gray-900 whitespace-nowrap">${createdAt}</td>
339
618
  <td class="px-4 py-3 text-sm text-gray-900">${emailCell}</td>
340
619
  <td class="px-4 py-3 text-sm text-gray-700">${typeCell}</td>
@@ -347,6 +626,16 @@
347
626
  `;
348
627
  }).join('');
349
628
 
629
+ // Bind checkbox events
630
+ tbody.querySelectorAll('.entry-checkbox').forEach((cb) => {
631
+ cb.addEventListener('change', (e) => {
632
+ const entryId = e.target.getAttribute('data-entry-id');
633
+ toggleEntrySelection(entryId, e.target.checked);
634
+ // Highlight row
635
+ e.target.closest('tr').classList.toggle('bg-blue-50', e.target.checked);
636
+ });
637
+ });
638
+
350
639
  tbody.querySelectorAll('[data-copy-email]').forEach((btn) => {
351
640
  btn.addEventListener('click', () => copyText(btn.getAttribute('data-copy-email') || ''));
352
641
  });
@@ -357,9 +646,68 @@
357
646
  const nextBtn = document.getElementById('btn-entries-next');
358
647
  if (prevBtn) prevBtn.disabled = usedOffset <= 0;
359
648
  if (nextBtn) nextBtn.disabled = usedOffset + usedLimit >= total;
649
+
650
+ // Update select-all checkbox state based on current selection
651
+ const selectAllCb = document.getElementById('select-all-entries');
652
+ if (selectAllCb && entries.length > 0) {
653
+ const allVisibleSelected = entries.every(e => selectedEntryIds.has(e.id));
654
+ selectAllCb.checked = allVisibleSelected;
655
+ selectAllCb.indeterminate = !allVisibleSelected && entries.some(e => selectedEntryIds.has(e.id));
656
+ }
360
657
  } catch (e) {
361
658
  showToast(e.message || 'Failed to load entries', 'error');
362
659
  if (subtitle) subtitle.textContent = 'Failed to load.';
660
+ } finally {
661
+ showLoading(false);
662
+ }
663
+ }
664
+
665
+ async function bulkRemoveEntries() {
666
+ if (selectedEntryIds.size === 0) {
667
+ showToast('No entries selected', 'error');
668
+ return;
669
+ }
670
+
671
+ const count = selectedEntryIds.size;
672
+ if (!confirm(`Are you sure you want to delete ${count} entr${count === 1 ? 'y' : 'ies'}? This action cannot be undone.`)) {
673
+ return;
674
+ }
675
+
676
+ try {
677
+ showLoading(true);
678
+ const btn = document.getElementById('btn-bulk-remove');
679
+ const spinner = document.getElementById('bulk-remove-spinner');
680
+ if (btn) btn.disabled = true;
681
+ if (spinner) spinner.classList.remove('hidden');
682
+
683
+ const res = await fetch(`${API_BASE}/api/admin/waiting-list/bulk-remove`, {
684
+ method: 'POST',
685
+ headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
686
+ body: JSON.stringify({ entryIds: Array.from(selectedEntryIds) }),
687
+ });
688
+
689
+ const data = await res.json();
690
+
691
+ if (!res.ok) {
692
+ showToast(data?.error || 'Failed to delete entries', 'error');
693
+ return;
694
+ }
695
+
696
+ showToast(`Successfully deleted ${data.removed?.count || 0} entrie(s)`, 'success');
697
+
698
+ // Clear selection and reload
699
+ selectedEntryIds.clear();
700
+ updateBulkDeleteButton();
701
+ loadEntries();
702
+ loadTypes(); // Refresh type counts
703
+ } catch (e) {
704
+ showToast(e.message || 'Failed to delete entries', 'error');
705
+ } finally {
706
+ showLoading(false);
707
+ const btn = document.getElementById('btn-bulk-remove');
708
+ const spinner = document.getElementById('bulk-remove-spinner');
709
+ if (btn) btn.disabled = true; // Will be re-enabled when selection changes
710
+ if (spinner) spinner.classList.add('hidden');
363
711
  }
364
712
  }
365
713
 
@@ -428,7 +776,28 @@
428
776
  if (refreshBtn) refreshBtn.onclick = loadStats;
429
777
 
430
778
  const refreshEntriesBtn = document.getElementById('btn-refresh-entries');
431
- if (refreshEntriesBtn) refreshEntriesBtn.onclick = loadEntries;
779
+ if (refreshEntriesBtn) refreshEntriesBtn.onclick = () => {
780
+ selectedType = null; // Reset type filter on refresh
781
+ loadEntries();
782
+ loadTypes(); // Reload types to update counts
783
+ };
784
+
785
+ const exportCsvBtn = document.getElementById('btn-export-csv');
786
+ if (exportCsvBtn) {
787
+ exportCsvBtn.onclick = exportCsv;
788
+ }
789
+
790
+ const bulkRemoveBtn = document.getElementById('btn-bulk-remove');
791
+ if (bulkRemoveBtn) {
792
+ bulkRemoveBtn.onclick = bulkRemoveEntries;
793
+ }
794
+
795
+ const selectAllCb = document.getElementById('select-all-entries');
796
+ if (selectAllCb) {
797
+ selectAllCb.addEventListener('change', (e) => {
798
+ toggleSelectAll(e.target.checked);
799
+ });
800
+ }
432
801
 
433
802
  const applyEntriesBtn = document.getElementById('btn-entries-apply');
434
803
  if (applyEntriesBtn) {
@@ -445,8 +814,12 @@
445
814
  document.getElementById('entries-type').value = '';
446
815
  document.getElementById('entries-email').value = '';
447
816
  document.getElementById('entries-limit').value = '50';
817
+ selectedType = null; // Reset type filter
818
+ selectedEntryIds.clear(); // Clear selection
448
819
  entriesState.offset = 0;
449
820
  loadEntries();
821
+ loadTypes(); // Reload types to update UI
822
+ updateBulkDeleteButton();
450
823
  };
451
824
  }
452
825
 
@@ -491,6 +864,14 @@
491
864
  };
492
865
  }
493
866
 
867
+ const copyCurlBtn = document.getElementById('btn-copy-curl');
868
+ if (copyCurlBtn) {
869
+ copyCurlBtn.onclick = () => {
870
+ const curlCommand = generateCurlExample();
871
+ copyText(curlCommand);
872
+ };
873
+ }
874
+
494
875
  const form = document.getElementById('subscribe-form');
495
876
  if (form) {
496
877
  form.onsubmit = async (event) => {
@@ -533,7 +914,9 @@
533
914
 
534
915
  bindEvents();
535
916
  loadStats();
917
+ loadTypes();
536
918
  loadEntries();
919
+ renderCurlExample();
537
920
  </script>
538
921
  <script>
539
922
  window.addEventListener("keydown", (e) => {