@intranefr/superbackend 1.6.6 → 1.7.7

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 (51) hide show
  1. package/.env.example +4 -0
  2. package/README.md +18 -0
  3. package/package.json +8 -2
  4. package/public/js/admin-superdemos.js +396 -0
  5. package/public/sdk/superdemos.iife.js +614 -0
  6. package/public/superdemos-qa.html +324 -0
  7. package/sdk/superdemos/browser/src/index.js +719 -0
  8. package/src/cli/agent-chat.js +369 -0
  9. package/src/cli/agent-list.js +42 -0
  10. package/src/controllers/adminAgentsChat.controller.js +172 -0
  11. package/src/controllers/adminSuperDemos.controller.js +382 -0
  12. package/src/controllers/superDemosPublic.controller.js +126 -0
  13. package/src/middleware.js +102 -19
  14. package/src/models/BlogAutomationLock.js +4 -4
  15. package/src/models/BlogPost.js +16 -16
  16. package/src/models/CacheEntry.js +17 -6
  17. package/src/models/JsonConfig.js +2 -4
  18. package/src/models/RateLimitMetricBucket.js +10 -5
  19. package/src/models/SuperDemo.js +38 -0
  20. package/src/models/SuperDemoProject.js +32 -0
  21. package/src/models/SuperDemoStep.js +27 -0
  22. package/src/routes/adminAgents.routes.js +10 -0
  23. package/src/routes/adminMarkdowns.routes.js +3 -0
  24. package/src/routes/adminSuperDemos.routes.js +31 -0
  25. package/src/routes/superDemos.routes.js +9 -0
  26. package/src/services/auditLogger.js +75 -37
  27. package/src/services/email.service.js +18 -3
  28. package/src/services/superDemosAuthoringSessions.service.js +132 -0
  29. package/src/services/superDemosWs.service.js +164 -0
  30. package/src/services/terminalsWs.service.js +35 -3
  31. package/src/utils/rbac/rightsRegistry.js +2 -0
  32. package/views/admin-agents.ejs +261 -11
  33. package/views/admin-dashboard.ejs +78 -8
  34. package/views/admin-superdemos.ejs +335 -0
  35. package/views/admin-terminals.ejs +462 -34
  36. package/views/partials/admin/agents-chat.ejs +80 -0
  37. package/views/partials/dashboard/nav-items.ejs +1 -0
  38. package/views/partials/dashboard/tab-bar.ejs +6 -0
  39. package/cookies.txt +0 -6
  40. package/cookies1.txt +0 -6
  41. package/cookies2.txt +0 -6
  42. package/cookies3.txt +0 -6
  43. package/cookies4.txt +0 -5
  44. package/cookies_old.txt +0 -5
  45. package/cookies_old_test.txt +0 -6
  46. package/cookies_super.txt +0 -5
  47. package/cookies_super_test.txt +0 -6
  48. package/cookies_test.txt +0 -6
  49. package/test-access.js +0 -63
  50. package/test-iframe-fix.html +0 -63
  51. package/test-iframe.html +0 -14
@@ -7,6 +7,9 @@
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
9
9
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css">
10
+ <style>
11
+ [v-cloak] { display: none; }
12
+ </style>
10
13
  </head>
11
14
  <body class="bg-gray-50 min-h-screen">
12
15
  <div id="app" class="p-6 max-w-6xl mx-auto" v-cloak>
@@ -15,13 +18,19 @@
15
18
  <h1 class="text-2xl font-bold text-gray-800">AI Agents</h1>
16
19
  <p class="text-gray-500">Configure your intelligent agent gateway</p>
17
20
  </div>
18
- <button @click="openCreateModal" class="bg-indigo-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-indigo-700 transition">
21
+ <button v-if="activeTab === 'agents'" @click="openCreateModal" class="bg-indigo-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-indigo-700 transition">
19
22
  <i class="ti ti-plus"></i>
20
23
  Create Agent
21
24
  </button>
22
25
  </div>
23
26
 
24
- <!-- Agent List -->
27
+ <div class="mb-6 bg-white border border-gray-200 rounded-lg p-1 inline-flex gap-1">
28
+ <button @click="activeTab = 'agents'" class="px-4 py-2 rounded-md text-sm font-medium" :class="activeTab === 'agents' ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:bg-gray-100'">Agents</button>
29
+ <button @click="activeTab = 'chat'" class="px-4 py-2 rounded-md text-sm font-medium" :class="activeTab === 'chat' ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:bg-gray-100'">Chat UI</button>
30
+ </div>
31
+
32
+ <div v-if="activeTab === 'agents'">
33
+
25
34
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
26
35
  <div v-for="agent in agents" :key="agent._id" class="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden hover:shadow-md transition">
27
36
  <div class="p-6">
@@ -59,7 +68,6 @@
59
68
  </div>
60
69
  </div>
61
70
 
62
- <!-- Empty State -->
63
71
  <div v-if="agents.length === 0" class="bg-white rounded-xl border-2 border-dashed border-gray-200 p-12 text-center">
64
72
  <i class="ti ti-robot text-6xl text-gray-200 mb-4 inline-block"></i>
65
73
  <h3 class="text-lg font-medium text-gray-800">No agents found</h3>
@@ -68,8 +76,12 @@
68
76
  Create Agent
69
77
  </button>
70
78
  </div>
79
+ </div>
80
+
81
+ <div v-if="activeTab === 'chat'">
82
+ <%- include('partials/admin/agents-chat') %>
83
+ </div>
71
84
 
72
- <!-- Modal -->
73
85
  <div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
74
86
  <div class="bg-white rounded-xl shadow-xl w-full max-w-2xl overflow-hidden max-h-[90vh] flex flex-col">
75
87
  <div class="px-6 py-4 border-b border-gray-100 flex justify-between items-center shrink-0">
@@ -140,7 +152,7 @@
140
152
  </div>
141
153
 
142
154
  <script>
143
- const { createApp, ref, onMounted } = Vue;
155
+ const { createApp, ref, reactive, onMounted, nextTick } = Vue;
144
156
 
145
157
  createApp({
146
158
  setup() {
@@ -149,6 +161,7 @@
149
161
  const showModal = ref(false);
150
162
  const editingAgent = ref(null);
151
163
  const saving = ref(false);
164
+ const activeTab = ref('agents');
152
165
 
153
166
  const availableTools = ['query_database', 'get_system_stats', 'raw_db_query', 'exec'];
154
167
 
@@ -164,24 +177,248 @@
164
177
 
165
178
  const baseUrl = '<%= baseUrl %>';
166
179
 
180
+ const chatScrollEl = ref(null);
181
+ const chat = reactive({
182
+ selectedAgentId: '',
183
+ chatId: '',
184
+ input: '',
185
+ messages: [],
186
+ sessions: [],
187
+ showSessions: false,
188
+ sending: false,
189
+ status: 'idle',
190
+ usage: null,
191
+ isFullscreen: false,
192
+ dbName: ''
193
+ });
194
+
195
+ let activeSseController = null;
196
+
197
+ const selectedAgent = () => agents.value.find(a => a._id === chat.selectedAgentId);
198
+
199
+ const scrollChatToBottom = async () => {
200
+ await nextTick();
201
+ if (chatScrollEl.value) chatScrollEl.value.scrollTop = chatScrollEl.value.scrollHeight;
202
+ };
203
+
167
204
  const fetchAgents = async () => {
168
205
  const res = await fetch(`${baseUrl}/api/admin/agents`);
169
206
  const data = await res.json();
170
207
  agents.value = data.items;
208
+ if (!chat.selectedAgentId && agents.value.length > 0) {
209
+ chat.selectedAgentId = agents.value[0]._id;
210
+ }
211
+ };
212
+
213
+ const fetchChatHealth = async () => {
214
+ try {
215
+ const res = await fetch(`${baseUrl}/api/admin/agents/chat/health`);
216
+ if (!res.ok) return;
217
+ const data = await res.json();
218
+ chat.dbName = data.dbName || '';
219
+ } catch (_) {
220
+ // no-op
221
+ }
222
+ };
223
+
224
+ const newSession = async () => {
225
+ if (!chat.selectedAgentId) return;
226
+ const res = await fetch(`${baseUrl}/api/admin/agents/chat/session/new`, {
227
+ method: 'POST',
228
+ headers: { 'Content-Type': 'application/json' },
229
+ body: JSON.stringify({ agentId: chat.selectedAgentId })
230
+ });
231
+ const data = await res.json();
232
+ if (!res.ok) return alert(data.error || 'Failed to create session');
233
+ chat.chatId = data.chatId;
234
+ chat.messages = [];
235
+ chat.status = 'idle';
236
+ };
237
+
238
+ const loadSessions = async () => {
239
+ if (!chat.selectedAgentId) return;
240
+ const res = await fetch(`${baseUrl}/api/admin/agents/chat/sessions?agentId=${encodeURIComponent(chat.selectedAgentId)}`);
241
+ const data = await res.json();
242
+ if (!res.ok) return alert(data.error || 'Failed to load sessions');
243
+ chat.sessions = data.items || [];
244
+ chat.showSessions = true;
245
+ };
246
+
247
+ const switchSession = async (sessionId) => {
248
+ console.log('[switchSession] Switching to session:', sessionId);
249
+ chat.chatId = sessionId;
250
+ chat.messages = [];
251
+ chat.status = 'loading';
252
+
253
+ try {
254
+ const url = `${baseUrl}/api/admin/agents/chat/session/${encodeURIComponent(sessionId)}/messages`;
255
+ console.log('[switchSession] Fetching from:', url);
256
+ const res = await fetch(url);
257
+ const data = await res.json();
258
+
259
+ console.log('[switchSession] Response status:', res.status, 'data:', data);
260
+
261
+ if (!res.ok) {
262
+ console.error('Failed to load session:', data.error);
263
+ chat.status = 'error';
264
+ return;
265
+ }
266
+
267
+ chat.messages = data.messages || [];
268
+ chat.status = 'idle';
269
+ console.log('[switchSession] Loaded', chat.messages.length, 'messages');
270
+ await scrollChatToBottom();
271
+ } catch (err) {
272
+ console.error('Error loading session:', err);
273
+ chat.status = 'error';
274
+ }
275
+
276
+ chat.showSessions = false;
277
+ };
278
+
279
+ const promptRenameSession = async () => {
280
+ if (!chat.chatId) return;
281
+ const label = prompt('Session label');
282
+ if (!label) return;
283
+ const res = await fetch(`${baseUrl}/api/admin/agents/chat/session/rename`, {
284
+ method: 'POST',
285
+ headers: { 'Content-Type': 'application/json' },
286
+ body: JSON.stringify({ chatId: chat.chatId, label })
287
+ });
288
+ const data = await res.json();
289
+ if (!res.ok) return alert(data.error || 'Failed to rename session');
290
+ await loadSessions();
291
+ };
292
+
293
+ const compactSession = async () => {
294
+ if (!chat.selectedAgentId || !chat.chatId) return;
295
+ const res = await fetch(`${baseUrl}/api/admin/agents/chat/session/compact`, {
296
+ method: 'POST',
297
+ headers: { 'Content-Type': 'application/json' },
298
+ body: JSON.stringify({ agentId: chat.selectedAgentId, chatId: chat.chatId })
299
+ });
300
+ const data = await res.json();
301
+ if (!res.ok) return alert(data.error || 'Failed to compact session');
302
+ alert(`Snapshot created: ${data.snapshotId}`);
303
+ };
304
+
305
+ const stopGeneration = () => {
306
+ if (activeSseController) activeSseController.abort();
307
+ chat.sending = false;
308
+ chat.status = 'stopped';
309
+ };
310
+
311
+ const sendChatMessage = async () => {
312
+ if (!chat.selectedAgentId || !chat.input.trim() || chat.sending) return;
313
+ if (!chat.chatId) await newSession();
314
+ if (!chat.chatId) return;
315
+
316
+ const promptText = chat.input;
317
+ chat.input = '';
318
+ chat.messages.push({ role: 'user', text: promptText });
319
+
320
+ const currentAgent = selectedAgent();
321
+ const assistantMsg = { role: 'assistant', text: '', agentName: currentAgent?.name || 'Agent' };
322
+ chat.messages.push(assistantMsg);
323
+ chat.sending = true;
324
+ chat.status = 'starting';
325
+ await scrollChatToBottom();
326
+
327
+ const controller = new AbortController();
328
+ activeSseController = controller;
329
+
330
+ try {
331
+ const res = await fetch(`${baseUrl}/api/admin/agents/chat/stream`, {
332
+ method: 'POST',
333
+ headers: { 'Content-Type': 'application/json' },
334
+ body: JSON.stringify({
335
+ agentId: chat.selectedAgentId,
336
+ chatId: chat.chatId,
337
+ content: promptText
338
+ }),
339
+ signal: controller.signal,
340
+ });
341
+
342
+ if (!res.ok || !res.body) {
343
+ const data = await res.json().catch(() => ({}));
344
+ throw new Error(data.error || 'Failed to stream response');
345
+ }
346
+
347
+ const reader = res.body.getReader();
348
+ const decoder = new TextDecoder();
349
+ let buffer = '';
350
+
351
+ while (true) {
352
+ const { done, value } = await reader.read();
353
+ if (done) break;
354
+ buffer += decoder.decode(value, { stream: true });
355
+ const chunks = buffer.split('\n\n');
356
+ buffer = chunks.pop() || '';
357
+
358
+ for (const chunk of chunks) {
359
+ const lines = chunk.split('\n');
360
+ let event = 'message';
361
+ let data = '';
362
+ for (const line of lines) {
363
+ if (line.startsWith('event: ')) event = line.slice(7).trim();
364
+ if (line.startsWith('data: ')) data += line.slice(6);
365
+ }
366
+
367
+ let parsed = {};
368
+ try { parsed = JSON.parse(data || '{}'); } catch (_) { parsed = {}; }
369
+
370
+ if (event === 'progress') {
371
+ chat.status = parsed.status || chat.status;
372
+ if (parsed.status === 'streaming_content' && typeof parsed.token === 'string') {
373
+ assistantMsg.text += parsed.token;
374
+ await scrollChatToBottom();
375
+ }
376
+ }
377
+
378
+ if (event === 'done') {
379
+ chat.chatId = parsed.chatId || chat.chatId;
380
+ if (!assistantMsg.text) assistantMsg.text = parsed.text || '';
381
+ chat.usage = parsed.usage
382
+ ? { total: parsed.usage.total_tokens || ((parsed.usage.prompt_tokens || 0) + (parsed.usage.completion_tokens || 0)) }
383
+ : null;
384
+ chat.status = 'done';
385
+ }
386
+
387
+ if (event === 'error') {
388
+ throw new Error(parsed.error || 'Streaming failed');
389
+ }
390
+ }
391
+ }
392
+ } catch (err) {
393
+ if (err.name !== 'AbortError') {
394
+ assistantMsg.text = assistantMsg.text || `Error: ${err.message}`;
395
+ chat.status = 'error';
396
+ }
397
+ } finally {
398
+ if (activeSseController === controller) activeSseController = null;
399
+ chat.sending = false;
400
+ }
401
+ };
402
+
403
+ const onChatInputKeydown = (e) => {
404
+ if (e.key === 'Enter' && !e.shiftKey) {
405
+ e.preventDefault();
406
+ sendChatMessage();
407
+ }
408
+ };
409
+
410
+ const toggleChatFullscreen = () => {
411
+ chat.isFullscreen = !chat.isFullscreen;
171
412
  };
172
413
 
173
414
  const fetchLLMConfig = async () => {
174
- // We need an endpoint to get LLM providers.
175
- // For now, let's try to get it from the LLM admin API if it exists
176
415
  try {
177
416
  const res = await fetch(`${baseUrl}/api/admin/llm/providers`);
178
417
  if (res.ok) {
179
418
  const data = await res.json();
180
419
  providers.value = data.providers || {};
181
420
  }
182
- } catch (e) {
183
- console.warn('Failed to fetch LLM providers');
184
- }
421
+ } catch (e) { console.warn('Failed to fetch LLM providers'); }
185
422
  };
186
423
 
187
424
  const openCreateModal = () => {
@@ -251,6 +488,7 @@
251
488
  onMounted(() => {
252
489
  fetchAgents();
253
490
  fetchLLMConfig();
491
+ fetchChatHealth();
254
492
  });
255
493
 
256
494
  return {
@@ -260,11 +498,23 @@
260
498
  editingAgent,
261
499
  formData,
262
500
  saving,
501
+ activeTab,
263
502
  availableTools,
503
+ chat,
504
+ chatScrollEl,
264
505
  openCreateModal,
265
506
  editAgent,
266
507
  confirmDelete,
267
- saveAgent
508
+ saveAgent,
509
+ newSession,
510
+ loadSessions,
511
+ switchSession,
512
+ promptRenameSession,
513
+ compactSession,
514
+ sendChatMessage,
515
+ onChatInputKeydown,
516
+ stopGeneration,
517
+ toggleChatFullscreen
268
518
  };
269
519
  }
270
520
  }).mount('#app');
@@ -68,6 +68,14 @@
68
68
  </div>
69
69
  </header>
70
70
 
71
+ <!-- Tab Limit Notification -->
72
+ <div v-if="showTabLimitNotification" class="bg-blue-50 border-l-4 border-blue-400 p-4 mx-6 mt-4 rounded-r-lg shadow-sm">
73
+ <div class="flex items-center">
74
+ <i class="ti ti-info-circle-filled text-blue-600 mr-3"></i>
75
+ <p class="text-sm text-blue-800">{{ tabLimitMessage }}</p>
76
+ </div>
77
+ </div>
78
+
71
79
  <div class="flex-1 flex overflow-hidden">
72
80
  <template v-if="!globalZenMode">
73
81
  <%- include('partials/dashboard/sidebar') %>
@@ -109,6 +117,7 @@
109
117
  // Initialize globals from EJS locals first (must run before nav-items)
110
118
  window.BASE_URL = '<%= baseUrl %>';
111
119
  window.ADMIN_PATH = '<%= adminPath %>';
120
+ window.MAX_TABS = <%- typeof maxTabs !== 'undefined' ? maxTabs : 5 %>;
112
121
  </script>
113
122
 
114
123
  <%- include('partials/dashboard/nav-items') %>
@@ -120,6 +129,7 @@
120
129
  setup() {
121
130
  const baseUrl = window.BASE_URL;
122
131
  const adminBase = window.ADMIN_PATH || '/admin';
132
+ const maxTabs = window.MAX_TABS || 5;
123
133
  const navSections = window.NAV_SECTIONS || [];
124
134
 
125
135
  // Tabs state
@@ -130,6 +140,10 @@
130
140
  // User authentication state
131
141
  const currentUser = ref({});
132
142
  const showUserMenu = ref(false);
143
+
144
+ // Tab limit notification state
145
+ const showTabLimitNotification = ref(false);
146
+ const tabLimitMessage = ref('');
133
147
 
134
148
  // ESC ESC to exit Zen Mode
135
149
  let escCount = 0;
@@ -173,12 +187,24 @@
173
187
  availableModuleIds.includes(tab.id)
174
188
  );
175
189
 
176
- if (existingTabs.length > 0) {
190
+ // Enforce tab limit - keep only the first maxTabs tabs
191
+ const limitedTabs = existingTabs.slice(0, maxTabs);
192
+
193
+ if (limitedTabs.length > 0) {
194
+ // Show notification if tabs were trimmed due to limit
195
+ if (existingTabs.length > maxTabs) {
196
+ tabLimitMessage.value = `Loaded ${limitedTabs.length} of ${existingTabs.length} saved tabs (limit: ${maxTabs}).`;
197
+ showTabLimitNotification.value = true;
198
+ setTimeout(() => {
199
+ showTabLimitNotification.value = false;
200
+ }, 4000);
201
+ }
202
+
177
203
  return {
178
- tabs: existingTabs,
179
- activeTabId: existingTabs.some(t => t.id === data.activeTabId)
204
+ tabs: limitedTabs,
205
+ activeTabId: limitedTabs.some(t => t.id === data.activeTabId)
180
206
  ? data.activeTabId
181
- : (existingTabs.length > 0 ? existingTabs[0].id : null)
207
+ : (limitedTabs.length > 0 ? limitedTabs[0].id : null)
182
208
  };
183
209
  }
184
210
  }
@@ -250,11 +276,23 @@
250
276
 
251
277
  if (tabs.length === 0) return null;
252
278
 
279
+ // Enforce tab limit - keep only the first maxTabs tabs
280
+ const limitedTabs = tabs.slice(0, maxTabs);
281
+
282
+ // Show notification if tabs were trimmed due to limit
283
+ if (tabs.length > maxTabs) {
284
+ tabLimitMessage.value = `Loaded ${limitedTabs.length} of ${tabs.length} URL tabs (limit: ${maxTabs}).`;
285
+ showTabLimitNotification.value = true;
286
+ setTimeout(() => {
287
+ showTabLimitNotification.value = false;
288
+ }, 4000);
289
+ }
290
+
253
291
  return {
254
- tabs,
255
- activeTabId: tabs.some(t => t.id === activeTabParam)
292
+ tabs: limitedTabs,
293
+ activeTabId: limitedTabs.some(t => t.id === activeTabParam)
256
294
  ? activeTabParam
257
- : tabs[0].id
295
+ : limitedTabs[0].id
258
296
  };
259
297
  } catch (error) {
260
298
  console.warn('Failed to load tabs from URL:', error);
@@ -300,11 +338,37 @@
300
338
  );
301
339
  });
302
340
 
341
+ // Tab count and limit computed properties
342
+ const tabCount = computed(() => tabs.value.length);
343
+ const isTabLimitReached = computed(() => tabs.value.length >= maxTabs);
344
+ const tabCountDisplay = computed(() => `${tabs.value.length}/${maxTabs} tabs`);
345
+
303
346
  // Tab methods
304
347
  const openTab = (item) => {
305
348
  if (!item || !item.id) return;
306
349
  const existing = tabs.value.find(t => t.id === item.id);
307
350
  if (!existing) {
351
+ // Auto-close oldest tab if limit would be exceeded
352
+ if (tabs.value.length >= maxTabs) {
353
+ const oldestTab = tabs.value[0]; // Leftmost tab
354
+ const wasOldestActive = activeTabId.value === oldestTab.id;
355
+
356
+ // Show notification about auto-closed tab
357
+ tabLimitMessage.value = `Closed "${oldestTab.label}" to make room for "${item.label}".`;
358
+ showTabLimitNotification.value = true;
359
+ setTimeout(() => {
360
+ showTabLimitNotification.value = false;
361
+ }, 3000);
362
+
363
+ // Close the oldest tab
364
+ tabs.value.shift(); // Remove first element
365
+
366
+ // If we closed the active tab and there are still tabs, activate the new first tab
367
+ if (wasOldestActive && tabs.value.length > 0) {
368
+ activeTabId.value = tabs.value[0].id;
369
+ }
370
+ }
371
+
308
372
  tabs.value.push({
309
373
  id: item.id,
310
374
  label: item.label,
@@ -527,7 +591,13 @@
527
591
  selectPaletteItem,
528
592
  selectModule,
529
593
  handleLogout,
530
- getIframeSrc
594
+ getIframeSrc,
595
+ // Tab limit properties
596
+ tabCount,
597
+ isTabLimitReached,
598
+ tabCountDisplay,
599
+ showTabLimitNotification,
600
+ tabLimitMessage
531
601
  };
532
602
  }
533
603
  }).mount('#app');