@intranefr/superbackend 1.5.0 → 1.5.2
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.
- package/.env.example +15 -0
- package/README.md +11 -0
- package/analysis-only.skill +0 -0
- package/index.js +23 -0
- package/package.json +8 -2
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +90 -6
- package/src/controllers/adminBlockDefinitions.controller.js +127 -0
- package/src/controllers/adminBlockDefinitionsAi.controller.js +54 -0
- package/src/controllers/adminCache.controller.js +342 -0
- package/src/controllers/adminContextBlockDefinitions.controller.js +141 -0
- package/src/controllers/adminCrons.controller.js +388 -0
- package/src/controllers/adminDbBrowser.controller.js +124 -0
- package/src/controllers/adminEjsVirtual.controller.js +13 -3
- package/src/controllers/adminExperiments.controller.js +200 -0
- package/src/controllers/adminHeadless.controller.js +9 -2
- package/src/controllers/adminHealthChecks.controller.js +570 -0
- package/src/controllers/adminI18n.controller.js +51 -29
- package/src/controllers/adminLlm.controller.js +126 -2
- package/src/controllers/adminPages.controller.js +720 -0
- package/src/controllers/adminPagesContextBlocksAi.controller.js +54 -0
- package/src/controllers/adminProxy.controller.js +113 -0
- package/src/controllers/adminRateLimits.controller.js +138 -0
- package/src/controllers/adminRbac.controller.js +803 -0
- package/src/controllers/adminScripts.controller.js +126 -4
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/blogAdmin.controller.js +279 -0
- package/src/controllers/blogAiAdmin.controller.js +224 -0
- package/src/controllers/blogAutomationAdmin.controller.js +141 -0
- package/src/controllers/blogInternal.controller.js +26 -0
- package/src/controllers/blogPublic.controller.js +89 -0
- package/src/controllers/experiments.controller.js +85 -0
- package/src/controllers/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/internalExperiments.controller.js +17 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +80 -0
- package/src/helpers/mongooseHelper.js +258 -0
- package/src/helpers/scriptBase.js +230 -0
- package/src/helpers/scriptRunner.js +335 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +810 -48
- package/src/models/BlockDefinition.js +27 -0
- package/src/models/BlogAutomationLock.js +14 -0
- package/src/models/BlogAutomationRun.js +39 -0
- package/src/models/BlogPost.js +42 -0
- package/src/models/CacheEntry.js +26 -0
- package/src/models/ConsoleEntry.js +32 -0
- package/src/models/ConsoleLog.js +23 -0
- package/src/models/ContextBlockDefinition.js +33 -0
- package/src/models/CronExecution.js +47 -0
- package/src/models/CronJob.js +70 -0
- package/src/models/Experiment.js +75 -0
- package/src/models/ExperimentAssignment.js +23 -0
- package/src/models/ExperimentEvent.js +26 -0
- package/src/models/ExperimentMetricBucket.js +30 -0
- package/src/models/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/GlobalSetting.js +1 -2
- package/src/models/HealthAutoHealAttempt.js +57 -0
- package/src/models/HealthCheck.js +132 -0
- package/src/models/HealthCheckRun.js +51 -0
- package/src/models/HealthIncident.js +49 -0
- package/src/models/Page.js +95 -0
- package/src/models/PageCollection.js +42 -0
- package/src/models/ProxyEntry.js +66 -0
- package/src/models/RateLimitCounter.js +19 -0
- package/src/models/RateLimitMetricBucket.js +20 -0
- package/src/models/RbacGrant.js +25 -0
- package/src/models/RbacGroup.js +16 -0
- package/src/models/RbacGroupMember.js +13 -0
- package/src/models/RbacGroupRole.js +13 -0
- package/src/models/RbacRole.js +25 -0
- package/src/models/RbacUserRole.js +13 -0
- package/src/models/ScriptDefinition.js +1 -0
- package/src/models/Webhook.js +2 -0
- package/src/routes/admin.routes.js +2 -0
- package/src/routes/adminBlog.routes.js +21 -0
- package/src/routes/adminBlogAi.routes.js +16 -0
- package/src/routes/adminBlogAutomation.routes.js +27 -0
- package/src/routes/adminCache.routes.js +20 -0
- package/src/routes/adminConsoleManager.routes.js +302 -0
- package/src/routes/adminCrons.routes.js +25 -0
- package/src/routes/adminDbBrowser.routes.js +65 -0
- package/src/routes/adminEjsVirtual.routes.js +2 -1
- package/src/routes/adminExperiments.routes.js +29 -0
- package/src/routes/adminHeadless.routes.js +2 -1
- package/src/routes/adminHealthChecks.routes.js +28 -0
- package/src/routes/adminI18n.routes.js +4 -3
- package/src/routes/adminLlm.routes.js +4 -2
- package/src/routes/adminPages.routes.js +55 -0
- package/src/routes/adminProxy.routes.js +15 -0
- package/src/routes/adminRateLimits.routes.js +17 -0
- package/src/routes/adminRbac.routes.js +38 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminUiComponents.routes.js +2 -1
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -0
- package/src/routes/experiments.routes.js +30 -0
- package/src/routes/fileManager.routes.js +62 -0
- package/src/routes/fileManagerStoragePolicy.routes.js +9 -0
- package/src/routes/healthChecksPublic.routes.js +9 -0
- package/src/routes/internalExperiments.routes.js +15 -0
- package/src/routes/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +1 -0
- package/src/routes/pages.routes.js +123 -0
- package/src/routes/proxy.routes.js +46 -0
- package/src/routes/rbac.routes.js +47 -0
- package/src/routes/webhook.routes.js +2 -1
- package/src/routes/workflows.routes.js +4 -0
- package/src/services/blockDefinitionsAi.service.js +247 -0
- package/src/services/blog.service.js +99 -0
- package/src/services/blogAutomation.service.js +978 -0
- package/src/services/blogCronsBootstrap.service.js +185 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +738 -0
- package/src/services/consoleOverride.service.js +7 -1
- package/src/services/cronScheduler.service.js +350 -0
- package/src/services/dbBrowser.service.js +536 -0
- package/src/services/ejsVirtual.service.js +102 -32
- package/src/services/experiments.service.js +273 -0
- package/src/services/experimentsAggregation.service.js +308 -0
- package/src/services/experimentsCronsBootstrap.service.js +118 -0
- package/src/services/experimentsRetention.service.js +43 -0
- package/src/services/experimentsWs.service.js +134 -0
- package/src/services/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/globalSettings.service.js +15 -0
- package/src/services/healthChecks.service.js +650 -0
- package/src/services/healthChecksBootstrap.service.js +109 -0
- package/src/services/healthChecksScheduler.service.js +106 -0
- package/src/services/jsonConfigs.service.js +2 -2
- package/src/services/llmDefaults.service.js +190 -0
- package/src/services/migrationAssets/s3.js +2 -2
- package/src/services/pages.service.js +602 -0
- package/src/services/pagesContext.service.js +331 -0
- package/src/services/pagesContextBlocksAi.service.js +349 -0
- package/src/services/proxy.service.js +535 -0
- package/src/services/rateLimiter.service.js +623 -0
- package/src/services/rbac.service.js +212 -0
- package/src/services/scriptsRunner.service.js +215 -15
- package/src/services/uiComponentsAi.service.js +6 -19
- package/src/services/workflow.service.js +23 -8
- package/src/utils/orgRoles.js +14 -0
- package/src/utils/rbac/engine.js +60 -0
- package/src/utils/rbac/rightsRegistry.js +33 -0
- package/views/admin-blog-automation.ejs +877 -0
- package/views/admin-blog-edit.ejs +542 -0
- package/views/admin-blog.ejs +399 -0
- package/views/admin-cache.ejs +681 -0
- package/views/admin-console-manager.ejs +680 -0
- package/views/admin-crons.ejs +645 -0
- package/views/admin-dashboard.ejs +28 -8
- package/views/admin-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-experiments.ejs +91 -0
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-health-checks.ejs +725 -0
- package/views/admin-i18n.ejs +59 -5
- package/views/admin-llm.ejs +99 -1
- package/views/admin-organizations.ejs +163 -1
- package/views/admin-pages.ejs +2424 -0
- package/views/admin-proxy.ejs +491 -0
- package/views/admin-rate-limiter.ejs +625 -0
- package/views/admin-rbac.ejs +1331 -0
- package/views/admin-scripts.ejs +597 -3
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-ui-components.ejs +57 -25
- package/views/admin-workflows.ejs +7 -7
- package/views/file-manager.ejs +866 -0
- package/views/pages/blocks/contact.ejs +27 -0
- package/views/pages/blocks/cta.ejs +18 -0
- package/views/pages/blocks/faq.ejs +20 -0
- package/views/pages/blocks/features.ejs +19 -0
- package/views/pages/blocks/hero.ejs +13 -0
- package/views/pages/blocks/html.ejs +5 -0
- package/views/pages/blocks/image.ejs +14 -0
- package/views/pages/blocks/testimonials.ejs +26 -0
- package/views/pages/blocks/text.ejs +10 -0
- package/views/pages/layouts/default.ejs +51 -0
- package/views/pages/layouts/minimal.ejs +42 -0
- package/views/pages/layouts/sidebar.ejs +54 -0
- package/views/pages/partials/footer.ejs +13 -0
- package/views/pages/partials/header.ejs +12 -0
- package/views/pages/partials/sidebar.ejs +8 -0
- package/views/pages/runtime/page.ejs +10 -0
- package/views/pages/templates/article.ejs +20 -0
- package/views/pages/templates/default.ejs +12 -0
- package/views/pages/templates/landing.ejs +14 -0
- package/views/pages/templates/listing.ejs +15 -0
- package/views/partials/admin-image-upload-modal.ejs +221 -0
- package/views/partials/dashboard/nav-items.ejs +12 -0
- package/views/partials/dashboard/palette.ejs +5 -3
- package/views/partials/llm-provider-model-picker.ejs +183 -0
- package/src/routes/llmUi.routes.js +0 -26
package/views/admin-i18n.ejs
CHANGED
|
@@ -113,6 +113,17 @@
|
|
|
113
113
|
<div class="text-xs text-gray-600 mb-1">To</div>
|
|
114
114
|
<input id="entryAiTo" class="border rounded px-3 py-2" readonly />
|
|
115
115
|
</div>
|
|
116
|
+
<div class="flex-1">
|
|
117
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
118
|
+
<%- include('partials/llm-provider-model-picker', {
|
|
119
|
+
providerInputId: 'entryAiProviderKey',
|
|
120
|
+
modelInputId: 'entryAiModel',
|
|
121
|
+
providerLabel: 'Provider',
|
|
122
|
+
modelLabel: 'Model',
|
|
123
|
+
showOpenRouterFetch: true,
|
|
124
|
+
}) %>
|
|
125
|
+
</div>
|
|
126
|
+
</div>
|
|
116
127
|
<div class="flex gap-2">
|
|
117
128
|
<button type="button" onclick="aiTranslateEntry()" class="px-4 py-2 bg-purple-600 text-white rounded hover:bg-purple-700">AI translate</button>
|
|
118
129
|
</div>
|
|
@@ -147,8 +158,15 @@
|
|
|
147
158
|
<input id="aiFilter" class="w-full border rounded px-3 py-2" placeholder="Filter keys..." oninput="renderAiCandidates()" />
|
|
148
159
|
</div>
|
|
149
160
|
<div>
|
|
150
|
-
<
|
|
151
|
-
|
|
161
|
+
<div class="grid grid-cols-1 gap-2">
|
|
162
|
+
<%- include('partials/llm-provider-model-picker', {
|
|
163
|
+
providerInputId: 'aiProviderKey',
|
|
164
|
+
modelInputId: 'aiModel',
|
|
165
|
+
providerLabel: 'Provider',
|
|
166
|
+
modelLabel: 'Model',
|
|
167
|
+
showOpenRouterFetch: true,
|
|
168
|
+
}) %>
|
|
169
|
+
</div>
|
|
152
170
|
</div>
|
|
153
171
|
<div class="flex items-center gap-2 pt-8">
|
|
154
172
|
<input id="aiMissing" type="checkbox" onchange="renderAiCandidates()" />
|
|
@@ -204,7 +222,7 @@
|
|
|
204
222
|
</div>
|
|
205
223
|
|
|
206
224
|
<script>
|
|
207
|
-
const API_BASE = window.location.origin + "<%= baseUrl %>"
|
|
225
|
+
const API_BASE = window.location.origin + "<%= baseUrl || '' %>";
|
|
208
226
|
let allLocales = [];
|
|
209
227
|
let aiResults = [];
|
|
210
228
|
let aiCandidates = [];
|
|
@@ -213,6 +231,22 @@
|
|
|
213
231
|
let aiToLocaleKeySet = null;
|
|
214
232
|
let aiToLocaleKeySetFor = null;
|
|
215
233
|
|
|
234
|
+
function initLlmPickers() {
|
|
235
|
+
if (!window.__llmProviderModelPicker || !window.__llmProviderModelPicker.init) return;
|
|
236
|
+
|
|
237
|
+
window.__llmProviderModelPicker.init({
|
|
238
|
+
apiBase: API_BASE,
|
|
239
|
+
providerInputId: 'aiProviderKey',
|
|
240
|
+
modelInputId: 'aiModel',
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
window.__llmProviderModelPicker.init({
|
|
244
|
+
apiBase: API_BASE,
|
|
245
|
+
providerInputId: 'entryAiProviderKey',
|
|
246
|
+
modelInputId: 'entryAiModel',
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
216
250
|
function showToast(message, type = 'success') {
|
|
217
251
|
const container = document.getElementById('toast-container');
|
|
218
252
|
const toast = document.createElement('div');
|
|
@@ -323,6 +357,7 @@
|
|
|
323
357
|
document.getElementById('entryLocale').disabled = false;
|
|
324
358
|
|
|
325
359
|
document.getElementById('modal').classList.remove('hidden');
|
|
360
|
+
initLlmPickers();
|
|
326
361
|
}
|
|
327
362
|
|
|
328
363
|
function createMissing(key) {
|
|
@@ -341,6 +376,7 @@
|
|
|
341
376
|
document.getElementById('entryLocale').disabled = true;
|
|
342
377
|
|
|
343
378
|
document.getElementById('modal').classList.remove('hidden');
|
|
379
|
+
initLlmPickers();
|
|
344
380
|
}
|
|
345
381
|
|
|
346
382
|
async function aiTranslateEntry() {
|
|
@@ -348,11 +384,19 @@
|
|
|
348
384
|
const fromLocale = document.getElementById('entryAiFrom').value;
|
|
349
385
|
const toLocale = document.getElementById('entryAiTo').value || document.getElementById('entryLocale').value;
|
|
350
386
|
const text = document.getElementById('entryValue').value;
|
|
387
|
+
const providerKey = document.getElementById('entryAiProviderKey')?.value;
|
|
388
|
+
const model = document.getElementById('entryAiModel')?.value;
|
|
351
389
|
|
|
352
390
|
const res = await fetch(`${API_BASE}/api/admin/i18n/ai/translate-text`, {
|
|
353
391
|
method: 'POST',
|
|
354
392
|
headers: { 'Content-Type': 'application/json' },
|
|
355
|
-
body: JSON.stringify({
|
|
393
|
+
body: JSON.stringify({
|
|
394
|
+
fromLocale,
|
|
395
|
+
toLocale,
|
|
396
|
+
text,
|
|
397
|
+
providerKey: providerKey ? String(providerKey).trim() : undefined,
|
|
398
|
+
model: model ? String(model).trim() : undefined,
|
|
399
|
+
})
|
|
356
400
|
});
|
|
357
401
|
|
|
358
402
|
if (!res.ok) {
|
|
@@ -390,6 +434,7 @@
|
|
|
390
434
|
document.getElementById('entryLocale').disabled = true;
|
|
391
435
|
|
|
392
436
|
document.getElementById('modal').classList.remove('hidden');
|
|
437
|
+
initLlmPickers();
|
|
393
438
|
} catch (e) {
|
|
394
439
|
showToast(e.message, 'error');
|
|
395
440
|
}
|
|
@@ -489,6 +534,7 @@
|
|
|
489
534
|
document.getElementById('aiCandidateSummary').textContent = '0 selected';
|
|
490
535
|
|
|
491
536
|
document.getElementById('aiModal').classList.remove('hidden');
|
|
537
|
+
initLlmPickers();
|
|
492
538
|
loadAiCandidates();
|
|
493
539
|
}
|
|
494
540
|
|
|
@@ -634,6 +680,7 @@
|
|
|
634
680
|
try {
|
|
635
681
|
const fromLocale = document.getElementById('aiFrom').value;
|
|
636
682
|
const toLocale = document.getElementById('aiTo').value;
|
|
683
|
+
const providerKey = document.getElementById('aiProviderKey')?.value;
|
|
637
684
|
const model = document.getElementById('aiModel').value;
|
|
638
685
|
const keys = Array.from(aiSelectedKeys);
|
|
639
686
|
|
|
@@ -645,7 +692,14 @@
|
|
|
645
692
|
const res = await fetch(`${API_BASE}/api/admin/i18n/ai/preview`, {
|
|
646
693
|
method: 'POST',
|
|
647
694
|
headers: { 'Content-Type': 'application/json' },
|
|
648
|
-
body: JSON.stringify({
|
|
695
|
+
body: JSON.stringify({
|
|
696
|
+
fromLocale,
|
|
697
|
+
toLocale,
|
|
698
|
+
keys,
|
|
699
|
+
missingOnly: false,
|
|
700
|
+
providerKey: providerKey ? String(providerKey).trim() : undefined,
|
|
701
|
+
model: model ? String(model).trim() : undefined,
|
|
702
|
+
})
|
|
649
703
|
});
|
|
650
704
|
|
|
651
705
|
if (!res.ok) {
|
package/views/admin-llm.ejs
CHANGED
|
@@ -34,6 +34,47 @@
|
|
|
34
34
|
</div>
|
|
35
35
|
|
|
36
36
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 space-y-8">
|
|
37
|
+
<!-- Defaults + Model Suggestions -->
|
|
38
|
+
<div class="bg-white rounded-lg shadow">
|
|
39
|
+
<div class="px-4 py-3 border-b">
|
|
40
|
+
<h2 class="text-lg font-semibold text-gray-900">Centralized Defaults</h2>
|
|
41
|
+
<p class="text-sm text-gray-600">Global and per-system defaults used by AI features (provider/model). Models are suggestions only.</p>
|
|
42
|
+
</div>
|
|
43
|
+
<div class="p-4 space-y-4">
|
|
44
|
+
<div class="flex items-center justify-end">
|
|
45
|
+
<button id="btnSaveCentralDefaults" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm">Save centralized defaults</button>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
48
|
+
<div>
|
|
49
|
+
<label class="block text-xs font-medium mb-1">Global default providerKey</label>
|
|
50
|
+
<input id="defaultsProviderKey" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="e.g. openrouter" />
|
|
51
|
+
</div>
|
|
52
|
+
<div>
|
|
53
|
+
<label class="block text-xs font-medium mb-1">Global default model</label>
|
|
54
|
+
<input id="defaultsModel" type="text" class="w-full border rounded px-2 py-1 text-sm" placeholder="e.g. google/gemini-2.5-flash-lite" />
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
|
59
|
+
<div>
|
|
60
|
+
<div class="flex items-center justify-between mb-1">
|
|
61
|
+
<label class="block text-xs font-medium">System defaults (JSON map)</label>
|
|
62
|
+
</div>
|
|
63
|
+
<textarea id="systemDefaultsJson" rows="8" class="w-full border rounded px-2 py-1 text-sm font-mono" placeholder="{\n "pageBuilder.blocks.generate": { "providerKey": "openrouter", "model": "..." }\n}"></textarea>
|
|
64
|
+
<p class="text-[11px] text-gray-500 mt-1">Stored as <code>llm.systemDefaults</code>.</p>
|
|
65
|
+
</div>
|
|
66
|
+
<div>
|
|
67
|
+
<div class="flex items-center justify-between mb-1">
|
|
68
|
+
<label class="block text-xs font-medium">Provider model suggestions (JSON map)</label>
|
|
69
|
+
<button id="btnFetchOpenRouterModels" class="px-2 py-1 bg-gray-200 text-gray-800 rounded text-xs">Fetch OpenRouter models</button>
|
|
70
|
+
</div>
|
|
71
|
+
<textarea id="providerModelsJson" rows="8" class="w-full border rounded px-2 py-1 text-sm font-mono" placeholder="{\n "openrouter": [\"google/gemini-2.5-flash-lite\"]\n}"></textarea>
|
|
72
|
+
<p class="text-[11px] text-gray-500 mt-1">Stored as <code>llm.providerModels</code>. Suggestions only.</p>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
37
78
|
<!-- Providers + Prompts -->
|
|
38
79
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
39
80
|
<!-- Providers -->
|
|
@@ -388,6 +429,9 @@ console.log(result.content);</pre>
|
|
|
388
429
|
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
389
430
|
let providers = {};
|
|
390
431
|
let prompts = {};
|
|
432
|
+
let defaults = { providerKey: '', model: '' };
|
|
433
|
+
let systemDefaults = {};
|
|
434
|
+
let providerModels = {};
|
|
391
435
|
let auditPage = 1;
|
|
392
436
|
let costPage = 1;
|
|
393
437
|
|
|
@@ -479,6 +523,14 @@ console.log(result.content);</pre>
|
|
|
479
523
|
if (!res.ok) throw new Error(data?.error || 'Failed to load config');
|
|
480
524
|
providers = data.providers || {};
|
|
481
525
|
prompts = data.prompts || {};
|
|
526
|
+
defaults = data.defaults || { providerKey: '', model: '' };
|
|
527
|
+
systemDefaults = data.systemDefaults || {};
|
|
528
|
+
providerModels = data.providerModels || {};
|
|
529
|
+
|
|
530
|
+
document.getElementById('defaultsProviderKey').value = defaults.providerKey || '';
|
|
531
|
+
document.getElementById('defaultsModel').value = defaults.model || '';
|
|
532
|
+
document.getElementById('systemDefaultsJson').value = JSON.stringify(systemDefaults || {}, null, 2);
|
|
533
|
+
document.getElementById('providerModelsJson').value = JSON.stringify(providerModels || {}, null, 2);
|
|
482
534
|
renderProviders();
|
|
483
535
|
renderPrompts();
|
|
484
536
|
} catch (e) {
|
|
@@ -488,10 +540,31 @@ console.log(result.content);</pre>
|
|
|
488
540
|
|
|
489
541
|
async function saveConfig() {
|
|
490
542
|
try {
|
|
543
|
+
defaults = {
|
|
544
|
+
providerKey: document.getElementById('defaultsProviderKey').value.trim(),
|
|
545
|
+
model: document.getElementById('defaultsModel').value.trim(),
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
try {
|
|
549
|
+
const rawSystemDefaults = document.getElementById('systemDefaultsJson').value;
|
|
550
|
+
systemDefaults = rawSystemDefaults && rawSystemDefaults.trim() ? JSON.parse(rawSystemDefaults) : {};
|
|
551
|
+
} catch (e) {
|
|
552
|
+
showToast('Invalid systemDefaults JSON: ' + e.message, 'error');
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
try {
|
|
557
|
+
const rawProviderModels = document.getElementById('providerModelsJson').value;
|
|
558
|
+
providerModels = rawProviderModels && rawProviderModels.trim() ? JSON.parse(rawProviderModels) : {};
|
|
559
|
+
} catch (e) {
|
|
560
|
+
showToast('Invalid providerModels JSON: ' + e.message, 'error');
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
491
564
|
const res = await fetch(`${API_BASE}/api/admin/llm/config`, {
|
|
492
565
|
method: 'POST',
|
|
493
566
|
headers: { 'Content-Type': 'application/json' },
|
|
494
|
-
body: JSON.stringify({ providers, prompts }),
|
|
567
|
+
body: JSON.stringify({ providers, prompts, defaults, systemDefaults, providerModels }),
|
|
495
568
|
});
|
|
496
569
|
const data = await res.json();
|
|
497
570
|
if (!res.ok) throw new Error(data?.error || 'Failed to save config');
|
|
@@ -501,6 +574,28 @@ console.log(result.content);</pre>
|
|
|
501
574
|
}
|
|
502
575
|
}
|
|
503
576
|
|
|
577
|
+
async function fetchOpenRouterModels() {
|
|
578
|
+
try {
|
|
579
|
+
const res = await fetch(`${API_BASE}/api/admin/llm/openrouter/models`);
|
|
580
|
+
const data = await res.json();
|
|
581
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to fetch OpenRouter models');
|
|
582
|
+
const models = Array.isArray(data?.models) ? data.models : [];
|
|
583
|
+
|
|
584
|
+
const rawProviderModels = document.getElementById('providerModelsJson').value;
|
|
585
|
+
let map = {};
|
|
586
|
+
try {
|
|
587
|
+
map = rawProviderModels && rawProviderModels.trim() ? JSON.parse(rawProviderModels) : {};
|
|
588
|
+
} catch (_) {
|
|
589
|
+
map = {};
|
|
590
|
+
}
|
|
591
|
+
map.openrouter = models;
|
|
592
|
+
document.getElementById('providerModelsJson').value = JSON.stringify(map, null, 2);
|
|
593
|
+
showToast('OpenRouter models loaded');
|
|
594
|
+
} catch (e) {
|
|
595
|
+
showToast(e.message, 'error');
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
504
599
|
function applyProviderPresetDefaults() {
|
|
505
600
|
const preset = document.getElementById('providerPreset').value;
|
|
506
601
|
const baseUrlInput = document.getElementById('providerBaseUrl');
|
|
@@ -848,6 +943,9 @@ console.log(result.content);</pre>
|
|
|
848
943
|
document.getElementById('btnSaveProvider').addEventListener('click', createOrUpdateProviderFromForm);
|
|
849
944
|
document.getElementById('providerPreset').addEventListener('change', applyProviderPresetDefaults);
|
|
850
945
|
|
|
946
|
+
document.getElementById('btnFetchOpenRouterModels').addEventListener('click', fetchOpenRouterModels);
|
|
947
|
+
document.getElementById('btnSaveCentralDefaults').addEventListener('click', saveConfig);
|
|
948
|
+
|
|
851
949
|
document.getElementById('btnAddPrompt').addEventListener('click', () => openPromptEditor(null));
|
|
852
950
|
document.getElementById('btnCancelPrompt').addEventListener('click', closePromptEditor);
|
|
853
951
|
document.getElementById('btnSavePrompt').addEventListener('click', createOrUpdatePromptFromForm);
|
|
@@ -142,6 +142,30 @@
|
|
|
142
142
|
<button id="btn-members-refresh" class="bg-gray-100 text-gray-800 px-3 py-2 rounded hover:bg-gray-200" disabled>Refresh</button>
|
|
143
143
|
</div>
|
|
144
144
|
|
|
145
|
+
<!-- Assign User Section -->
|
|
146
|
+
<div class="bg-gray-50 rounded-lg border p-4 mt-4">
|
|
147
|
+
<h4 class="font-semibold text-gray-900">Assign User to Organization</h4>
|
|
148
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-3">
|
|
149
|
+
<div>
|
|
150
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">User *</label>
|
|
151
|
+
<select id="assign-user-select" class="w-full border rounded px-3 py-2">
|
|
152
|
+
<option value="">Loading users...</option>
|
|
153
|
+
</select>
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
157
|
+
<select id="assign-user-role" class="w-full border rounded px-3 py-2">
|
|
158
|
+
<option value="member">member</option>
|
|
159
|
+
<option value="admin">admin</option>
|
|
160
|
+
<option value="viewer">viewer</option>
|
|
161
|
+
</select>
|
|
162
|
+
</div>
|
|
163
|
+
<div class="flex items-end">
|
|
164
|
+
<button id="btn-assign-user" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600" disabled>Assign User</button>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
145
169
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-4">
|
|
146
170
|
<div>
|
|
147
171
|
<label class="block text-sm font-medium text-gray-700 mb-1">Role</label>
|
|
@@ -453,8 +477,138 @@
|
|
|
453
477
|
orgs: { offset: 0, limit: 25, total: 0, selectedOrgId: null },
|
|
454
478
|
members: { offset: 0, limit: 50, total: 0 },
|
|
455
479
|
invites: { offset: 0, limit: 50, total: 0 },
|
|
480
|
+
availableUsers: [],
|
|
456
481
|
};
|
|
457
482
|
|
|
483
|
+
async function loadAvailableUsers() {
|
|
484
|
+
|
|
485
|
+
console.log('Loading available users...');
|
|
486
|
+
|
|
487
|
+
const orgId = state.orgs.selectedOrgId;
|
|
488
|
+
if (!orgId) {
|
|
489
|
+
console.log('No org selected');
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
try {
|
|
494
|
+
// Load all users
|
|
495
|
+
const usersRes = await fetch(`${API_BASE}/api/admin/users`, {
|
|
496
|
+
headers: { 'Accept': 'application/json' },
|
|
497
|
+
credentials: 'same-origin'
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
const usersData = await usersRes.json();
|
|
501
|
+
|
|
502
|
+
if (!usersRes.ok) {
|
|
503
|
+
console.error('Failed to load users:', usersData?.error);
|
|
504
|
+
const select = document.getElementById('assign-user-select');
|
|
505
|
+
if (select) {
|
|
506
|
+
select.innerHTML = `<option value="">Error: ${usersData?.error || 'Unknown error'}</option>`;
|
|
507
|
+
}
|
|
508
|
+
return;
|
|
509
|
+
}else{
|
|
510
|
+
console.log('Loaded users:', usersData);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Load current org members
|
|
514
|
+
const membersRes = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members?limit=500`, {
|
|
515
|
+
headers: { 'Accept': 'application/json' },
|
|
516
|
+
credentials: 'same-origin'
|
|
517
|
+
});
|
|
518
|
+
const membersData = await membersRes.json();
|
|
519
|
+
|
|
520
|
+
if (!membersRes.ok) {
|
|
521
|
+
console.error('Failed to load members:', membersData?.error);
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const allUsers = Array.isArray(usersData?.users) ? usersData.users : [];
|
|
526
|
+
const currentMembers = Array.isArray(membersData?.members) ? membersData.members : [];
|
|
527
|
+
const memberUserIds = new Set(currentMembers.map(m => String(m.userId)));
|
|
528
|
+
|
|
529
|
+
// Filter out users who are already members
|
|
530
|
+
const availableUsers = allUsers.filter(user => !memberUserIds.has(String(user._id)));
|
|
531
|
+
state.availableUsers = availableUsers;
|
|
532
|
+
|
|
533
|
+
// Update the select dropdown
|
|
534
|
+
const select = document.getElementById('assign-user-select');
|
|
535
|
+
if (select) {
|
|
536
|
+
select.innerHTML = '<option value="">Select a user...</option>';
|
|
537
|
+
availableUsers.forEach(user => {
|
|
538
|
+
const option = document.createElement('option');
|
|
539
|
+
option.value = user._id;
|
|
540
|
+
option.textContent = `${user.name || 'No name'} (${user.email})`;
|
|
541
|
+
select.appendChild(option);
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Enable/disable assign button based on availability
|
|
546
|
+
const assignBtn = document.getElementById('btn-assign-user');
|
|
547
|
+
if (assignBtn) {
|
|
548
|
+
assignBtn.disabled = availableUsers.length === 0;
|
|
549
|
+
if (availableUsers.length === 0) {
|
|
550
|
+
select.innerHTML = '<option value="">No available users</option>';
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
} catch (e) {
|
|
554
|
+
console.error('Failed to load available users:', e);
|
|
555
|
+
const select = document.getElementById('assign-user-select');
|
|
556
|
+
if (select) {
|
|
557
|
+
select.innerHTML = '<option value="">Failed to load users</option>';
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
async function assignUserToOrg() {
|
|
563
|
+
const orgId = state.orgs.selectedOrgId;
|
|
564
|
+
const userId = document.getElementById('assign-user-select')?.value;
|
|
565
|
+
const role = document.getElementById('assign-user-role')?.value;
|
|
566
|
+
|
|
567
|
+
if (!orgId) {
|
|
568
|
+
showToast('No organization selected', 'error');
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!userId) {
|
|
573
|
+
showToast('Please select a user', 'error');
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!role) {
|
|
578
|
+
showToast('Please select a role', 'error');
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const userName = state.availableUsers.find(u => u._id === userId)?.name || userId;
|
|
583
|
+
|
|
584
|
+
try {
|
|
585
|
+
const res = await fetch(`${API_BASE}${ORGS_ADMIN_PATH}/${encodeURIComponent(orgId)}/members`, {
|
|
586
|
+
method: 'POST',
|
|
587
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
588
|
+
credentials: 'same-origin',
|
|
589
|
+
body: JSON.stringify({ userId, role }),
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
const data = await res.json();
|
|
593
|
+
|
|
594
|
+
if (!res.ok) {
|
|
595
|
+
showToast(data?.error || 'Failed to assign user', 'error');
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
showToast(data?.message || 'User assigned successfully', 'success');
|
|
600
|
+
|
|
601
|
+
// Reset form
|
|
602
|
+
document.getElementById('assign-user-select').value = '';
|
|
603
|
+
document.getElementById('assign-user-role').value = 'member';
|
|
604
|
+
|
|
605
|
+
// Reload members list and available users
|
|
606
|
+
await Promise.all([loadMembers(), loadAvailableUsers()]);
|
|
607
|
+
} catch (e) {
|
|
608
|
+
showToast(e.message || 'Failed to assign user', 'error');
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
|
|
458
612
|
async function loadOrgs() {
|
|
459
613
|
const q = document.getElementById('orgs-q')?.value?.trim();
|
|
460
614
|
const status = document.getElementById('orgs-status')?.value?.trim();
|
|
@@ -539,6 +693,7 @@
|
|
|
539
693
|
</td>
|
|
540
694
|
<td class="px-4 py-3 text-sm text-gray-700 whitespace-nowrap">
|
|
541
695
|
<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>
|
|
696
|
+
<button class="text-gray-600 hover:text-gray-800 mr-2" data-copy="${escapeHtml(o?._id)}" title="Copy orgId">Copy ID</button>
|
|
542
697
|
${o?.status === 'active'
|
|
543
698
|
? `<button class="text-orange-600 hover:text-orange-800 mr-2" data-disable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Disable</button>`
|
|
544
699
|
: `<button class="text-green-600 hover:text-green-800 mr-2" data-enable="${escapeHtml(o?._id)}" data-name="${escapeHtml(o?.name)}">Enable</button>`
|
|
@@ -567,6 +722,7 @@
|
|
|
567
722
|
const ids = [
|
|
568
723
|
'btn-selected-refresh',
|
|
569
724
|
'btn-members-refresh',
|
|
725
|
+
'btn-assign-user',
|
|
570
726
|
'btn-members-apply',
|
|
571
727
|
'btn-members-reset',
|
|
572
728
|
'btn-members-prev',
|
|
@@ -591,7 +747,7 @@
|
|
|
591
747
|
state.invites.offset = 0;
|
|
592
748
|
|
|
593
749
|
setSelectedControlsEnabled(true);
|
|
594
|
-
await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites()]);
|
|
750
|
+
await Promise.all([loadSelectedOrg(), loadMembers(), loadInvites(), loadAvailableUsers()]);
|
|
595
751
|
await loadOrgs();
|
|
596
752
|
}
|
|
597
753
|
|
|
@@ -1039,6 +1195,9 @@
|
|
|
1039
1195
|
if (e.target.matches('[data-edit]')) {
|
|
1040
1196
|
const btn = e.target;
|
|
1041
1197
|
openEditOrgModal(btn.dataset.edit, btn.dataset.name, btn.dataset.description, btn.dataset.owner, btn.dataset.status);
|
|
1198
|
+
} else if (e.target.matches('[data-copy]')) {
|
|
1199
|
+
const btn = e.target;
|
|
1200
|
+
copyText(btn.dataset.copy);
|
|
1042
1201
|
} else if (e.target.matches('[data-disable]')) {
|
|
1043
1202
|
const btn = e.target;
|
|
1044
1203
|
disableOrganization(btn.dataset.disable, btn.dataset.name);
|
|
@@ -1057,6 +1216,9 @@
|
|
|
1057
1216
|
const membersRefreshBtn = document.getElementById('btn-members-refresh');
|
|
1058
1217
|
if (membersRefreshBtn) membersRefreshBtn.onclick = () => loadMembers();
|
|
1059
1218
|
|
|
1219
|
+
const assignUserBtn = document.getElementById('btn-assign-user');
|
|
1220
|
+
if (assignUserBtn) assignUserBtn.onclick = () => assignUserToOrg();
|
|
1221
|
+
|
|
1060
1222
|
const membersApplyBtn = document.getElementById('btn-members-apply');
|
|
1061
1223
|
if (membersApplyBtn) membersApplyBtn.onclick = () => { state.members.offset = 0; loadMembers(); };
|
|
1062
1224
|
|