@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,625 @@
|
|
|
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
|
+
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
|
7
|
+
<meta http-equiv="Pragma" content="no-cache" />
|
|
8
|
+
<meta http-equiv="Expires" content="0" />
|
|
9
|
+
<title>Rate Limiter - Admin</title>
|
|
10
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
11
|
+
<style>
|
|
12
|
+
.toast { animation: slideIn 0.3s ease-out; }
|
|
13
|
+
@keyframes slideIn {
|
|
14
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
15
|
+
to { transform: translateX(0); opacity: 1; }
|
|
16
|
+
}
|
|
17
|
+
.fade-out { animation: fadeOut 0.3s ease-out forwards; }
|
|
18
|
+
@keyframes fadeOut {
|
|
19
|
+
from { opacity: 1; }
|
|
20
|
+
to { opacity: 0; }
|
|
21
|
+
}
|
|
22
|
+
.tab-active { background: #1d4ed8; color: white; }
|
|
23
|
+
.tab-inactive { background: #e5e7eb; color: #111827; }
|
|
24
|
+
</style>
|
|
25
|
+
</head>
|
|
26
|
+
<body class="bg-gray-100">
|
|
27
|
+
<div class="min-h-screen">
|
|
28
|
+
<div class="bg-white shadow">
|
|
29
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
30
|
+
<div class="flex justify-between items-center">
|
|
31
|
+
<div>
|
|
32
|
+
<h1 class="text-2xl font-bold text-gray-900">Rate Limiter</h1>
|
|
33
|
+
<p class="text-sm text-gray-600 mt-1">Manage limiter rules (advanced) and view metrics.</p>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
40
|
+
<div class="flex gap-2 mb-6">
|
|
41
|
+
<button id="tab-rate-limits" class="px-4 py-2 rounded tab-active">Rate limits</button>
|
|
42
|
+
<button id="tab-metrics" class="px-4 py-2 rounded tab-inactive">Metrics</button>
|
|
43
|
+
<button id="tab-usage" class="px-4 py-2 rounded tab-inactive">Usage</button>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div id="panel-rate-limits">
|
|
47
|
+
<div class="flex gap-2 mb-4">
|
|
48
|
+
<button id="subtab-limiter" class="px-3 py-2 rounded tab-active">Limiter override</button>
|
|
49
|
+
<button id="subtab-raw" class="px-3 py-2 rounded tab-inactive">Raw config (JSON)</button>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div id="subpanel-limiter">
|
|
53
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
|
54
|
+
<div class="bg-white rounded-lg shadow">
|
|
55
|
+
<div class="px-4 py-3 border-b">
|
|
56
|
+
<div class="flex items-center justify-between">
|
|
57
|
+
<div>
|
|
58
|
+
<h2 class="text-lg font-semibold text-gray-900">Limiters</h2>
|
|
59
|
+
<p class="text-sm text-gray-600">Integration points discovered at runtime and configured via JSON Configs.</p>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="flex gap-2">
|
|
62
|
+
<button id="btnEnableSelected" class="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700">Enable selected</button>
|
|
63
|
+
<button id="btnDisableSelected" class="px-3 py-2 bg-gray-800 text-white rounded hover:bg-gray-900">Disable selected</button>
|
|
64
|
+
<button id="btnEnableAll" class="px-3 py-2 bg-green-600 text-white rounded hover:bg-green-700">Enable all</button>
|
|
65
|
+
<button id="btnDisableAll" class="px-3 py-2 bg-gray-800 text-white rounded hover:bg-gray-900">Disable all</button>
|
|
66
|
+
<button id="btnReload" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Refresh</button>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="overflow-x-auto">
|
|
71
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
72
|
+
<thead class="bg-gray-50">
|
|
73
|
+
<tr>
|
|
74
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Sel</th>
|
|
75
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Id</th>
|
|
76
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Mode</th>
|
|
77
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Enabled</th>
|
|
78
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Limit</th>
|
|
79
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Window (ms)</th>
|
|
80
|
+
</tr>
|
|
81
|
+
</thead>
|
|
82
|
+
<tbody id="rows" class="bg-white divide-y divide-gray-200"></tbody>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<div class="bg-white rounded-lg shadow">
|
|
88
|
+
<div class="px-4 py-3 border-b">
|
|
89
|
+
<div class="flex justify-between items-center">
|
|
90
|
+
<h2 class="text-lg font-semibold text-gray-900" id="editorTitle">Editor</h2>
|
|
91
|
+
<div class="flex gap-2">
|
|
92
|
+
<button id="btnReset" class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300" style="display:none;">Reset override</button>
|
|
93
|
+
<button id="btnSave" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700" style="display:none;">Save</button>
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="p-4">
|
|
99
|
+
<div class="mb-4">
|
|
100
|
+
<div class="text-xs text-gray-500">Limiter id</div>
|
|
101
|
+
<div class="text-sm font-semibold" id="limiterId">-</div>
|
|
102
|
+
<div class="text-xs text-gray-500 mt-2">Integration</div>
|
|
103
|
+
<pre id="integration" class="bg-gray-50 rounded p-3 text-xs overflow-auto max-h-32">-</pre>
|
|
104
|
+
</div>
|
|
105
|
+
|
|
106
|
+
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
107
|
+
<div>
|
|
108
|
+
<label class="block text-sm font-medium mb-1">Enabled</label>
|
|
109
|
+
<select id="enabled" class="w-full border rounded px-3 py-2">
|
|
110
|
+
<option value="true">true</option>
|
|
111
|
+
<option value="false">false</option>
|
|
112
|
+
</select>
|
|
113
|
+
</div>
|
|
114
|
+
<div>
|
|
115
|
+
<label class="block text-sm font-medium mb-1">Mode</label>
|
|
116
|
+
<select id="mode" class="w-full border rounded px-3 py-2">
|
|
117
|
+
<option value="reportOnly">reportOnly</option>
|
|
118
|
+
<option value="enforce">enforce</option>
|
|
119
|
+
<option value="disabled">disabled</option>
|
|
120
|
+
</select>
|
|
121
|
+
</div>
|
|
122
|
+
<div>
|
|
123
|
+
<label class="block text-sm font-medium mb-1">Algorithm</label>
|
|
124
|
+
<select id="algorithm" class="w-full border rounded px-3 py-2">
|
|
125
|
+
<option value="fixedWindow">fixedWindow</option>
|
|
126
|
+
</select>
|
|
127
|
+
</div>
|
|
128
|
+
<div>
|
|
129
|
+
<label class="block text-sm font-medium mb-1">Identity</label>
|
|
130
|
+
<select id="identityType" class="w-full border rounded px-3 py-2">
|
|
131
|
+
<option value="userIdOrIp">userIdOrIp</option>
|
|
132
|
+
<option value="userId">userId</option>
|
|
133
|
+
<option value="ip">ip</option>
|
|
134
|
+
<option value="orgId">orgId</option>
|
|
135
|
+
<option value="header">header</option>
|
|
136
|
+
</select>
|
|
137
|
+
</div>
|
|
138
|
+
<div id="identityHeaderNameWrap" style="display:none;">
|
|
139
|
+
<label class="block text-sm font-medium mb-1">Header name</label>
|
|
140
|
+
<input id="identityHeaderName" class="w-full border rounded px-3 py-2" placeholder="e.g. x-api-key" />
|
|
141
|
+
</div>
|
|
142
|
+
<div>
|
|
143
|
+
<label class="block text-sm font-medium mb-1">Limit max</label>
|
|
144
|
+
<input id="limitMax" type="number" min="0" step="1" class="w-full border rounded px-3 py-2" />
|
|
145
|
+
</div>
|
|
146
|
+
<div>
|
|
147
|
+
<label class="block text-sm font-medium mb-1">Window ms</label>
|
|
148
|
+
<input id="limitWindowMs" type="number" min="1" step="1" class="w-full border rounded px-3 py-2" />
|
|
149
|
+
</div>
|
|
150
|
+
<div>
|
|
151
|
+
<label class="block text-sm font-medium mb-1">Metrics enabled</label>
|
|
152
|
+
<select id="metricsEnabled" class="w-full border rounded px-3 py-2">
|
|
153
|
+
<option value="true">true</option>
|
|
154
|
+
<option value="false">false</option>
|
|
155
|
+
</select>
|
|
156
|
+
</div>
|
|
157
|
+
<div>
|
|
158
|
+
<label class="block text-sm font-medium mb-1">Metrics bucket ms</label>
|
|
159
|
+
<input id="metricsBucketMs" type="number" min="1000" step="1000" class="w-full border rounded px-3 py-2" />
|
|
160
|
+
</div>
|
|
161
|
+
<div>
|
|
162
|
+
<label class="block text-sm font-medium mb-1">Retention days</label>
|
|
163
|
+
<input id="metricsRetentionDays" type="number" min="1" step="1" class="w-full border rounded px-3 py-2" />
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="mt-6">
|
|
168
|
+
<div class="flex items-center justify-between mb-2">
|
|
169
|
+
<label class="block text-sm font-medium">Override JSON (advanced)</label>
|
|
170
|
+
<button id="btnSaveOverrideJson" class="px-3 py-2 bg-gray-800 text-white rounded hover:bg-gray-900" style="display:none;">Save JSON override</button>
|
|
171
|
+
</div>
|
|
172
|
+
<textarea id="overrideJson" spellcheck="false" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="10"></textarea>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div id="subpanel-raw" style="display:none;">
|
|
180
|
+
<div class="bg-white rounded-lg shadow">
|
|
181
|
+
<div class="px-4 py-3 border-b">
|
|
182
|
+
<div class="flex justify-between items-center">
|
|
183
|
+
<div>
|
|
184
|
+
<h2 class="text-lg font-semibold text-gray-900">Raw rate limits config</h2>
|
|
185
|
+
<p class="text-sm text-gray-600">Stored in JSON Configs (alias: rate-limits)</p>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="flex gap-2">
|
|
188
|
+
<button id="btnLoadRaw" class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Load</button>
|
|
189
|
+
<button id="btnSaveRaw" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="p-4">
|
|
194
|
+
<textarea id="rawConfig" spellcheck="false" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="18"></textarea>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
|
|
200
|
+
<div id="panel-metrics" style="display:none;">
|
|
201
|
+
<div class="bg-white rounded-lg shadow">
|
|
202
|
+
<div class="px-4 py-3 border-b">
|
|
203
|
+
<div class="flex items-center justify-between">
|
|
204
|
+
<div>
|
|
205
|
+
<h2 class="text-lg font-semibold text-gray-900">Metrics</h2>
|
|
206
|
+
<p class="text-sm text-gray-600">Counts are computed per limiter bucket.</p>
|
|
207
|
+
</div>
|
|
208
|
+
<button id="btnMetricsReload" class="px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">Refresh</button>
|
|
209
|
+
</div>
|
|
210
|
+
</div>
|
|
211
|
+
|
|
212
|
+
<div class="p-4">
|
|
213
|
+
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
|
|
214
|
+
<div>
|
|
215
|
+
<label class="block text-sm font-medium mb-1">Start (optional, ISO)</label>
|
|
216
|
+
<input id="metricsStart" class="w-full border rounded px-3 py-2" placeholder="2026-01-25T00:00:00.000Z" />
|
|
217
|
+
</div>
|
|
218
|
+
<div>
|
|
219
|
+
<label class="block text-sm font-medium mb-1">End (optional, ISO)</label>
|
|
220
|
+
<input id="metricsEnd" class="w-full border rounded px-3 py-2" placeholder="2026-01-25T23:59:59.000Z" />
|
|
221
|
+
</div>
|
|
222
|
+
<div class="flex items-end">
|
|
223
|
+
<button id="btnMetricsApply" class="px-4 py-2 bg-gray-900 text-white rounded hover:bg-gray-800 w-full">Apply range</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div class="overflow-x-auto">
|
|
228
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
229
|
+
<thead class="bg-gray-50">
|
|
230
|
+
<tr>
|
|
231
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Limiter</th>
|
|
232
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Checked</th>
|
|
233
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Allowed</th>
|
|
234
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Blocked</th>
|
|
235
|
+
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 uppercase">Block rate</th>
|
|
236
|
+
</tr>
|
|
237
|
+
</thead>
|
|
238
|
+
<tbody id="metricsRows" class="bg-white divide-y divide-gray-200"></tbody>
|
|
239
|
+
</table>
|
|
240
|
+
</div>
|
|
241
|
+
|
|
242
|
+
<div class="mt-4">
|
|
243
|
+
<div class="text-xs text-gray-500">Range</div>
|
|
244
|
+
<div id="metricsRange" class="text-sm">-</div>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
|
|
250
|
+
<div id="panel-usage" style="display:none;">
|
|
251
|
+
<div class="bg-white rounded-lg shadow">
|
|
252
|
+
<div class="px-4 py-3 border-b">
|
|
253
|
+
<h2 class="text-lg font-semibold text-gray-900">Usage</h2>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="p-4">
|
|
256
|
+
<div class="mb-4">
|
|
257
|
+
<div class="flex items-center justify-between mb-2">
|
|
258
|
+
<div class="text-sm font-medium">Express middleware</div>
|
|
259
|
+
<button id="btnCopyUsage" class="px-3 py-2 bg-gray-200 text-gray-800 rounded hover:bg-gray-300">Copy</button>
|
|
260
|
+
</div>
|
|
261
|
+
<pre id="usageSnippet" class="bg-gray-50 rounded p-3 text-sm overflow-auto">const { helpers } = require('@intranefr/superbackend');
|
|
262
|
+
|
|
263
|
+
app.use('/api', helpers.rateLimiter.limit('globalApiLimiter'));</pre>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div class="text-sm text-gray-700 space-y-2">
|
|
267
|
+
<p><strong>Identity default:</strong> userId when authenticated (Bearer JWT) else IP.</p>
|
|
268
|
+
<p><strong>Custom identity:</strong> you can provide orgId / custom identity by using <code>check()</code> programmatically or passing <code>getIdentity</code> in middleware options.</p>
|
|
269
|
+
<p><strong>Modes:</strong> reportOnly records would-be blocks; enforce blocks with 429.</p>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
|
|
275
|
+
</div>
|
|
276
|
+
</div>
|
|
277
|
+
|
|
278
|
+
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
279
|
+
|
|
280
|
+
<script>
|
|
281
|
+
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
282
|
+
|
|
283
|
+
let items = [];
|
|
284
|
+
let selectedId = null;
|
|
285
|
+
let selectedIds = new Set();
|
|
286
|
+
|
|
287
|
+
function $(id) { return document.getElementById(id); }
|
|
288
|
+
|
|
289
|
+
function showToast(message, type = 'success') {
|
|
290
|
+
const container = document.getElementById('toast-container');
|
|
291
|
+
const toast = document.createElement('div');
|
|
292
|
+
toast.className = `toast px-6 py-4 rounded-lg shadow-lg text-white ${type === 'success' ? 'bg-green-500' : 'bg-red-500'}`;
|
|
293
|
+
toast.textContent = message;
|
|
294
|
+
container.appendChild(toast);
|
|
295
|
+
|
|
296
|
+
setTimeout(() => {
|
|
297
|
+
toast.classList.add('fade-out');
|
|
298
|
+
setTimeout(() => toast.remove(), 300);
|
|
299
|
+
}, 2500);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function setTabs(active) {
|
|
303
|
+
const tabs = [
|
|
304
|
+
{ key: 'rate-limits', tab: 'tab-rate-limits', panel: 'panel-rate-limits' },
|
|
305
|
+
{ key: 'metrics', tab: 'tab-metrics', panel: 'panel-metrics' },
|
|
306
|
+
{ key: 'usage', tab: 'tab-usage', panel: 'panel-usage' },
|
|
307
|
+
];
|
|
308
|
+
tabs.forEach(t => {
|
|
309
|
+
const btn = $(t.tab);
|
|
310
|
+
const panel = $(t.panel);
|
|
311
|
+
if (!btn || !panel) return;
|
|
312
|
+
const isActive = t.key === active;
|
|
313
|
+
btn.className = `px-4 py-2 rounded ${isActive ? 'tab-active' : 'tab-inactive'}`;
|
|
314
|
+
panel.style.display = isActive ? '' : 'none';
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function setSubTabs(active) {
|
|
319
|
+
const tabs = [
|
|
320
|
+
{ key: 'limiter', tab: 'subtab-limiter', panel: 'subpanel-limiter' },
|
|
321
|
+
{ key: 'raw', tab: 'subtab-raw', panel: 'subpanel-raw' },
|
|
322
|
+
];
|
|
323
|
+
tabs.forEach(t => {
|
|
324
|
+
const btn = $(t.tab);
|
|
325
|
+
const panel = $(t.panel);
|
|
326
|
+
if (!btn || !panel) return;
|
|
327
|
+
const isActive = t.key === active;
|
|
328
|
+
btn.className = `px-3 py-2 rounded ${isActive ? 'tab-active' : 'tab-inactive'}`;
|
|
329
|
+
panel.style.display = isActive ? '' : 'none';
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function safeJsonParse(raw, fallback) {
|
|
334
|
+
try { return JSON.parse(raw); } catch (_) { return fallback; }
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function renderRows() {
|
|
338
|
+
const body = $('rows');
|
|
339
|
+
if (!body) return;
|
|
340
|
+
body.innerHTML = '';
|
|
341
|
+
|
|
342
|
+
items.forEach(item => {
|
|
343
|
+
const tr = document.createElement('tr');
|
|
344
|
+
tr.className = `cursor-pointer ${item.id === selectedId ? 'bg-blue-50' : ''}`;
|
|
345
|
+
tr.onclick = () => setSelected(item.id);
|
|
346
|
+
|
|
347
|
+
const checked = selectedIds.has(item.id);
|
|
348
|
+
|
|
349
|
+
const eff = item.effective || {};
|
|
350
|
+
const max = eff.limit?.max ?? '';
|
|
351
|
+
const windowMs = eff.limit?.windowMs ?? '';
|
|
352
|
+
|
|
353
|
+
tr.innerHTML = `
|
|
354
|
+
<td class="px-4 py-2 text-sm text-gray-700"><input type="checkbox" data-id="${item.id}" ${checked ? 'checked' : ''} /></td>
|
|
355
|
+
<td class="px-4 py-2 text-sm text-gray-900">${item.id}</td>
|
|
356
|
+
<td class="px-4 py-2 text-sm text-gray-700">${String(eff.mode || '')}</td>
|
|
357
|
+
<td class="px-4 py-2 text-sm text-gray-700">${String(eff.enabled !== false)}</td>
|
|
358
|
+
<td class="px-4 py-2 text-sm text-gray-700">${String(max)}</td>
|
|
359
|
+
<td class="px-4 py-2 text-sm text-gray-700">${String(windowMs)}</td>
|
|
360
|
+
`;
|
|
361
|
+
|
|
362
|
+
body.appendChild(tr);
|
|
363
|
+
|
|
364
|
+
const checkbox = tr.querySelector('input[type="checkbox"]');
|
|
365
|
+
if (checkbox) {
|
|
366
|
+
checkbox.addEventListener('click', (e) => e.stopPropagation());
|
|
367
|
+
checkbox.addEventListener('change', (e) => {
|
|
368
|
+
const id = e.target?.dataset?.id;
|
|
369
|
+
if (!id) return;
|
|
370
|
+
if (e.target.checked) selectedIds.add(id);
|
|
371
|
+
else selectedIds.delete(id);
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function setSelected(id) {
|
|
378
|
+
selectedId = id;
|
|
379
|
+
const item = items.find(x => x.id === id);
|
|
380
|
+
|
|
381
|
+
$('btnSave').style.display = item ? 'inline-block' : 'none';
|
|
382
|
+
$('btnReset').style.display = item ? 'inline-block' : 'none';
|
|
383
|
+
$('btnSaveOverrideJson').style.display = item ? 'inline-block' : 'none';
|
|
384
|
+
|
|
385
|
+
$('editorTitle').textContent = item ? `Editing: ${item.id}` : 'Editor';
|
|
386
|
+
$('limiterId').textContent = item ? item.id : '-';
|
|
387
|
+
$('integration').textContent = item?.integration ? JSON.stringify(item.integration, null, 2) : '-';
|
|
388
|
+
|
|
389
|
+
const eff = item?.effective || {};
|
|
390
|
+
|
|
391
|
+
$('enabled').value = String(eff.enabled !== false);
|
|
392
|
+
$('mode').value = String(eff.mode || 'reportOnly');
|
|
393
|
+
$('algorithm').value = String(eff.algorithm || 'fixedWindow');
|
|
394
|
+
|
|
395
|
+
const identityType = String(eff.identity?.type || 'userIdOrIp');
|
|
396
|
+
$('identityType').value = identityType;
|
|
397
|
+
$('identityHeaderName').value = String(eff.identity?.headerName || '');
|
|
398
|
+
$('identityHeaderNameWrap').style.display = identityType === 'header' ? '' : 'none';
|
|
399
|
+
|
|
400
|
+
$('limitMax').value = String(eff.limit?.max ?? '');
|
|
401
|
+
$('limitWindowMs').value = String(eff.limit?.windowMs ?? '');
|
|
402
|
+
|
|
403
|
+
$('metricsEnabled').value = String(eff.metrics?.enabled !== false);
|
|
404
|
+
$('metricsBucketMs').value = String(eff.metrics?.bucketMs ?? 60000);
|
|
405
|
+
$('metricsRetentionDays').value = String(eff.metrics?.retentionDays ?? 14);
|
|
406
|
+
|
|
407
|
+
$('overrideJson').value = JSON.stringify(item?.override || {}, null, 2);
|
|
408
|
+
|
|
409
|
+
renderRows();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function loadLimiters() {
|
|
413
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits`, { headers: { 'Accept': 'application/json' } });
|
|
414
|
+
const data = await res.json();
|
|
415
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to load');
|
|
416
|
+
items = Array.isArray(data.items) ? data.items : [];
|
|
417
|
+
|
|
418
|
+
const nextSelected = new Set();
|
|
419
|
+
items.forEach((i) => {
|
|
420
|
+
if (selectedIds.has(i.id)) nextSelected.add(i.id);
|
|
421
|
+
});
|
|
422
|
+
selectedIds = nextSelected;
|
|
423
|
+
|
|
424
|
+
if (selectedId && !items.some(x => x.id === selectedId)) selectedId = null;
|
|
425
|
+
renderRows();
|
|
426
|
+
if (!selectedId && items.length) setSelected(items[0].id);
|
|
427
|
+
if (!items.length) setSelected(null);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
async function bulkEnabled({ enabled, all, ids }) {
|
|
431
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits/bulk-enabled`, {
|
|
432
|
+
method: 'POST',
|
|
433
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
434
|
+
body: JSON.stringify({ enabled, all, ids }),
|
|
435
|
+
});
|
|
436
|
+
const data = await res.json();
|
|
437
|
+
if (!res.ok) throw new Error(data?.error || 'Bulk update failed');
|
|
438
|
+
await loadLimiters();
|
|
439
|
+
showToast('Updated', 'success');
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function buildOverrideFromForm() {
|
|
443
|
+
const enabled = $('enabled').value === 'true';
|
|
444
|
+
const mode = $('mode').value;
|
|
445
|
+
const identityType = $('identityType').value;
|
|
446
|
+
|
|
447
|
+
const out = {
|
|
448
|
+
enabled,
|
|
449
|
+
mode,
|
|
450
|
+
algorithm: $('algorithm').value,
|
|
451
|
+
limit: {
|
|
452
|
+
max: Number($('limitMax').value || 0) || 0,
|
|
453
|
+
windowMs: Number($('limitWindowMs').value || 60000) || 60000,
|
|
454
|
+
},
|
|
455
|
+
identity: {
|
|
456
|
+
type: identityType,
|
|
457
|
+
},
|
|
458
|
+
metrics: {
|
|
459
|
+
enabled: $('metricsEnabled').value === 'true',
|
|
460
|
+
bucketMs: Number($('metricsBucketMs').value || 60000) || 60000,
|
|
461
|
+
retentionDays: Number($('metricsRetentionDays').value || 14) || 14,
|
|
462
|
+
},
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
if (identityType === 'header') {
|
|
466
|
+
out.identity.headerName = String($('identityHeaderName').value || '').trim();
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return out;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
async function saveOverride(override) {
|
|
473
|
+
const id = String(selectedId || '').trim();
|
|
474
|
+
if (!id) return;
|
|
475
|
+
|
|
476
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits/${encodeURIComponent(id)}`, {
|
|
477
|
+
method: 'PUT',
|
|
478
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
479
|
+
body: JSON.stringify({ override }),
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
const data = await res.json();
|
|
483
|
+
if (!res.ok) throw new Error(data?.error || 'Save failed');
|
|
484
|
+
await loadLimiters();
|
|
485
|
+
showToast('Saved', 'success');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
async function resetOverride() {
|
|
489
|
+
const id = String(selectedId || '').trim();
|
|
490
|
+
if (!id) return;
|
|
491
|
+
|
|
492
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits/${encodeURIComponent(id)}/reset`, {
|
|
493
|
+
method: 'POST',
|
|
494
|
+
headers: { 'Accept': 'application/json' },
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const data = await res.json();
|
|
498
|
+
if (!res.ok) throw new Error(data?.error || 'Reset failed');
|
|
499
|
+
await loadLimiters();
|
|
500
|
+
showToast('Reset override', 'success');
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
async function loadRawConfig() {
|
|
504
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits/config`, { headers: { 'Accept': 'application/json' } });
|
|
505
|
+
const data = await res.json();
|
|
506
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to load raw config');
|
|
507
|
+
$('rawConfig').value = String(data?.config?.jsonRaw || '');
|
|
508
|
+
showToast('Loaded raw config', 'success');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function saveRawConfig() {
|
|
512
|
+
const jsonRaw = $('rawConfig').value;
|
|
513
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits/config`, {
|
|
514
|
+
method: 'PUT',
|
|
515
|
+
headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
|
|
516
|
+
body: JSON.stringify({ jsonRaw }),
|
|
517
|
+
});
|
|
518
|
+
const data = await res.json();
|
|
519
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to save raw config');
|
|
520
|
+
showToast('Saved raw config', 'success');
|
|
521
|
+
await loadLimiters();
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
async function loadMetrics() {
|
|
525
|
+
const start = String($('metricsStart').value || '').trim();
|
|
526
|
+
const end = String($('metricsEnd').value || '').trim();
|
|
527
|
+
const qs = new URLSearchParams();
|
|
528
|
+
if (start) qs.set('start', start);
|
|
529
|
+
if (end) qs.set('end', end);
|
|
530
|
+
|
|
531
|
+
const res = await fetch(`${API_BASE}/api/admin/rate-limits/metrics?${qs.toString()}`, { headers: { 'Accept': 'application/json' } });
|
|
532
|
+
const data = await res.json();
|
|
533
|
+
if (!res.ok) throw new Error(data?.error || 'Failed to load metrics');
|
|
534
|
+
|
|
535
|
+
$('metricsRange').textContent = data?.range ? `${data.range.start} → ${data.range.end}` : '-';
|
|
536
|
+
|
|
537
|
+
const totals = data?.totals || {};
|
|
538
|
+
const rows = $('metricsRows');
|
|
539
|
+
rows.innerHTML = '';
|
|
540
|
+
|
|
541
|
+
Object.keys(totals).sort().forEach((id) => {
|
|
542
|
+
const t = totals[id] || {};
|
|
543
|
+
const checked = Number(t.checked || 0) || 0;
|
|
544
|
+
const blocked = Number(t.blocked || 0) || 0;
|
|
545
|
+
const allowed = Number(t.allowed || 0) || 0;
|
|
546
|
+
const rate = checked ? (blocked / checked) : 0;
|
|
547
|
+
|
|
548
|
+
const tr = document.createElement('tr');
|
|
549
|
+
tr.innerHTML = `
|
|
550
|
+
<td class="px-4 py-2 text-sm text-gray-900">${id}</td>
|
|
551
|
+
<td class="px-4 py-2 text-sm text-gray-700">${checked}</td>
|
|
552
|
+
<td class="px-4 py-2 text-sm text-gray-700">${allowed}</td>
|
|
553
|
+
<td class="px-4 py-2 text-sm text-gray-700">${blocked}</td>
|
|
554
|
+
<td class="px-4 py-2 text-sm text-gray-700">${(rate * 100).toFixed(2)}%</td>
|
|
555
|
+
`;
|
|
556
|
+
rows.appendChild(tr);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
showToast('Loaded metrics', 'success');
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function bind() {
|
|
563
|
+
$('tab-rate-limits').onclick = () => setTabs('rate-limits');
|
|
564
|
+
$('tab-metrics').onclick = () => { setTabs('metrics'); loadMetrics().catch(e => showToast(e.message, 'error')); };
|
|
565
|
+
$('tab-usage').onclick = () => setTabs('usage');
|
|
566
|
+
|
|
567
|
+
$('subtab-limiter').onclick = () => setSubTabs('limiter');
|
|
568
|
+
$('subtab-raw').onclick = () => setSubTabs('raw');
|
|
569
|
+
|
|
570
|
+
$('btnReload').onclick = () => loadLimiters().catch(e => showToast(e.message, 'error'));
|
|
571
|
+
|
|
572
|
+
$('btnEnableAll').onclick = () => bulkEnabled({ enabled: true, all: true }).catch(e => showToast(e.message, 'error'));
|
|
573
|
+
$('btnDisableAll').onclick = () => bulkEnabled({ enabled: false, all: true }).catch(e => showToast(e.message, 'error'));
|
|
574
|
+
$('btnEnableSelected').onclick = () => {
|
|
575
|
+
const ids = Array.from(selectedIds);
|
|
576
|
+
if (!ids.length) return showToast('Select at least 1 limiter', 'error');
|
|
577
|
+
return bulkEnabled({ enabled: true, all: false, ids }).catch(e => showToast(e.message, 'error'));
|
|
578
|
+
};
|
|
579
|
+
$('btnDisableSelected').onclick = () => {
|
|
580
|
+
const ids = Array.from(selectedIds);
|
|
581
|
+
if (!ids.length) return showToast('Select at least 1 limiter', 'error');
|
|
582
|
+
return bulkEnabled({ enabled: false, all: false, ids }).catch(e => showToast(e.message, 'error'));
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
$('btnSave').onclick = () => saveOverride(buildOverrideFromForm()).catch(e => showToast(e.message, 'error'));
|
|
586
|
+
$('btnSaveOverrideJson').onclick = () => {
|
|
587
|
+
const parsed = safeJsonParse($('overrideJson').value, null);
|
|
588
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
589
|
+
showToast('Override JSON must be an object', 'error');
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
saveOverride(parsed).catch(e => showToast(e.message, 'error'));
|
|
593
|
+
};
|
|
594
|
+
$('btnReset').onclick = () => resetOverride().catch(e => showToast(e.message, 'error'));
|
|
595
|
+
|
|
596
|
+
$('identityType').onchange = () => {
|
|
597
|
+
const v = $('identityType').value;
|
|
598
|
+
$('identityHeaderNameWrap').style.display = v === 'header' ? '' : 'none';
|
|
599
|
+
};
|
|
600
|
+
|
|
601
|
+
$('btnLoadRaw').onclick = () => loadRawConfig().catch(e => showToast(e.message, 'error'));
|
|
602
|
+
$('btnSaveRaw').onclick = () => saveRawConfig().catch(e => showToast(e.message, 'error'));
|
|
603
|
+
|
|
604
|
+
$('btnMetricsReload').onclick = () => loadMetrics().catch(e => showToast(e.message, 'error'));
|
|
605
|
+
$('btnMetricsApply').onclick = () => loadMetrics().catch(e => showToast(e.message, 'error'));
|
|
606
|
+
|
|
607
|
+
$('btnCopyUsage').onclick = async () => {
|
|
608
|
+
try {
|
|
609
|
+
await navigator.clipboard.writeText($('usageSnippet').textContent);
|
|
610
|
+
showToast('Copied', 'success');
|
|
611
|
+
} catch (e) {
|
|
612
|
+
showToast(e.message || 'Copy failed', 'error');
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
(function init() {
|
|
618
|
+
bind();
|
|
619
|
+
setTabs('rate-limits');
|
|
620
|
+
setSubTabs('limiter');
|
|
621
|
+
loadLimiters().catch(e => showToast(e.message, 'error'));
|
|
622
|
+
})();
|
|
623
|
+
</script>
|
|
624
|
+
</body>
|
|
625
|
+
</html>
|