@intranefr/superbackend 1.4.3
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/.commiat +4 -0
- package/.env.example +47 -0
- package/README.md +110 -0
- package/index.js +94 -0
- package/package.json +67 -0
- package/public/css/styles.css +139 -0
- package/public/js/animations.js +41 -0
- package/sdk/error-tracking/browser/package.json +16 -0
- package/sdk/error-tracking/browser/src/core.js +270 -0
- package/sdk/error-tracking/browser/src/embed.js +18 -0
- package/sdk/error-tracking/browser/src/index.js +1 -0
- package/server.js +5 -0
- package/src/admin/endpointRegistry.js +300 -0
- package/src/controllers/admin.controller.js +321 -0
- package/src/controllers/adminAssets.controller.js +530 -0
- package/src/controllers/adminAssetsStorage.controller.js +260 -0
- package/src/controllers/adminEjsVirtual.controller.js +354 -0
- package/src/controllers/adminFeatureFlags.controller.js +155 -0
- package/src/controllers/adminHeadless.controller.js +1071 -0
- package/src/controllers/adminI18n.controller.js +604 -0
- package/src/controllers/adminJsonConfigs.controller.js +97 -0
- package/src/controllers/adminLlm.controller.js +273 -0
- package/src/controllers/adminMigration.controller.js +257 -0
- package/src/controllers/adminSeoConfig.controller.js +515 -0
- package/src/controllers/adminStats.controller.js +121 -0
- package/src/controllers/adminUploadNamespaces.controller.js +208 -0
- package/src/controllers/assets.controller.js +248 -0
- package/src/controllers/auth.controller.js +93 -0
- package/src/controllers/billing.controller.js +223 -0
- package/src/controllers/featureFlags.controller.js +35 -0
- package/src/controllers/forms.controller.js +217 -0
- package/src/controllers/globalSettings.controller.js +252 -0
- package/src/controllers/headlessCrud.controller.js +126 -0
- package/src/controllers/i18n.controller.js +12 -0
- package/src/controllers/invite.controller.js +249 -0
- package/src/controllers/jsonConfigs.controller.js +19 -0
- package/src/controllers/metrics.controller.js +149 -0
- package/src/controllers/notificationAdmin.controller.js +264 -0
- package/src/controllers/notifications.controller.js +131 -0
- package/src/controllers/org.controller.js +357 -0
- package/src/controllers/orgAdmin.controller.js +491 -0
- package/src/controllers/stripeAdmin.controller.js +410 -0
- package/src/controllers/user.controller.js +361 -0
- package/src/controllers/userAdmin.controller.js +277 -0
- package/src/controllers/waitingList.controller.js +167 -0
- package/src/controllers/webhook.controller.js +200 -0
- package/src/middleware/auth.js +66 -0
- package/src/middleware/errorCapture.js +170 -0
- package/src/middleware/headlessApiTokenAuth.js +57 -0
- package/src/middleware/org.js +108 -0
- package/src/middleware.js +901 -0
- package/src/models/ActionEvent.js +31 -0
- package/src/models/ActivityLog.js +41 -0
- package/src/models/Asset.js +84 -0
- package/src/models/AuditEvent.js +93 -0
- package/src/models/EmailLog.js +28 -0
- package/src/models/ErrorAggregate.js +72 -0
- package/src/models/FormSubmission.js +41 -0
- package/src/models/GlobalSetting.js +38 -0
- package/src/models/HeadlessApiToken.js +24 -0
- package/src/models/HeadlessModelDefinition.js +41 -0
- package/src/models/I18nEntry.js +77 -0
- package/src/models/I18nLocale.js +33 -0
- package/src/models/Invite.js +70 -0
- package/src/models/JsonConfig.js +46 -0
- package/src/models/Notification.js +60 -0
- package/src/models/Organization.js +57 -0
- package/src/models/OrganizationMember.js +43 -0
- package/src/models/StripeCatalogItem.js +77 -0
- package/src/models/StripeWebhookEvent.js +57 -0
- package/src/models/User.js +89 -0
- package/src/models/VirtualEjsFile.js +60 -0
- package/src/models/VirtualEjsFileVersion.js +43 -0
- package/src/models/VirtualEjsGroupChange.js +32 -0
- package/src/models/WaitingList.js +41 -0
- package/src/models/Webhook.js +63 -0
- package/src/models/Workflow.js +29 -0
- package/src/models/WorkflowExecution.js +12 -0
- package/src/routes/admin.routes.js +26 -0
- package/src/routes/adminAssets.routes.js +28 -0
- package/src/routes/adminAssetsStorage.routes.js +13 -0
- package/src/routes/adminAudit.routes.js +196 -0
- package/src/routes/adminEjsVirtual.routes.js +17 -0
- package/src/routes/adminErrors.routes.js +164 -0
- package/src/routes/adminFeatureFlags.routes.js +12 -0
- package/src/routes/adminHeadless.routes.js +38 -0
- package/src/routes/adminI18n.routes.js +22 -0
- package/src/routes/adminJsonConfigs.routes.js +15 -0
- package/src/routes/adminLlm.routes.js +12 -0
- package/src/routes/adminMigration.routes.js +81 -0
- package/src/routes/adminSeoConfig.routes.js +20 -0
- package/src/routes/adminUploadNamespaces.routes.js +13 -0
- package/src/routes/assets.routes.js +21 -0
- package/src/routes/auth.routes.js +12 -0
- package/src/routes/billing.routes.js +11 -0
- package/src/routes/errorTracking.routes.js +31 -0
- package/src/routes/featureFlags.routes.js +9 -0
- package/src/routes/forms.routes.js +9 -0
- package/src/routes/formsAdmin.routes.js +13 -0
- package/src/routes/globalSettings.routes.js +18 -0
- package/src/routes/headless.routes.js +15 -0
- package/src/routes/i18n.routes.js +8 -0
- package/src/routes/invite.routes.js +9 -0
- package/src/routes/jsonConfigs.routes.js +8 -0
- package/src/routes/log.routes.js +111 -0
- package/src/routes/metrics.routes.js +9 -0
- package/src/routes/notificationAdmin.routes.js +15 -0
- package/src/routes/notifications.routes.js +12 -0
- package/src/routes/org.routes.js +31 -0
- package/src/routes/orgAdmin.routes.js +20 -0
- package/src/routes/publicAssets.routes.js +7 -0
- package/src/routes/stripeAdmin.routes.js +20 -0
- package/src/routes/user.routes.js +22 -0
- package/src/routes/userAdmin.routes.js +15 -0
- package/src/routes/waitingList.routes.js +13 -0
- package/src/routes/waitingListAdmin.routes.js +9 -0
- package/src/routes/webhook.routes.js +32 -0
- package/src/routes/workflowWebhook.routes.js +54 -0
- package/src/routes/workflows.routes.js +110 -0
- package/src/services/assets.service.js +110 -0
- package/src/services/audit.service.js +62 -0
- package/src/services/auditLogger.js +165 -0
- package/src/services/ejsVirtual.service.js +614 -0
- package/src/services/email.service.js +351 -0
- package/src/services/errorLogger.js +221 -0
- package/src/services/featureFlags.service.js +202 -0
- package/src/services/forms.service.js +214 -0
- package/src/services/globalSettings.service.js +49 -0
- package/src/services/headlessApiTokens.service.js +158 -0
- package/src/services/headlessCrypto.service.js +31 -0
- package/src/services/headlessModels.service.js +356 -0
- package/src/services/i18n.service.js +314 -0
- package/src/services/i18nInferredKeys.service.js +337 -0
- package/src/services/jsonConfigs.service.js +392 -0
- package/src/services/llm.service.js +749 -0
- package/src/services/migration.service.js +581 -0
- package/src/services/migrationAssets/fsLocal.js +58 -0
- package/src/services/migrationAssets/index.js +134 -0
- package/src/services/migrationAssets/s3.js +75 -0
- package/src/services/migrationAssets/sftp.js +92 -0
- package/src/services/notification.service.js +212 -0
- package/src/services/objectStorage.service.js +514 -0
- package/src/services/seoConfig.service.js +402 -0
- package/src/services/storage.js +150 -0
- package/src/services/stripe.service.js +185 -0
- package/src/services/stripeHelper.service.js +264 -0
- package/src/services/uploadNamespaces.service.js +326 -0
- package/src/services/webhook.service.js +157 -0
- package/src/services/workflow.service.js +271 -0
- package/src/utils/asyncHandler.js +5 -0
- package/src/utils/encryption.js +80 -0
- package/src/utils/jwt.js +40 -0
- package/src/utils/orgRoles.js +156 -0
- package/src/utils/validation.js +26 -0
- package/src/utils/webhookRetry.js +93 -0
- package/views/admin-assets.ejs +444 -0
- package/views/admin-audit.ejs +283 -0
- package/views/admin-coolify-deploy.ejs +207 -0
- package/views/admin-dashboard-home.ejs +291 -0
- package/views/admin-dashboard.ejs +397 -0
- package/views/admin-ejs-virtual.ejs +280 -0
- package/views/admin-errors.ejs +368 -0
- package/views/admin-feature-flags.ejs +390 -0
- package/views/admin-forms.ejs +526 -0
- package/views/admin-global-settings.ejs +436 -0
- package/views/admin-headless.ejs +2020 -0
- package/views/admin-i18n-locales.ejs +221 -0
- package/views/admin-i18n.ejs +728 -0
- package/views/admin-json-configs.ejs +410 -0
- package/views/admin-llm.ejs +884 -0
- package/views/admin-metrics.ejs +274 -0
- package/views/admin-migration.ejs +814 -0
- package/views/admin-notifications.ejs +430 -0
- package/views/admin-organizations.ejs +984 -0
- package/views/admin-seo-config.ejs +673 -0
- package/views/admin-stripe-pricing.ejs +558 -0
- package/views/admin-test.ejs +342 -0
- package/views/admin-users.ejs +452 -0
- package/views/admin-waiting-list.ejs +547 -0
- package/views/admin-webhooks.ejs +329 -0
- package/views/admin-workflows.ejs +310 -0
- package/views/partials/admin-assets-script.ejs +2022 -0
- package/views/partials/admin-test-sidebar.ejs +14 -0
- package/views/partials/dashboard/nav-items.ejs +66 -0
- package/views/partials/dashboard/palette.ejs +63 -0
- package/views/partials/dashboard/sidebar.ejs +21 -0
- package/views/partials/dashboard/tab-bar.ejs +26 -0
- package/views/partials/footer.ejs +3 -0
|
@@ -0,0 +1,390 @@
|
|
|
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>Feature Flags - Admin</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.toast {
|
|
10
|
+
animation: slideIn 0.3s ease-out;
|
|
11
|
+
}
|
|
12
|
+
@keyframes slideIn {
|
|
13
|
+
from { transform: translateX(100%); opacity: 0; }
|
|
14
|
+
to { transform: translateX(0); opacity: 1; }
|
|
15
|
+
}
|
|
16
|
+
.fade-out {
|
|
17
|
+
animation: fadeOut 0.3s ease-out forwards;
|
|
18
|
+
}
|
|
19
|
+
@keyframes fadeOut {
|
|
20
|
+
from { opacity: 1; }
|
|
21
|
+
to { opacity: 0; }
|
|
22
|
+
}
|
|
23
|
+
</style>
|
|
24
|
+
</head>
|
|
25
|
+
<body class="bg-gray-100">
|
|
26
|
+
<div class="min-h-screen">
|
|
27
|
+
<div class="bg-white shadow">
|
|
28
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
|
|
29
|
+
<div class="flex justify-between items-center">
|
|
30
|
+
<div>
|
|
31
|
+
<h1 class="text-2xl font-bold text-gray-900">Feature Flags</h1>
|
|
32
|
+
<p class="text-sm text-gray-600 mt-1">Manage FEATURE_FLAG.* definitions (stored as Global Settings JSON)</p>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="flex items-center gap-4">
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
41
|
+
<div class="mb-6 flex justify-between items-center">
|
|
42
|
+
<button onclick="loadFlags()" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600 flex items-center">
|
|
43
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
44
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
|
|
45
|
+
</svg>
|
|
46
|
+
Refresh
|
|
47
|
+
</button>
|
|
48
|
+
<button onclick="showCreateModal()" class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600 flex items-center">
|
|
49
|
+
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
50
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
|
|
51
|
+
</svg>
|
|
52
|
+
New Flag
|
|
53
|
+
</button>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div id="flags-container" class="grid grid-cols-1 gap-6">
|
|
57
|
+
<div class="text-center py-12">
|
|
58
|
+
<p class="text-gray-600">Loading flags...</p>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="mt-8 bg-blue-50 border-l-4 border-blue-500 p-4">
|
|
63
|
+
<h3 class="font-semibold text-blue-900 mb-2">📋 Storage</h3>
|
|
64
|
+
<div class="text-sm text-blue-800 space-y-2">
|
|
65
|
+
<p>Flags are stored as Global Settings with keys: <code class="bg-blue-100 px-1 rounded">FEATURE_FLAG.<key></code> and <code class="bg-blue-100 px-1 rounded">type=json</code>.</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div id="toast-container" class="fixed top-4 right-4 space-y-2 z-50"></div>
|
|
72
|
+
|
|
73
|
+
<div id="edit-modal" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
|
|
74
|
+
<div class="relative top-20 mx-auto p-5 border w-full max-w-2xl shadow-lg rounded-md bg-white">
|
|
75
|
+
<div class="flex justify-between items-center mb-4">
|
|
76
|
+
<h3 class="text-xl font-bold" id="modal-title">Edit Flag</h3>
|
|
77
|
+
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600">
|
|
78
|
+
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
79
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<form id="edit-form" onsubmit="handleSubmit(event)">
|
|
85
|
+
<input type="hidden" id="edit-mode">
|
|
86
|
+
<input type="hidden" id="edit-key">
|
|
87
|
+
|
|
88
|
+
<div class="mb-4" id="key-input-container">
|
|
89
|
+
<label class="block text-sm font-medium mb-2">Key *</label>
|
|
90
|
+
<input type="text" id="new-key" class="w-full border rounded px-3 py-2" placeholder="e.g. new_checkout" required>
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
<div class="mb-4">
|
|
94
|
+
<label class="block text-sm font-medium mb-2">Description</label>
|
|
95
|
+
<input type="text" id="edit-description" class="w-full border rounded px-3 py-2" placeholder="Optional">
|
|
96
|
+
</div>
|
|
97
|
+
|
|
98
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
99
|
+
<div class="mb-4">
|
|
100
|
+
<label class="flex items-center">
|
|
101
|
+
<input type="checkbox" id="edit-enabled" class="mr-2">
|
|
102
|
+
<span class="text-sm font-medium">Enabled (global default)</span>
|
|
103
|
+
</label>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="mb-4">
|
|
106
|
+
<label class="block text-sm font-medium mb-2">Rollout % (0..100)</label>
|
|
107
|
+
<input type="number" id="edit-rollout" class="w-full border rounded px-3 py-2" min="0" max="100" value="0">
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
|
|
111
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
112
|
+
<div class="mb-4">
|
|
113
|
+
<label class="block text-sm font-medium mb-2">Allow user IDs (comma-separated)</label>
|
|
114
|
+
<textarea id="edit-allow-users" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="3" placeholder="ObjectId, ObjectId"></textarea>
|
|
115
|
+
</div>
|
|
116
|
+
<div class="mb-4">
|
|
117
|
+
<label class="block text-sm font-medium mb-2">Allow org IDs (comma-separated)</label>
|
|
118
|
+
<textarea id="edit-allow-orgs" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="3" placeholder="ObjectId, ObjectId"></textarea>
|
|
119
|
+
</div>
|
|
120
|
+
</div>
|
|
121
|
+
|
|
122
|
+
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
123
|
+
<div class="mb-4">
|
|
124
|
+
<label class="block text-sm font-medium mb-2">Deny user IDs (comma-separated)</label>
|
|
125
|
+
<textarea id="edit-deny-users" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="3" placeholder="ObjectId, ObjectId"></textarea>
|
|
126
|
+
</div>
|
|
127
|
+
<div class="mb-4">
|
|
128
|
+
<label class="block text-sm font-medium mb-2">Deny org IDs (comma-separated)</label>
|
|
129
|
+
<textarea id="edit-deny-orgs" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="3" placeholder="ObjectId, ObjectId"></textarea>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
|
|
133
|
+
<div class="mb-4">
|
|
134
|
+
<label class="block text-sm font-medium mb-2">Payload (JSON)</label>
|
|
135
|
+
<textarea id="edit-payload" class="w-full border rounded px-3 py-2 font-mono text-sm" rows="6">null</textarea>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div class="flex justify-end space-x-2">
|
|
139
|
+
<button type="button" onclick="closeModal()" class="px-4 py-2 bg-gray-300 text-gray-700 rounded hover:bg-gray-400">Cancel</button>
|
|
140
|
+
<button type="submit" class="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600">Save</button>
|
|
141
|
+
</div>
|
|
142
|
+
</form>
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
<script>
|
|
147
|
+
const API_BASE = window.location.origin + "<%= baseUrl %>" || window.location.origin;
|
|
148
|
+
let allFlags = [];
|
|
149
|
+
|
|
150
|
+
function showToast(message, type = 'success') {
|
|
151
|
+
const container = document.getElementById('toast-container');
|
|
152
|
+
const toast = document.createElement('div');
|
|
153
|
+
toast.className = `toast px-6 py-4 rounded-lg shadow-lg text-white ${
|
|
154
|
+
type === 'success' ? 'bg-green-500' : 'bg-red-500'
|
|
155
|
+
}`;
|
|
156
|
+
toast.textContent = message;
|
|
157
|
+
container.appendChild(toast);
|
|
158
|
+
|
|
159
|
+
setTimeout(() => {
|
|
160
|
+
toast.classList.add('fade-out');
|
|
161
|
+
setTimeout(() => toast.remove(), 300);
|
|
162
|
+
}, 3000);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function csvToArray(text) {
|
|
166
|
+
if (!text || !text.trim()) return [];
|
|
167
|
+
return text
|
|
168
|
+
.split(',')
|
|
169
|
+
.map((v) => v.trim())
|
|
170
|
+
.filter((v) => v);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function arrayToCsv(arr) {
|
|
174
|
+
if (!arr || !Array.isArray(arr) || arr.length === 0) return '';
|
|
175
|
+
return arr.join(', ');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function loadFlags() {
|
|
179
|
+
try {
|
|
180
|
+
const response = await fetch(`${API_BASE}/api/admin/feature-flags`);
|
|
181
|
+
const data = await response.json();
|
|
182
|
+
|
|
183
|
+
if (!response.ok) {
|
|
184
|
+
throw new Error(data?.error || 'Failed to load flags');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
allFlags = Array.isArray(data) ? data : [];
|
|
188
|
+
renderFlags();
|
|
189
|
+
showToast('Flags loaded successfully');
|
|
190
|
+
} catch (error) {
|
|
191
|
+
showToast(error.message, 'error');
|
|
192
|
+
document.getElementById('flags-container').innerHTML = `
|
|
193
|
+
<div class="text-center py-12 text-red-600">
|
|
194
|
+
<p>Error loading flags: ${error.message}</p>
|
|
195
|
+
</div>
|
|
196
|
+
`;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderFlags() {
|
|
201
|
+
const container = document.getElementById('flags-container');
|
|
202
|
+
|
|
203
|
+
if (!allFlags || allFlags.length === 0) {
|
|
204
|
+
container.innerHTML = `
|
|
205
|
+
<div class="text-center py-12">
|
|
206
|
+
<p class="text-gray-600">No feature flags found. Create your first flag!</p>
|
|
207
|
+
</div>
|
|
208
|
+
`;
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
container.innerHTML = allFlags.map((flag) => {
|
|
213
|
+
const allowUsers = flag.allowListUserIds?.length || 0;
|
|
214
|
+
const allowOrgs = flag.allowListOrgIds?.length || 0;
|
|
215
|
+
const denyUsers = flag.denyListUserIds?.length || 0;
|
|
216
|
+
const denyOrgs = flag.denyListOrgIds?.length || 0;
|
|
217
|
+
|
|
218
|
+
return `
|
|
219
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
220
|
+
<div class="flex justify-between items-start mb-4">
|
|
221
|
+
<div class="flex-1">
|
|
222
|
+
<div class="flex items-center space-x-2 mb-2">
|
|
223
|
+
<h3 class="text-lg font-bold text-gray-900">${escapeHtml(flag.key)}</h3>
|
|
224
|
+
${flag.enabled ? '<span class="px-2 py-1 text-xs rounded bg-green-100 text-green-700">Enabled</span>' : '<span class="px-2 py-1 text-xs rounded bg-gray-100 text-gray-600">Disabled</span>'}
|
|
225
|
+
<span class="px-2 py-1 text-xs rounded bg-blue-50 text-blue-700">Rollout: ${Number(flag.rolloutPercentage || 0)}%</span>
|
|
226
|
+
</div>
|
|
227
|
+
${flag.description ? `<p class="text-sm text-gray-600 mb-3">${escapeHtml(flag.description)}</p>` : ''}
|
|
228
|
+
<div class="text-xs text-gray-600 grid grid-cols-2 sm:grid-cols-4 gap-2">
|
|
229
|
+
<div>Allow users: <span class="font-semibold">${allowUsers}</span></div>
|
|
230
|
+
<div>Allow orgs: <span class="font-semibold">${allowOrgs}</span></div>
|
|
231
|
+
<div>Deny users: <span class="font-semibold">${denyUsers}</span></div>
|
|
232
|
+
<div>Deny orgs: <span class="font-semibold">${denyOrgs}</span></div>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
<div class="flex space-x-2 ml-4">
|
|
236
|
+
<button onclick="editFlag('${escapeJs(flag.key)}')" class="text-blue-600 hover:text-blue-800" title="Edit">
|
|
237
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
238
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path>
|
|
239
|
+
</svg>
|
|
240
|
+
</button>
|
|
241
|
+
<button onclick="deleteFlag('${escapeJs(flag.key)}')" class="text-red-600 hover:text-red-800" title="Delete">
|
|
242
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
243
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
|
|
244
|
+
</svg>
|
|
245
|
+
</button>
|
|
246
|
+
</div>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="bg-gray-50 rounded p-3">
|
|
249
|
+
<p class="text-xs text-gray-500 mb-1">Payload</p>
|
|
250
|
+
<pre class="text-sm text-gray-800 overflow-auto max-h-40">${escapeHtml(JSON.stringify(flag.payload ?? null, null, 2))}</pre>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
`;
|
|
254
|
+
}).join('');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function escapeHtml(text) {
|
|
258
|
+
const div = document.createElement('div');
|
|
259
|
+
div.textContent = String(text ?? '');
|
|
260
|
+
return div.innerHTML;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function escapeJs(text) {
|
|
264
|
+
return String(text ?? '').replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function showCreateModal() {
|
|
268
|
+
document.getElementById('modal-title').textContent = 'Create New Flag';
|
|
269
|
+
document.getElementById('edit-mode').value = 'create';
|
|
270
|
+
document.getElementById('edit-key').value = '';
|
|
271
|
+
document.getElementById('new-key').value = '';
|
|
272
|
+
document.getElementById('edit-description').value = '';
|
|
273
|
+
document.getElementById('edit-enabled').checked = false;
|
|
274
|
+
document.getElementById('edit-rollout').value = '0';
|
|
275
|
+
document.getElementById('edit-allow-users').value = '';
|
|
276
|
+
document.getElementById('edit-allow-orgs').value = '';
|
|
277
|
+
document.getElementById('edit-deny-users').value = '';
|
|
278
|
+
document.getElementById('edit-deny-orgs').value = '';
|
|
279
|
+
document.getElementById('edit-payload').value = 'null';
|
|
280
|
+
|
|
281
|
+
document.getElementById('key-input-container').style.display = 'block';
|
|
282
|
+
document.getElementById('edit-modal').classList.remove('hidden');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function editFlag(key) {
|
|
286
|
+
const flag = allFlags.find((f) => f.key === key);
|
|
287
|
+
if (!flag) return;
|
|
288
|
+
|
|
289
|
+
document.getElementById('modal-title').textContent = 'Edit Flag';
|
|
290
|
+
document.getElementById('edit-mode').value = 'edit';
|
|
291
|
+
document.getElementById('edit-key').value = key;
|
|
292
|
+
document.getElementById('new-key').value = key;
|
|
293
|
+
document.getElementById('edit-description').value = flag.description || '';
|
|
294
|
+
document.getElementById('edit-enabled').checked = Boolean(flag.enabled);
|
|
295
|
+
document.getElementById('edit-rollout').value = String(Number(flag.rolloutPercentage || 0));
|
|
296
|
+
document.getElementById('edit-allow-users').value = arrayToCsv(flag.allowListUserIds);
|
|
297
|
+
document.getElementById('edit-allow-orgs').value = arrayToCsv(flag.allowListOrgIds);
|
|
298
|
+
document.getElementById('edit-deny-users').value = arrayToCsv(flag.denyListUserIds);
|
|
299
|
+
document.getElementById('edit-deny-orgs').value = arrayToCsv(flag.denyListOrgIds);
|
|
300
|
+
document.getElementById('edit-payload').value = JSON.stringify(flag.payload ?? null, null, 2);
|
|
301
|
+
|
|
302
|
+
document.getElementById('key-input-container').style.display = 'none';
|
|
303
|
+
document.getElementById('edit-modal').classList.remove('hidden');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function closeModal() {
|
|
307
|
+
document.getElementById('edit-modal').classList.add('hidden');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function handleSubmit(event) {
|
|
311
|
+
event.preventDefault();
|
|
312
|
+
|
|
313
|
+
const mode = document.getElementById('edit-mode').value;
|
|
314
|
+
const key = mode === 'create' ? document.getElementById('new-key').value.trim() : document.getElementById('edit-key').value;
|
|
315
|
+
|
|
316
|
+
const description = document.getElementById('edit-description').value.trim();
|
|
317
|
+
const enabled = document.getElementById('edit-enabled').checked;
|
|
318
|
+
const rolloutPercentage = Number(document.getElementById('edit-rollout').value || 0);
|
|
319
|
+
|
|
320
|
+
let payload = null;
|
|
321
|
+
const payloadRaw = document.getElementById('edit-payload').value;
|
|
322
|
+
try {
|
|
323
|
+
payload = payloadRaw && payloadRaw.trim() ? JSON.parse(payloadRaw) : null;
|
|
324
|
+
} catch (e) {
|
|
325
|
+
showToast(`Invalid payload JSON: ${e.message}`, 'error');
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const body = {
|
|
330
|
+
key,
|
|
331
|
+
description,
|
|
332
|
+
enabled,
|
|
333
|
+
rolloutPercentage,
|
|
334
|
+
allowListUserIds: csvToArray(document.getElementById('edit-allow-users').value),
|
|
335
|
+
allowListOrgIds: csvToArray(document.getElementById('edit-allow-orgs').value),
|
|
336
|
+
denyListUserIds: csvToArray(document.getElementById('edit-deny-users').value),
|
|
337
|
+
denyListOrgIds: csvToArray(document.getElementById('edit-deny-orgs').value),
|
|
338
|
+
payload,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
try {
|
|
342
|
+
const response = await fetch(`${API_BASE}/api/admin/feature-flags${mode === 'edit' ? `/${encodeURIComponent(key)}` : ''}`, {
|
|
343
|
+
method: mode === 'edit' ? 'PUT' : 'POST',
|
|
344
|
+
headers: { 'Content-Type': 'application/json' },
|
|
345
|
+
body: JSON.stringify(body),
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const data = await response.json();
|
|
349
|
+
if (!response.ok) {
|
|
350
|
+
throw new Error(data?.error || 'Operation failed');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
showToast(mode === 'edit' ? 'Flag updated successfully' : 'Flag created successfully');
|
|
354
|
+
closeModal();
|
|
355
|
+
await loadFlags();
|
|
356
|
+
} catch (error) {
|
|
357
|
+
showToast(error.message, 'error');
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function deleteFlag(key) {
|
|
362
|
+
if (!confirm(`Are you sure you want to delete flag "${key}"? This cannot be undone.`)) return;
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
const response = await fetch(`${API_BASE}/api/admin/feature-flags/${encodeURIComponent(key)}`, { method: 'DELETE' });
|
|
366
|
+
const data = await response.json();
|
|
367
|
+
|
|
368
|
+
if (!response.ok) {
|
|
369
|
+
throw new Error(data?.error || 'Failed to delete flag');
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
showToast('Flag deleted successfully');
|
|
373
|
+
await loadFlags();
|
|
374
|
+
} catch (error) {
|
|
375
|
+
showToast(error.message, 'error');
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
loadFlags();
|
|
380
|
+
</script>
|
|
381
|
+
<script>
|
|
382
|
+
window.addEventListener("keydown", (e) => {
|
|
383
|
+
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
|
|
384
|
+
e.preventDefault();
|
|
385
|
+
window.parent.postMessage({ type: "keydown", ctrlK: true }, "*");
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
</script>
|
|
389
|
+
</body>
|
|
390
|
+
</html>
|