@intranefr/superbackend 1.4.4 → 1.5.1
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 +5 -0
- package/README.md +11 -0
- package/index.js +39 -1
- package/package.json +11 -3
- package/public/sdk/ui-components.iife.js +191 -0
- package/sdk/ui-components/browser/src/index.js +228 -0
- package/src/admin/endpointRegistry.js +120 -0
- package/src/controllers/admin.controller.js +111 -5
- 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/adminHeadless.controller.js +91 -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 +320 -0
- package/src/controllers/adminSeoConfig.controller.js +71 -48
- package/src/controllers/adminTerminals.controller.js +39 -0
- package/src/controllers/adminUiComponents.controller.js +315 -0
- package/src/controllers/adminUiComponentsAi.controller.js +34 -0
- 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/fileManager.controller.js +190 -0
- package/src/controllers/fileManagerStoragePolicy.controller.js +23 -0
- package/src/controllers/healthChecksPublic.controller.js +196 -0
- package/src/controllers/metrics.controller.js +64 -4
- package/src/controllers/orgAdmin.controller.js +366 -0
- package/src/controllers/uiComponentsPublic.controller.js +118 -0
- package/src/middleware/auth.js +7 -0
- package/src/middleware/internalCronAuth.js +29 -0
- package/src/middleware/rbac.js +62 -0
- package/src/middleware.js +879 -56
- 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/ExternalDbConnection.js +49 -0
- package/src/models/FileEntry.js +22 -0
- package/src/models/HeadlessModelDefinition.js +10 -0
- 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 +42 -0
- package/src/models/ScriptRun.js +22 -0
- package/src/models/UiComponent.js +29 -0
- package/src/models/UiComponentProject.js +26 -0
- package/src/models/UiComponentProjectComponent.js +18 -0
- package/src/routes/admin.routes.js +1 -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/adminHeadless.routes.js +8 -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/adminScripts.routes.js +21 -0
- package/src/routes/adminSeoConfig.routes.js +5 -4
- package/src/routes/adminTerminals.routes.js +13 -0
- package/src/routes/adminUiComponents.routes.js +30 -0
- package/src/routes/blogInternal.routes.js +14 -0
- package/src/routes/blogPublic.routes.js +9 -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/log.routes.js +43 -60
- package/src/routes/metrics.routes.js +4 -2
- package/src/routes/orgAdmin.routes.js +6 -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/uiComponentsPublic.routes.js +9 -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 +184 -0
- package/src/services/blogPublishing.service.js +58 -0
- package/src/services/cacheLayer.service.js +696 -0
- package/src/services/consoleManager.service.js +700 -0
- package/src/services/consoleOverride.service.js +6 -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/fileManager.service.js +475 -0
- package/src/services/fileManagerStoragePolicy.service.js +285 -0
- package/src/services/headlessExternalModels.service.js +292 -0
- package/src/services/headlessModels.service.js +26 -6
- 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/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 +259 -0
- package/src/services/terminals.service.js +152 -0
- package/src/services/terminalsWs.service.js +100 -0
- package/src/services/uiComponentsAi.service.js +299 -0
- package/src/services/uiComponentsCrypto.service.js +39 -0
- 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 +29 -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-db-browser.ejs +445 -0
- package/views/admin-ejs-virtual.ejs +16 -10
- package/views/admin-file-manager.ejs +942 -0
- package/views/admin-headless.ejs +294 -24
- 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 +528 -10
- 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 +497 -0
- package/views/admin-seo-config.ejs +61 -7
- package/views/admin-terminals.ejs +328 -0
- package/views/admin-ui-components.ejs +741 -0
- package/views/admin-users.ejs +261 -4
- 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 +14 -0
- package/views/partials/llm-provider-model-picker.ejs +183 -0
|
@@ -0,0 +1,497 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Admin Scripts</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body class="bg-gray-50">
|
|
10
|
+
<div class="max-w-7xl mx-auto px-6 py-6">
|
|
11
|
+
<div class="flex items-center justify-between mb-6">
|
|
12
|
+
<div>
|
|
13
|
+
<h1 class="text-2xl font-semibold text-gray-900">Scripts</h1>
|
|
14
|
+
<div class="text-sm text-gray-500">Configure and run Bash / Node / Browser scripts</div>
|
|
15
|
+
</div>
|
|
16
|
+
<div class="flex items-center gap-2">
|
|
17
|
+
<button id="btn-new" class="px-3 py-2 rounded bg-blue-600 text-white text-sm hover:bg-blue-700">New</button>
|
|
18
|
+
<button id="btn-save" class="px-3 py-2 rounded bg-gray-900 text-white text-sm hover:bg-black">Save</button>
|
|
19
|
+
<button id="btn-delete" class="px-3 py-2 rounded bg-red-600 text-white text-sm hover:bg-red-700">Delete</button>
|
|
20
|
+
<button id="btn-run" class="px-3 py-2 rounded bg-emerald-600 text-white text-sm hover:bg-emerald-700">Run</button>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div class="grid grid-cols-12 gap-6">
|
|
25
|
+
<div class="col-span-4">
|
|
26
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
27
|
+
<div class="p-3 border-b border-gray-200 flex items-center justify-between">
|
|
28
|
+
<div class="text-sm font-medium text-gray-800">Scripts</div>
|
|
29
|
+
<button id="btn-refresh" class="text-sm text-blue-600 hover:underline">Refresh</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div id="scripts-list" class="p-2 max-h-[70vh] overflow-auto"></div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div class="mt-4 bg-white border border-gray-200 rounded-lg">
|
|
35
|
+
<div class="p-3 border-b border-gray-200">
|
|
36
|
+
<div class="text-sm font-medium text-gray-800">Runs</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div id="runs-list" class="p-2 max-h-[30vh] overflow-auto"></div>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<div class="col-span-8">
|
|
43
|
+
<div class="bg-white border border-gray-200 rounded-lg">
|
|
44
|
+
<div class="p-4 grid grid-cols-2 gap-4">
|
|
45
|
+
<div>
|
|
46
|
+
<label class="text-xs font-semibold text-gray-600">Name</label>
|
|
47
|
+
<input id="f-name" class="mt-1 w-full border rounded px-3 py-2" placeholder="Update ssh and sync" />
|
|
48
|
+
</div>
|
|
49
|
+
<div>
|
|
50
|
+
<label class="text-xs font-semibold text-gray-600">Code</label>
|
|
51
|
+
<input id="f-code" class="mt-1 w-full border rounded px-3 py-2 font-mono text-sm" placeholder="update-ssh-sync" />
|
|
52
|
+
</div>
|
|
53
|
+
<div class="col-span-2">
|
|
54
|
+
<label class="text-xs font-semibold text-gray-600">Description</label>
|
|
55
|
+
<input id="f-desc" class="mt-1 w-full border rounded px-3 py-2" placeholder="(optional)" />
|
|
56
|
+
</div>
|
|
57
|
+
<div>
|
|
58
|
+
<label class="text-xs font-semibold text-gray-600">Type</label>
|
|
59
|
+
<select id="f-type" class="mt-1 w-full border rounded px-3 py-2">
|
|
60
|
+
<option value="bash">bash</option>
|
|
61
|
+
<option value="node">node</option>
|
|
62
|
+
<option value="browser">browser</option>
|
|
63
|
+
</select>
|
|
64
|
+
</div>
|
|
65
|
+
<div>
|
|
66
|
+
<label class="text-xs font-semibold text-gray-600">Runner</label>
|
|
67
|
+
<select id="f-runner" class="mt-1 w-full border rounded px-3 py-2">
|
|
68
|
+
<option value="host">host</option>
|
|
69
|
+
<option value="vm2">vm2</option>
|
|
70
|
+
<option value="browser">browser</option>
|
|
71
|
+
</select>
|
|
72
|
+
</div>
|
|
73
|
+
<div>
|
|
74
|
+
<label class="text-xs font-semibold text-gray-600">Timeout (ms)</label>
|
|
75
|
+
<input id="f-timeout" type="number" class="mt-1 w-full border rounded px-3 py-2" />
|
|
76
|
+
</div>
|
|
77
|
+
<div>
|
|
78
|
+
<label class="text-xs font-semibold text-gray-600">Working directory</label>
|
|
79
|
+
<input id="f-cwd" class="mt-1 w-full border rounded px-3 py-2 font-mono text-sm" placeholder="(optional)" />
|
|
80
|
+
</div>
|
|
81
|
+
<div class="col-span-2">
|
|
82
|
+
<label class="text-xs font-semibold text-gray-600">Enabled</label>
|
|
83
|
+
<div class="mt-2 flex items-center gap-2">
|
|
84
|
+
<input id="f-enabled" type="checkbox" class="h-4 w-4" />
|
|
85
|
+
<span class="text-sm text-gray-700" title="When disabled, the server will reject any attempt to run this script (UI, API, and scheduled runs).">Script can be run</span>
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="border-t border-gray-200 p-4">
|
|
91
|
+
<div class="flex items-center justify-between mb-2">
|
|
92
|
+
<div class="text-sm font-medium text-gray-800">Environment</div>
|
|
93
|
+
<button id="btn-add-env" class="text-sm text-blue-600 hover:underline">Add</button>
|
|
94
|
+
</div>
|
|
95
|
+
<div class="overflow-auto">
|
|
96
|
+
<table class="w-full text-sm">
|
|
97
|
+
<thead>
|
|
98
|
+
<tr class="text-left text-gray-500">
|
|
99
|
+
<th class="py-1 pr-2">Key</th>
|
|
100
|
+
<th class="py-1 pr-2">Value</th>
|
|
101
|
+
<th class="py-1 w-12"></th>
|
|
102
|
+
</tr>
|
|
103
|
+
</thead>
|
|
104
|
+
<tbody id="env-tbody"></tbody>
|
|
105
|
+
</table>
|
|
106
|
+
</div>
|
|
107
|
+
</div>
|
|
108
|
+
|
|
109
|
+
<div class="border-t border-gray-200 p-4">
|
|
110
|
+
<div class="text-sm font-medium text-gray-800 mb-2">Script</div>
|
|
111
|
+
<textarea id="f-script" class="w-full h-56 border rounded px-3 py-2 font-mono text-sm" placeholder="#!/usr/bin/env bash\n..."></textarea>
|
|
112
|
+
<div id="runner-warning" class="mt-2 text-xs text-amber-700"></div>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
|
|
116
|
+
<div class="mt-4 bg-white border border-gray-200 rounded-lg">
|
|
117
|
+
<div class="p-3 border-b border-gray-200 flex items-center justify-between">
|
|
118
|
+
<div class="text-sm font-medium text-gray-800">Output</div>
|
|
119
|
+
<button id="btn-clear-output" class="text-sm text-gray-600 hover:underline">Clear</button>
|
|
120
|
+
</div>
|
|
121
|
+
<pre id="output" class="p-3 text-xs font-mono whitespace-pre-wrap max-h-[40vh] overflow-auto"></pre>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<script>
|
|
128
|
+
window.BASE_URL = '<%= baseUrl %>';
|
|
129
|
+
window.ADMIN_PATH = '<%= adminPath %>';
|
|
130
|
+
|
|
131
|
+
const state = {
|
|
132
|
+
scripts: [],
|
|
133
|
+
runs: [],
|
|
134
|
+
selected: null,
|
|
135
|
+
selectedId: null,
|
|
136
|
+
es: null,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function qs(id) {
|
|
140
|
+
return document.getElementById(id);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function toast(msg) {
|
|
144
|
+
alert(msg);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function api(path, opts) {
|
|
148
|
+
const baseUrl = window.BASE_URL || '';
|
|
149
|
+
const url = baseUrl + path;
|
|
150
|
+
const res = await fetch(url, {
|
|
151
|
+
credentials: 'same-origin',
|
|
152
|
+
headers: {
|
|
153
|
+
'Content-Type': 'application/json',
|
|
154
|
+
},
|
|
155
|
+
...opts,
|
|
156
|
+
});
|
|
157
|
+
const json = await res.json().catch(() => ({}));
|
|
158
|
+
if (!res.ok) {
|
|
159
|
+
throw new Error(json.error || 'Request failed');
|
|
160
|
+
}
|
|
161
|
+
return json;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function setOutput(text, append = false) {
|
|
165
|
+
const el = qs('output');
|
|
166
|
+
if (!append) el.textContent = '';
|
|
167
|
+
el.textContent += String(text || '');
|
|
168
|
+
el.scrollTop = el.scrollHeight;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function setRunnerWarning() {
|
|
172
|
+
const type = qs('f-type').value;
|
|
173
|
+
const runner = qs('f-runner').value;
|
|
174
|
+
const el = qs('runner-warning');
|
|
175
|
+
let msg = '';
|
|
176
|
+
if (type === 'bash' && runner === 'host') {
|
|
177
|
+
msg = 'Warning: bash host runner executes on the server host.';
|
|
178
|
+
}
|
|
179
|
+
if (type === 'node' && runner === 'host') {
|
|
180
|
+
msg = 'Warning: node host runner executes on the server host.';
|
|
181
|
+
}
|
|
182
|
+
if (type === 'node' && runner === 'vm2') {
|
|
183
|
+
msg = 'vm2 mode is best-effort and does not support arbitrary Node APIs.';
|
|
184
|
+
}
|
|
185
|
+
if (type === 'browser') {
|
|
186
|
+
msg = 'Browser scripts run in this page only.';
|
|
187
|
+
}
|
|
188
|
+
el.textContent = msg;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function normalizeRunnerOptions() {
|
|
192
|
+
const type = qs('f-type').value;
|
|
193
|
+
const runnerSelect = qs('f-runner');
|
|
194
|
+
const runner = runnerSelect.value;
|
|
195
|
+
const allowed =
|
|
196
|
+
type === 'bash' ? ['host'] :
|
|
197
|
+
type === 'node' ? ['host', 'vm2'] :
|
|
198
|
+
['browser'];
|
|
199
|
+
|
|
200
|
+
runnerSelect.innerHTML = allowed
|
|
201
|
+
.map((v) => `<option value="${v}">${v}</option>`)
|
|
202
|
+
.join('');
|
|
203
|
+
|
|
204
|
+
if (allowed.includes(runner)) runnerSelect.value = runner;
|
|
205
|
+
setRunnerWarning();
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function currentPayload() {
|
|
209
|
+
const env = [];
|
|
210
|
+
qs('env-tbody').querySelectorAll('tr').forEach((tr) => {
|
|
211
|
+
const key = tr.querySelector('.env-key').value.trim();
|
|
212
|
+
const value = tr.querySelector('.env-val').value;
|
|
213
|
+
if (!key) return;
|
|
214
|
+
env.push({ key, value });
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
name: qs('f-name').value.trim(),
|
|
219
|
+
codeIdentifier: qs('f-code').value.trim(),
|
|
220
|
+
description: qs('f-desc').value,
|
|
221
|
+
type: qs('f-type').value,
|
|
222
|
+
runner: qs('f-runner').value,
|
|
223
|
+
timeoutMs: Number(qs('f-timeout').value || 0) || undefined,
|
|
224
|
+
defaultWorkingDirectory: qs('f-cwd').value,
|
|
225
|
+
enabled: !!qs('f-enabled').checked,
|
|
226
|
+
env,
|
|
227
|
+
script: qs('f-script').value,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function renderEnv(env) {
|
|
232
|
+
const tbody = qs('env-tbody');
|
|
233
|
+
tbody.innerHTML = '';
|
|
234
|
+
const items = Array.isArray(env) ? env : [];
|
|
235
|
+
for (const item of items) {
|
|
236
|
+
const tr = document.createElement('tr');
|
|
237
|
+
tr.innerHTML = `
|
|
238
|
+
<td class="py-1 pr-2"><input class="env-key w-full border rounded px-2 py-1 font-mono text-xs" value="${String(item.key || '').replace(/</g,'<')}" /></td>
|
|
239
|
+
<td class="py-1 pr-2"><input class="env-val w-full border rounded px-2 py-1 font-mono text-xs" value="${String(item.value || '').replace(/</g,'<')}" /></td>
|
|
240
|
+
<td class="py-1 text-right"><button class="btn-del-env text-red-600 hover:underline text-xs">Del</button></td>
|
|
241
|
+
`;
|
|
242
|
+
tbody.appendChild(tr);
|
|
243
|
+
tr.querySelector('.btn-del-env').addEventListener('click', () => {
|
|
244
|
+
tr.remove();
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function clearForm() {
|
|
250
|
+
state.selectedId = null;
|
|
251
|
+
state.selected = { enabled: true, timeoutMs: 300000, type: 'bash', runner: 'host', env: [] };
|
|
252
|
+
qs('f-name').value = '';
|
|
253
|
+
qs('f-code').value = '';
|
|
254
|
+
qs('f-desc').value = '';
|
|
255
|
+
qs('f-type').value = 'bash';
|
|
256
|
+
normalizeRunnerOptions();
|
|
257
|
+
qs('f-timeout').value = '300000';
|
|
258
|
+
qs('f-cwd').value = '';
|
|
259
|
+
qs('f-enabled').checked = true;
|
|
260
|
+
qs('f-script').value = '';
|
|
261
|
+
renderEnv([]);
|
|
262
|
+
renderRuns();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function setFormFromScript(s) {
|
|
266
|
+
state.selectedId = s._id;
|
|
267
|
+
state.selected = s;
|
|
268
|
+
qs('f-name').value = s.name || '';
|
|
269
|
+
qs('f-code').value = s.codeIdentifier || '';
|
|
270
|
+
qs('f-desc').value = s.description || '';
|
|
271
|
+
qs('f-type').value = s.type || 'bash';
|
|
272
|
+
normalizeRunnerOptions();
|
|
273
|
+
qs('f-runner').value = s.runner || (s.type === 'bash' ? 'host' : 'host');
|
|
274
|
+
qs('f-timeout').value = String(s.timeoutMs || 300000);
|
|
275
|
+
qs('f-cwd').value = s.defaultWorkingDirectory || '';
|
|
276
|
+
qs('f-enabled').checked = !!s.enabled;
|
|
277
|
+
qs('f-script').value = s.script || '';
|
|
278
|
+
renderEnv(s.env || []);
|
|
279
|
+
loadRuns();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function renderScripts() {
|
|
283
|
+
const list = qs('scripts-list');
|
|
284
|
+
list.innerHTML = '';
|
|
285
|
+
if (!state.scripts.length) {
|
|
286
|
+
list.innerHTML = '<div class="text-sm text-gray-500 p-2">No scripts</div>';
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
state.scripts.forEach((s) => {
|
|
290
|
+
const btn = document.createElement('button');
|
|
291
|
+
const active = state.selectedId === s._id;
|
|
292
|
+
btn.className = `w-full text-left px-3 py-2 rounded border ${active ? 'bg-blue-50 border-blue-200' : 'bg-white border-gray-200 hover:bg-gray-50'} mb-2`;
|
|
293
|
+
btn.innerHTML = `
|
|
294
|
+
<div class="flex items-center justify-between gap-2">
|
|
295
|
+
<div class="font-medium text-gray-900">${String(s.name || '').replace(/</g,'<')}</div>
|
|
296
|
+
<div class="text-[10px] uppercase bg-gray-200 text-gray-800 px-2 py-0.5 rounded">${String(s.type || '').replace(/</g,'<')}</div>
|
|
297
|
+
</div>
|
|
298
|
+
<div class="text-xs text-gray-500 font-mono">${String(s.codeIdentifier || '').replace(/</g,'<')} · ${String(s.runner || '').replace(/</g,'<')}</div>
|
|
299
|
+
`;
|
|
300
|
+
btn.addEventListener('click', () => setFormFromScript(s));
|
|
301
|
+
list.appendChild(btn);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function renderRuns() {
|
|
306
|
+
const list = qs('runs-list');
|
|
307
|
+
list.innerHTML = '';
|
|
308
|
+
if (!state.runs.length) {
|
|
309
|
+
list.innerHTML = '<div class="text-sm text-gray-500 p-2">No runs</div>';
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
state.runs.forEach((r) => {
|
|
313
|
+
const btn = document.createElement('button');
|
|
314
|
+
btn.className = 'w-full text-left px-3 py-2 rounded border bg-white border-gray-200 hover:bg-gray-50 mb-2';
|
|
315
|
+
btn.innerHTML = `
|
|
316
|
+
<div class="flex items-center justify-between">
|
|
317
|
+
<div class="text-sm font-medium">${String(r.status || '').replace(/</g,'<')}</div>
|
|
318
|
+
<div class="text-xs text-gray-500">${r.exitCode === null || r.exitCode === undefined ? '' : 'exit ' + r.exitCode}</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="text-xs text-gray-500 font-mono">${String(r._id || '').slice(0, 10)} · ${String(r.createdAt || '').replace(/</g,'<')}</div>
|
|
321
|
+
`;
|
|
322
|
+
btn.addEventListener('click', () => {
|
|
323
|
+
setOutput('');
|
|
324
|
+
if (r.outputTail) setOutput(r.outputTail, true);
|
|
325
|
+
});
|
|
326
|
+
list.appendChild(btn);
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
async function loadScripts() {
|
|
331
|
+
const json = await api('/api/admin/scripts');
|
|
332
|
+
state.scripts = json.items || [];
|
|
333
|
+
renderScripts();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function loadRuns() {
|
|
337
|
+
if (!state.selectedId) {
|
|
338
|
+
state.runs = [];
|
|
339
|
+
renderRuns();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const json = await api('/api/admin/scripts/runs?scriptId=' + encodeURIComponent(state.selectedId));
|
|
343
|
+
state.runs = json.items || [];
|
|
344
|
+
renderRuns();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function saveScript() {
|
|
348
|
+
const payload = currentPayload();
|
|
349
|
+
if (!payload.name) throw new Error('name is required');
|
|
350
|
+
if (!payload.codeIdentifier) throw new Error('codeIdentifier is required');
|
|
351
|
+
if (!payload.type) throw new Error('type is required');
|
|
352
|
+
if (!payload.runner) throw new Error('runner is required');
|
|
353
|
+
|
|
354
|
+
if (payload.type === 'bash' && payload.runner !== 'host') {
|
|
355
|
+
throw new Error('bash runner must be host');
|
|
356
|
+
}
|
|
357
|
+
if (payload.type === 'browser' && payload.runner !== 'browser') {
|
|
358
|
+
throw new Error('browser runner must be browser');
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (!state.selectedId) {
|
|
362
|
+
const res = await api('/api/admin/scripts', { method: 'POST', body: JSON.stringify(payload) });
|
|
363
|
+
toast('Created');
|
|
364
|
+
await loadScripts();
|
|
365
|
+
setFormFromScript(res.item);
|
|
366
|
+
} else {
|
|
367
|
+
const res = await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId), { method: 'PUT', body: JSON.stringify(payload) });
|
|
368
|
+
toast('Saved');
|
|
369
|
+
await loadScripts();
|
|
370
|
+
setFormFromScript(res.item);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function deleteScript() {
|
|
375
|
+
if (!state.selectedId) return;
|
|
376
|
+
if (!confirm('Delete script?')) return;
|
|
377
|
+
await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId), { method: 'DELETE' });
|
|
378
|
+
toast('Deleted');
|
|
379
|
+
clearForm();
|
|
380
|
+
await loadScripts();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
function closeStream() {
|
|
384
|
+
if (state.es) {
|
|
385
|
+
try { state.es.close(); } catch {}
|
|
386
|
+
}
|
|
387
|
+
state.es = null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function startSse(runId) {
|
|
391
|
+
closeStream();
|
|
392
|
+
const baseUrl = window.BASE_URL || '';
|
|
393
|
+
const url = baseUrl + '/api/admin/scripts/runs/' + encodeURIComponent(runId) + '/stream';
|
|
394
|
+
const es = new EventSource(url);
|
|
395
|
+
state.es = es;
|
|
396
|
+
|
|
397
|
+
const onLog = (e) => {
|
|
398
|
+
const data = JSON.parse(e.data || '{}');
|
|
399
|
+
setOutput(data.line || '', true);
|
|
400
|
+
};
|
|
401
|
+
const onStatus = (e) => {
|
|
402
|
+
const data = JSON.parse(e.data || '{}');
|
|
403
|
+
setOutput(`\n[status] ${data.status}${data.exitCode === undefined ? '' : ' (exit ' + data.exitCode + ')'}\n`, true);
|
|
404
|
+
loadRuns();
|
|
405
|
+
};
|
|
406
|
+
const onDone = () => {
|
|
407
|
+
closeStream();
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
es.addEventListener('log', onLog);
|
|
411
|
+
es.addEventListener('status', onStatus);
|
|
412
|
+
es.addEventListener('done', onDone);
|
|
413
|
+
es.onerror = () => {
|
|
414
|
+
setOutput('\n[stream] disconnected\n', true);
|
|
415
|
+
closeStream();
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async function runScript() {
|
|
420
|
+
const payload = currentPayload();
|
|
421
|
+
|
|
422
|
+
if (payload.type === 'browser') {
|
|
423
|
+
setOutput('');
|
|
424
|
+
const originalLog = console.log;
|
|
425
|
+
const originalErr = console.error;
|
|
426
|
+
try {
|
|
427
|
+
console.log = (...args) => setOutput(args.join(' ') + '\n', true);
|
|
428
|
+
console.error = (...args) => setOutput(args.join(' ') + '\n', true);
|
|
429
|
+
const fn = new Function(payload.script);
|
|
430
|
+
fn();
|
|
431
|
+
setOutput('\n[done] ok\n', true);
|
|
432
|
+
} finally {
|
|
433
|
+
console.log = originalLog;
|
|
434
|
+
console.error = originalErr;
|
|
435
|
+
}
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (!state.selectedId) {
|
|
440
|
+
await saveScript();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
setOutput('');
|
|
444
|
+
const res = await api('/api/admin/scripts/' + encodeURIComponent(state.selectedId) + '/run', { method: 'POST' });
|
|
445
|
+
startSse(res.runId);
|
|
446
|
+
await loadRuns();
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
qs('btn-refresh').addEventListener('click', loadScripts);
|
|
450
|
+
qs('btn-new').addEventListener('click', clearForm);
|
|
451
|
+
qs('btn-save').addEventListener('click', async () => {
|
|
452
|
+
try {
|
|
453
|
+
await saveScript();
|
|
454
|
+
} catch (e) {
|
|
455
|
+
toast(e.message);
|
|
456
|
+
}
|
|
457
|
+
});
|
|
458
|
+
qs('btn-delete').addEventListener('click', async () => {
|
|
459
|
+
try {
|
|
460
|
+
await deleteScript();
|
|
461
|
+
} catch (e) {
|
|
462
|
+
toast(e.message);
|
|
463
|
+
}
|
|
464
|
+
});
|
|
465
|
+
qs('btn-run').addEventListener('click', async () => {
|
|
466
|
+
try {
|
|
467
|
+
await runScript();
|
|
468
|
+
} catch (e) {
|
|
469
|
+
toast(e.message);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
qs('btn-clear-output').addEventListener('click', () => setOutput(''));
|
|
473
|
+
qs('btn-add-env').addEventListener('click', () => {
|
|
474
|
+
const tbody = qs('env-tbody');
|
|
475
|
+
const tr = document.createElement('tr');
|
|
476
|
+
tr.innerHTML = `
|
|
477
|
+
<td class="py-1 pr-2"><input class="env-key w-full border rounded px-2 py-1 font-mono text-xs" /></td>
|
|
478
|
+
<td class="py-1 pr-2"><input class="env-val w-full border rounded px-2 py-1 font-mono text-xs" /></td>
|
|
479
|
+
<td class="py-1 text-right"><button class="btn-del-env text-red-600 hover:underline text-xs">Del</button></td>
|
|
480
|
+
`;
|
|
481
|
+
tbody.appendChild(tr);
|
|
482
|
+
tr.querySelector('.btn-del-env').addEventListener('click', () => tr.remove());
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
qs('f-type').addEventListener('change', () => {
|
|
486
|
+
normalizeRunnerOptions();
|
|
487
|
+
});
|
|
488
|
+
qs('f-runner').addEventListener('change', setRunnerWarning);
|
|
489
|
+
|
|
490
|
+
(async function init() {
|
|
491
|
+
clearForm();
|
|
492
|
+
normalizeRunnerOptions();
|
|
493
|
+
await loadScripts();
|
|
494
|
+
})();
|
|
495
|
+
</script>
|
|
496
|
+
</body>
|
|
497
|
+
</html>
|
|
@@ -102,6 +102,15 @@
|
|
|
102
102
|
<label class="block text-sm font-medium">Route path</label>
|
|
103
103
|
<input id="seo-ai-route" class="mt-1 w-full border rounded px-3 py-2 text-sm" placeholder="/marketplace" />
|
|
104
104
|
</div>
|
|
105
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
106
|
+
<%- include('partials/llm-provider-model-picker', {
|
|
107
|
+
providerInputId: 'seoAiProviderKey',
|
|
108
|
+
modelInputId: 'seoAiModel',
|
|
109
|
+
providerLabel: 'Provider',
|
|
110
|
+
modelLabel: 'Model',
|
|
111
|
+
showOpenRouterFetch: true,
|
|
112
|
+
}) %>
|
|
113
|
+
</div>
|
|
105
114
|
<div class="flex gap-2">
|
|
106
115
|
<button class="px-3 py-2 bg-purple-600 text-white rounded hover:bg-purple-700 text-sm" onclick="seoAiGenerateFromView()">Generate entry</button>
|
|
107
116
|
<button class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm" onclick="seoAiApplyProposed()">Apply</button>
|
|
@@ -124,6 +133,15 @@
|
|
|
124
133
|
<label class="block text-sm font-medium">Instruction</label>
|
|
125
134
|
<input id="seo-ai-instruction" class="mt-1 w-full border rounded px-3 py-2 text-sm" placeholder="Improve description including desktop apps" />
|
|
126
135
|
</div>
|
|
136
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
137
|
+
<%- include('partials/llm-provider-model-picker', {
|
|
138
|
+
providerInputId: 'seoAiImproveProviderKey',
|
|
139
|
+
modelInputId: 'seoAiImproveModel',
|
|
140
|
+
providerLabel: 'Provider',
|
|
141
|
+
modelLabel: 'Model',
|
|
142
|
+
showOpenRouterFetch: true,
|
|
143
|
+
}) %>
|
|
144
|
+
</div>
|
|
127
145
|
<div class="flex gap-2">
|
|
128
146
|
<label class="inline-flex items-center gap-2 text-sm text-gray-700">
|
|
129
147
|
<input id="seo-ai-robots-noindex" type="checkbox" class="rounded border-gray-300" />
|
|
@@ -219,9 +237,16 @@
|
|
|
219
237
|
</div>
|
|
220
238
|
|
|
221
239
|
<div class="mb-3">
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-2">
|
|
241
|
+
<%- include('partials/llm-provider-model-picker', {
|
|
242
|
+
providerInputId: 'ogAiProviderKey',
|
|
243
|
+
modelInputId: 'ai-model',
|
|
244
|
+
providerLabel: 'Provider',
|
|
245
|
+
modelLabel: 'Model (optional override)',
|
|
246
|
+
showOpenRouterFetch: true,
|
|
247
|
+
}) %>
|
|
248
|
+
</div>
|
|
249
|
+
<div class="mt-1 text-xs text-gray-600">Legacy OpenRouter keys (<code>seoconfig.ai.openrouter.apiKey</code> / <code>ai.openrouter.apiKey</code>) are still supported when provider is <code>openrouter</code>.</div>
|
|
225
250
|
</div>
|
|
226
251
|
|
|
227
252
|
<div class="flex justify-end gap-2">
|
|
@@ -233,7 +258,15 @@
|
|
|
233
258
|
</div>
|
|
234
259
|
|
|
235
260
|
<script>
|
|
236
|
-
const API_BASE = window.location.origin + "<%= baseUrl %>";
|
|
261
|
+
const API_BASE = window.location.origin + "<%= baseUrl || '' %>";
|
|
262
|
+
|
|
263
|
+
function initLlmPickers() {
|
|
264
|
+
if (!window.__llmProviderModelPicker || !window.__llmProviderModelPicker.init) return;
|
|
265
|
+
|
|
266
|
+
window.__llmProviderModelPicker.init({ apiBase: API_BASE, providerInputId: 'seoAiProviderKey', modelInputId: 'seoAiModel' });
|
|
267
|
+
window.__llmProviderModelPicker.init({ apiBase: API_BASE, providerInputId: 'seoAiImproveProviderKey', modelInputId: 'seoAiImproveModel' });
|
|
268
|
+
window.__llmProviderModelPicker.init({ apiBase: API_BASE, providerInputId: 'ogAiProviderKey', modelInputId: 'ai-model' });
|
|
269
|
+
}
|
|
237
270
|
|
|
238
271
|
function showToast(message, type = 'success') {
|
|
239
272
|
const container = document.getElementById('toast-container');
|
|
@@ -409,10 +442,17 @@
|
|
|
409
442
|
try {
|
|
410
443
|
const viewPath = document.getElementById('seo-ai-view').value;
|
|
411
444
|
const routePath = document.getElementById('seo-ai-route').value;
|
|
445
|
+
const providerKey = document.getElementById('seoAiProviderKey')?.value;
|
|
446
|
+
const model = document.getElementById('seoAiModel')?.value;
|
|
412
447
|
const res = await fetch(`${API_BASE}/api/admin/seo-config/ai/generate-entry`, {
|
|
413
448
|
method: 'POST',
|
|
414
449
|
headers: { 'Content-Type': 'application/json' },
|
|
415
|
-
body: JSON.stringify({
|
|
450
|
+
body: JSON.stringify({
|
|
451
|
+
viewPath,
|
|
452
|
+
routePath,
|
|
453
|
+
providerKey: providerKey ? String(providerKey).trim() : undefined,
|
|
454
|
+
model: model ? String(model).trim() : undefined,
|
|
455
|
+
}),
|
|
416
456
|
});
|
|
417
457
|
const data = await res.json().catch(() => ({}));
|
|
418
458
|
if (!res.ok) throw new Error(data?.error || 'Failed to generate entry');
|
|
@@ -432,10 +472,17 @@
|
|
|
432
472
|
try {
|
|
433
473
|
const routePath = document.getElementById('seo-ai-existing-route').value;
|
|
434
474
|
const instruction = document.getElementById('seo-ai-instruction').value;
|
|
475
|
+
const providerKey = document.getElementById('seoAiImproveProviderKey')?.value;
|
|
476
|
+
const model = document.getElementById('seoAiImproveModel')?.value;
|
|
435
477
|
const res = await fetch(`${API_BASE}/api/admin/seo-config/ai/improve-entry`, {
|
|
436
478
|
method: 'POST',
|
|
437
479
|
headers: { 'Content-Type': 'application/json' },
|
|
438
|
-
body: JSON.stringify({
|
|
480
|
+
body: JSON.stringify({
|
|
481
|
+
routePath,
|
|
482
|
+
instruction,
|
|
483
|
+
providerKey: providerKey ? String(providerKey).trim() : undefined,
|
|
484
|
+
model: model ? String(model).trim() : undefined,
|
|
485
|
+
}),
|
|
439
486
|
});
|
|
440
487
|
const data = await res.json().catch(() => ({}));
|
|
441
488
|
if (!res.ok) throw new Error(data?.error || 'Failed to improve entry');
|
|
@@ -617,6 +664,7 @@
|
|
|
617
664
|
document.getElementById('ai-instruction').value = '';
|
|
618
665
|
document.getElementById('ai-model').value = '';
|
|
619
666
|
document.getElementById('ai-modal').classList.remove('hidden');
|
|
667
|
+
initLlmPickers();
|
|
620
668
|
}
|
|
621
669
|
|
|
622
670
|
function closeAiModal() {
|
|
@@ -634,11 +682,17 @@
|
|
|
634
682
|
const svgRaw = document.getElementById('og-svg-raw').value;
|
|
635
683
|
const instruction = document.getElementById('ai-instruction').value;
|
|
636
684
|
const model = document.getElementById('ai-model').value;
|
|
685
|
+
const providerKey = document.getElementById('ogAiProviderKey')?.value;
|
|
637
686
|
|
|
638
687
|
const res = await fetch(`${API_BASE}/api/admin/seo-config/ai/edit-svg`, {
|
|
639
688
|
method: 'POST',
|
|
640
689
|
headers: { 'Content-Type': 'application/json' },
|
|
641
|
-
body: JSON.stringify({
|
|
690
|
+
body: JSON.stringify({
|
|
691
|
+
svgRaw,
|
|
692
|
+
instruction,
|
|
693
|
+
providerKey: providerKey ? String(providerKey).trim() : undefined,
|
|
694
|
+
model: model || undefined,
|
|
695
|
+
}),
|
|
642
696
|
});
|
|
643
697
|
const data = await res.json().catch(() => ({}));
|
|
644
698
|
if (!res.ok) throw new Error(data?.error || 'AI failed');
|