@aimeloic/monkey-tester 4.0.5 → 4.0.9
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/htmlTemplate.js +459 -363
- package/monkey.backup.js +1 -1
- package/package.json +1 -1
package/htmlTemplate.js
CHANGED
|
@@ -1,13 +1,33 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
//
|
|
3
|
+
// ─── Unicode-safe base64 helpers ────────────────────────────────────────────
|
|
4
|
+
// Use these on the CALLER side to produce endpointsJsonB64:
|
|
5
|
+
// import { encodePayload } from './ui.js';
|
|
6
|
+
// const endpointsJsonB64 = encodePayload(endpointsData);
|
|
7
|
+
//
|
|
8
|
+
function encodePayload(obj) {
|
|
9
|
+
return btoa(unescape(encodeURIComponent(JSON.stringify(obj))));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// The reciprocal decode runs inside each HTML template (see _decode below).
|
|
13
|
+
// It is injected as a one-liner so every template is self-contained.
|
|
14
|
+
const _decode = `function _decode(b64){return JSON.parse(decodeURIComponent(escape(atob(b64.replace(/\\s+/g,'')))));}`;
|
|
15
|
+
|
|
16
|
+
// ─── Runtime sandbox (injected into tester page) ─────────────────────────────
|
|
4
17
|
function runtimeClientSandbox() {
|
|
5
|
-
|
|
18
|
+
// Unicode-safe decode (whitespace-stripped for safety)
|
|
19
|
+
function _decode(b64) {
|
|
20
|
+
return JSON.parse(decodeURIComponent(escape(atob(b64.replace(/\s+/g, '')))));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ENDPOINTS = _decode(
|
|
24
|
+
document.getElementById('__monkey_data__').getAttribute('data-payload')
|
|
25
|
+
);
|
|
26
|
+
|
|
6
27
|
let currentKey = null;
|
|
7
28
|
|
|
8
29
|
document.getElementById('base-url').value = window.location.origin;
|
|
9
30
|
|
|
10
|
-
// Auto-paste Token directly if it exists in local storage
|
|
11
31
|
const savedToken = localStorage.getItem('__auth_token__');
|
|
12
32
|
if (savedToken) {
|
|
13
33
|
document.getElementById('jwt-input').value = savedToken;
|
|
@@ -25,7 +45,9 @@ function runtimeClientSandbox() {
|
|
|
25
45
|
const item = document.createElement('div');
|
|
26
46
|
item.className = 'nav-item' + (i === 0 ? ' active' : '');
|
|
27
47
|
item.setAttribute('data-key', key);
|
|
28
|
-
item.innerHTML =
|
|
48
|
+
item.innerHTML =
|
|
49
|
+
'<span class="method-badge ' + ep.method + '">' + ep.method + '</span>' +
|
|
50
|
+
'<span class="nav-label">' + ep.path + '</span>';
|
|
29
51
|
item.addEventListener('click', () => {
|
|
30
52
|
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
31
53
|
item.classList.add('active');
|
|
@@ -43,14 +65,17 @@ function runtimeClientSandbox() {
|
|
|
43
65
|
const main = document.getElementById('main-panel');
|
|
44
66
|
if (!ep) return;
|
|
45
67
|
|
|
46
|
-
let html =
|
|
47
|
-
|
|
48
|
-
|
|
68
|
+
let html =
|
|
69
|
+
'<div class="endpoint-title">' + ep.title + '</div>' +
|
|
70
|
+
'<div class="endpoint-path"><span class="method-badge ' + ep.method + '">' + ep.method + '</span><span>' + ep.path + '</span></div>' +
|
|
71
|
+
'<div class="endpoint-desc">' + ep.desc + '</div>';
|
|
49
72
|
|
|
50
73
|
if (ep.params && ep.params.length) {
|
|
51
74
|
html += '<div class="form-section"><div class="form-section-title">Path Parameters</div>';
|
|
52
75
|
ep.params.forEach(p => {
|
|
53
|
-
html +=
|
|
76
|
+
html +=
|
|
77
|
+
'<div class="field-row"><label class="field-label">' + p.label + '</label>' +
|
|
78
|
+
'<input type="text" id="param-' + p.name + '" placeholder="' + p.placeholder + '" /></div>';
|
|
54
79
|
});
|
|
55
80
|
html += '</div>';
|
|
56
81
|
}
|
|
@@ -58,12 +83,19 @@ function runtimeClientSandbox() {
|
|
|
58
83
|
if (ep.fields && ep.fields.length) {
|
|
59
84
|
html += '<div class="form-section"><div class="form-section-title">HTTP JSON Payload Parameters</div>';
|
|
60
85
|
ep.fields.forEach(f => {
|
|
61
|
-
html +=
|
|
86
|
+
html +=
|
|
87
|
+
'<div class="field-row"><label class="field-label">' + f.label + '</label>' +
|
|
88
|
+
'<input type="' + (f.type || 'text') + '" id="field-' + f.name + '" placeholder="' + (f.placeholder || '') + '" /></div>';
|
|
62
89
|
});
|
|
63
90
|
html += '</div>';
|
|
64
91
|
}
|
|
65
92
|
|
|
66
|
-
html +=
|
|
93
|
+
html +=
|
|
94
|
+
'<div class="btn-row">' +
|
|
95
|
+
'<button class="btn" id="_exec_btn">Execute Route</button>' +
|
|
96
|
+
'<button class="btn btn-secondary" id="_clear_btn">Clear Context</button>' +
|
|
97
|
+
'</div>';
|
|
98
|
+
|
|
67
99
|
main.innerHTML = html;
|
|
68
100
|
document.getElementById('_exec_btn').addEventListener('click', sendRequest);
|
|
69
101
|
document.getElementById('_clear_btn').addEventListener('click', clearResponse);
|
|
@@ -72,6 +104,7 @@ function runtimeClientSandbox() {
|
|
|
72
104
|
async function sendRequest() {
|
|
73
105
|
const ep = ENDPOINTS[currentKey];
|
|
74
106
|
let path = ep.path;
|
|
107
|
+
|
|
75
108
|
if (ep.params && ep.params.length) {
|
|
76
109
|
for (const p of ep.params) {
|
|
77
110
|
const val = (document.getElementById('param-' + p.name) || {}).value || '';
|
|
@@ -79,6 +112,7 @@ function runtimeClientSandbox() {
|
|
|
79
112
|
path = path.replace(':' + p.name, encodeURIComponent(val.trim()));
|
|
80
113
|
}
|
|
81
114
|
}
|
|
115
|
+
|
|
82
116
|
const baseUrl = document.getElementById('base-url').value.replace(/\/+$/, '');
|
|
83
117
|
const url = baseUrl + path;
|
|
84
118
|
const headers = { 'Content-Type': 'application/json' };
|
|
@@ -113,10 +147,10 @@ function runtimeClientSandbox() {
|
|
|
113
147
|
|
|
114
148
|
function setResponse(data, state, status, ms) {
|
|
115
149
|
const badge = document.getElementById('status-badge');
|
|
116
|
-
const body
|
|
150
|
+
const body = document.getElementById('response-body');
|
|
117
151
|
if (state === 'loading') {
|
|
118
152
|
badge.className = 'status-badge status-idle'; badge.textContent = '…';
|
|
119
|
-
body.className
|
|
153
|
+
body.className = 'response-body empty'; body.textContent = 'Executing transmission…';
|
|
120
154
|
return;
|
|
121
155
|
}
|
|
122
156
|
badge.className = 'status-badge ' + (state === 'ok' ? 'status-ok' : 'status-err');
|
|
@@ -130,76 +164,87 @@ function runtimeClientSandbox() {
|
|
|
130
164
|
document.getElementById('status-badge').className = 'status-badge status-idle';
|
|
131
165
|
document.getElementById('status-badge').textContent = '—';
|
|
132
166
|
const body = document.getElementById('response-body');
|
|
133
|
-
body.className = 'response-body empty';
|
|
167
|
+
body.className = 'response-body empty';
|
|
168
|
+
body.textContent = 'Execute a request row to generate feedback data';
|
|
134
169
|
}
|
|
135
170
|
|
|
136
171
|
function highlight(str) {
|
|
137
|
-
return str
|
|
138
|
-
.replace(
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
172
|
+
return str
|
|
173
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
174
|
+
.replace(
|
|
175
|
+
/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false)\b|\bnull\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g,
|
|
176
|
+
m => {
|
|
177
|
+
if (/^"/.test(m)) return /:$/.test(m)
|
|
178
|
+
? '<span class="json-key">' + m + '</span>'
|
|
179
|
+
: '<span class="json-str">' + m + '</span>';
|
|
180
|
+
if (/true|false/.test(m)) return '<span class="json-bool">' + m + '</span>';
|
|
181
|
+
if (/null/.test(m)) return '<span class="json-null">' + m + '</span>';
|
|
182
|
+
return '<span class="json-num">' + m + '</span>';
|
|
183
|
+
}
|
|
184
|
+
);
|
|
144
185
|
}
|
|
145
186
|
|
|
146
187
|
function showToast(msg) {
|
|
147
|
-
const t = document.getElementById('toast');
|
|
188
|
+
const t = document.getElementById('toast');
|
|
189
|
+
t.textContent = msg;
|
|
190
|
+
t.classList.add('show');
|
|
148
191
|
setTimeout(() => t.classList.remove('show'), 2500);
|
|
149
192
|
}
|
|
150
193
|
|
|
151
194
|
buildSidebar();
|
|
152
195
|
}
|
|
153
196
|
|
|
154
|
-
//
|
|
197
|
+
// ─── UI templates ─────────────────────────────────────────────────────────────
|
|
155
198
|
const UI = {
|
|
199
|
+
|
|
200
|
+
// ── Tester sandbox ──────────────────────────────────────────────────────────
|
|
156
201
|
tester: (endpointsJsonB64) => `<!DOCTYPE html>
|
|
157
202
|
<html lang="en">
|
|
158
203
|
<head>
|
|
159
204
|
<meta charset="UTF-8"><title>Endtester — Environment Hub</title>
|
|
160
205
|
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
|
|
161
206
|
<style>
|
|
162
|
-
:root { --bg
|
|
163
|
-
*
|
|
164
|
-
body { background:
|
|
165
|
-
header { border-bottom:
|
|
166
|
-
.logo { font-family:
|
|
167
|
-
.logo span { color:
|
|
168
|
-
.header-right { margin-left:
|
|
169
|
-
.jwt-wrap
|
|
170
|
-
.jwt-wrap label
|
|
171
|
-
#jwt-input
|
|
172
|
-
.layout { display:
|
|
173
|
-
aside { border-right:
|
|
174
|
-
.section-label { font-size:
|
|
175
|
-
.nav-item { display:
|
|
176
|
-
.nav-item.active { border-left-color:
|
|
177
|
-
.method-badge { font-family:
|
|
178
|
-
.GET
|
|
179
|
-
.nav-label { font-size:
|
|
180
|
-
main { overflow-y:
|
|
181
|
-
.endpoint-title { font-family:
|
|
182
|
-
.endpoint-path { font-family:
|
|
183
|
-
.endpoint-desc { color:
|
|
184
|
-
.form-section { background:
|
|
185
|
-
.form-section-title { font-size:
|
|
186
|
-
.field-row { display:
|
|
187
|
-
.field-label { font-family:
|
|
188
|
-
input[type=text],
|
|
189
|
-
.btn-row { margin-top:
|
|
190
|
-
.btn { background:
|
|
191
|
-
.btn-secondary { background:
|
|
192
|
-
.response-panel { border-left:
|
|
193
|
-
.response-header { padding:
|
|
194
|
-
.response-header-title { font-size:
|
|
195
|
-
.status-badge { font-family:
|
|
196
|
-
.status-ok
|
|
197
|
-
.response-body { flex:
|
|
198
|
-
.response-body.empty { color:
|
|
199
|
-
.json-render-block { display:
|
|
200
|
-
.json-key
|
|
201
|
-
#toast { position:
|
|
202
|
-
#toast.show { opacity:
|
|
207
|
+
:root { --bg:#0e0c09; --surface:#181510; --surface2:#221d14; --border:#3a3020; --accent:#e8a838; --text:#f0e8d8; --text-dim:#9a8c78; --red:#d45c3c; --green:#6ba05a; --blue:#5a86c0; --radius:8px; }
|
|
208
|
+
* { box-sizing:border-box; margin:0; padding:0; }
|
|
209
|
+
body { background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif; font-size:14px; height:100vh; overflow:hidden; background-image:radial-gradient(ellipse 80% 60% at 50% -20%,#3a2a0a22 0%,transparent 70%); }
|
|
210
|
+
header { border-bottom:1px solid var(--border); padding:16px 32px; display:flex; align-items:center; gap:20px; background:#0e0c09ee; backdrop-filter:blur(8px); height:65px; }
|
|
211
|
+
.logo { font-family:'Playfair Display',serif; font-size:20px; color:var(--accent); }
|
|
212
|
+
.logo span { color:var(--text-dim); font-size:11px; font-family:'DM Mono',monospace; margin-left:8px; }
|
|
213
|
+
.header-right { margin-left:auto; display:flex; align-items:center; gap:16px; }
|
|
214
|
+
.jwt-wrap,.base-url-wrap { display:flex; align-items:center; gap:8px; }
|
|
215
|
+
.jwt-wrap label,.base-url-wrap label { color:var(--text-dim); font-size:11px; font-family:'DM Mono',monospace; }
|
|
216
|
+
#jwt-input,#base-url { background:var(--surface2); border:1px solid var(--border); color:var(--text); font-family:'DM Mono',monospace; font-size:12px; padding:6px 12px; border-radius:var(--radius); width:220px; outline:none; }
|
|
217
|
+
.layout { display:grid; grid-template-columns:280px 1fr 450px; height:calc(100vh - 65px); overflow:hidden; }
|
|
218
|
+
aside { border-right:1px solid var(--border); overflow-y:auto; padding:16px 0; background:#0b0907; }
|
|
219
|
+
.section-label { font-size:10px; font-family:'DM Mono',monospace; color:var(--text-dim); text-transform:uppercase; padding:12px 18px 6px; }
|
|
220
|
+
.nav-item { display:flex; align-items:center; gap:10px; padding:10px 18px; cursor:pointer; border-left:2px solid transparent; color:var(--text-dim); }
|
|
221
|
+
.nav-item.active { border-left-color:var(--accent); background:var(--surface); color:var(--accent); }
|
|
222
|
+
.method-badge { font-family:'DM Mono',monospace; font-size:9px; font-weight:600; padding:2px 6px; border-radius:4px; min-width:52px; text-align:center; }
|
|
223
|
+
.GET{background:#1a3a22;color:#6ba05a;} .POST{background:#1a2e3a;color:#5a86c0;} .PUT{background:#3a2e10;color:#e8a838;} .DELETE{background:#3a1a14;color:#d45c3c;}
|
|
224
|
+
.nav-label { font-size:12px; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
225
|
+
main { overflow-y:auto; padding:32px; background:#0e0c09; }
|
|
226
|
+
.endpoint-title { font-family:'Playfair Display',serif; font-size:24px; color:var(--accent); margin-bottom:8px; }
|
|
227
|
+
.endpoint-path { font-family:'DM Mono',monospace; font-size:13px; color:var(--text-dim); margin-bottom:24px; display:flex; align-items:center; gap:8px; }
|
|
228
|
+
.endpoint-desc { color:var(--text-dim); font-size:13px; line-height:1.6; margin-bottom:24px; border-left:2px solid var(--border); padding-left:12px; }
|
|
229
|
+
.form-section { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; margin-bottom:20px; }
|
|
230
|
+
.form-section-title { font-size:11px; font-family:'DM Mono',monospace; color:var(--text-dim); text-transform:uppercase; margin-bottom:16px; border-bottom:1px solid var(--border); padding-bottom:6px; }
|
|
231
|
+
.field-row { display:grid; grid-template-columns:150px 1fr; align-items:center; gap:16px; margin-bottom:14px; }
|
|
232
|
+
.field-label { font-family:'DM Mono',monospace; font-size:12px; color:var(--text-dim); text-align:right; }
|
|
233
|
+
input[type=text],input[type=password],input[type=number] { background:var(--surface2); border:1px solid var(--border); color:var(--text); font-size:13px; padding:8px 12px; border-radius:var(--radius); width:100%; outline:none; }
|
|
234
|
+
.btn-row { margin-top:24px; display:flex; gap:12px; }
|
|
235
|
+
.btn { background:var(--accent); color:#0e0c09; border:none; padding:10px 24px; border-radius:var(--radius); font-size:13px; font-weight:500; cursor:pointer; }
|
|
236
|
+
.btn-secondary { background:var(--surface2); color:var(--text-dim); border:1px solid var(--border); }
|
|
237
|
+
.response-panel { border-left:1px solid var(--border); display:flex; flex-direction:column; background:#110e0a; }
|
|
238
|
+
.response-header { padding:16px 20px; border-bottom:1px solid var(--border); display:flex; align-items:center; background:var(--surface); height:50px; }
|
|
239
|
+
.response-header-title { font-size:11px; font-family:'DM Mono',monospace; color:var(--text-dim); text-transform:uppercase; }
|
|
240
|
+
.status-badge { font-family:'DM Mono',monospace; font-size:12px; margin-left:auto; padding:2px 8px; border-radius:4px; }
|
|
241
|
+
.status-ok{background:#1a3a22;color:#6ba05a;} .status-err{background:#3a1a14;color:#d45c3c;} .status-idle{background:var(--surface2);color:var(--text-dim);}
|
|
242
|
+
.response-body { flex:1; overflow-y:auto; padding:0; background:#0d0b08; }
|
|
243
|
+
.response-body.empty { color:var(--text-dim); display:flex; align-items:center; justify-content:center; padding:20px; font-size:13px; }
|
|
244
|
+
.json-render-block { display:block; padding:20px; font-family:'DM Mono',monospace; font-size:12px; line-height:1.5; white-space:pre; }
|
|
245
|
+
.json-key{color:#e8a838;} .json-str{color:#9ab878;} .json-num{color:#5a86c0;} .json-bool{color:#c47a1e;} .json-null{color:var(--text-dim);}
|
|
246
|
+
#toast { position:fixed; bottom:24px; right:24px; background:var(--surface2); border:1px solid var(--border); padding:10px 18px; border-radius:var(--radius); opacity:0; transition:all .25s; font-family:'DM Mono',monospace; font-size:12px; color:var(--accent); }
|
|
247
|
+
#toast.show { opacity:1; }
|
|
203
248
|
</style>
|
|
204
249
|
</head>
|
|
205
250
|
<body>
|
|
@@ -224,320 +269,371 @@ const UI = {
|
|
|
224
269
|
</body>
|
|
225
270
|
</html>`,
|
|
226
271
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
dashboard: (endpointsJsonB64) => `<!DOCTYPE html><html><head><title>Dynamic Admin Dashboard</title>
|
|
276
|
-
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
277
|
-
<style>
|
|
278
|
-
:root { --bg: #0e0c09; --surface: #181510; --surface2: #221d14; --border: #3a3020; --accent: #e8a838; --text: #f0e8d8; --text-dim: #9a8c78; --red: #d45c3c; --green: #6ba05a; }
|
|
279
|
-
body { background: var(--bg); color: var(--text); font-family: 'DM Sans', sans-serif; padding: 40px; margin: 0; }
|
|
280
|
-
header { display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--border); padding-bottom: 20px; margin-bottom: 30px; }
|
|
281
|
-
h1 { color: var(--accent); margin: 0; font-size: 24px; }
|
|
282
|
-
.nav-links { display: flex; gap: 16px; align-items: center; }
|
|
283
|
-
.nav-links a { color: var(--text-dim); text-decoration: none; font-size: 14px; }
|
|
284
|
-
.nav-links a:hover { color: var(--accent); }
|
|
285
|
-
.logout-btn { background: #3a1a14; color: var(--red); border: 1px solid #5a2014; padding: 8px 16px; border-radius: 6px; cursor: pointer; font-weight:600; }
|
|
286
|
-
.selector-banner { background: var(--surface); border: 1px solid var(--border); padding: 12px 20px; border-radius: 8px; margin-bottom: 24px; display: flex; align-items: center; gap: 12px; }
|
|
287
|
-
select { background: var(--surface2); border: 1px solid var(--border); color: var(--text); padding: 8px 12px; border-radius: 6px; outline: none; font-weight: 500; }
|
|
288
|
-
.grid { display: grid; grid-template-columns: 360px 1fr; gap: 30px; }
|
|
289
|
-
.panel { background: var(--surface); border: 1px solid var(--border); padding: 24px; border-radius: 8px; height: fit-content; }
|
|
290
|
-
h3 { color: var(--accent); margin-top: 0; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 8px; font-size:14px; text-transform:uppercase; letter-spacing:0.5px; }
|
|
291
|
-
.field { margin-bottom: 16px; }
|
|
292
|
-
label { display: block; font-size: 11px; color: var(--text-dim); text-transform: uppercase; margin-bottom: 6px; font-family: monospace; }
|
|
293
|
-
input { background: var(--surface2); border: 1px solid var(--border); color: var(--text); padding: 10px; width: 100%; box-sizing: border-box; border-radius: 6px; outline: none; }
|
|
294
|
-
.btn { background: var(--accent); color: #0e0c09; border: none; padding: 11px; width: 100%; border-radius: 6px; font-weight: 700; cursor: pointer; }
|
|
295
|
-
.btn-cancel { background: var(--surface2); color: var(--text-dim); border: 1px solid var(--border); margin-top: 8px; }
|
|
296
|
-
.table-wrap { overflow-x: auto; background: var(--surface); border-radius: 8px; border: 1px solid var(--border); }
|
|
297
|
-
table { width: 100%; border-collapse: collapse; }
|
|
298
|
-
th { color: var(--text-dim); text-align: left; padding: 14px; border-bottom: 2px solid var(--border); font-size: 11px; text-transform: uppercase; background:#13100b; }
|
|
299
|
-
td { padding: 14px; border-bottom: 1px solid var(--surface2); font-size: 13px; font-family: monospace; }
|
|
300
|
-
.actions-cell { display: flex; gap: 8px; justify-content: flex-end; }
|
|
301
|
-
.btn-sm { padding: 4px 8px; border-radius: 4px; border: none; font-weight: 600; font-size: 11px; cursor: pointer; }
|
|
302
|
-
.btn-edit { background: #1a2e3a; color: #5a86c0; border: 1px solid #224054; }
|
|
303
|
-
.btn-del { background: #3a1a14; color: var(--red); border: 1px solid #5a2014; }
|
|
304
|
-
</style>
|
|
305
|
-
</head>
|
|
306
|
-
<body>
|
|
307
|
-
<div id="__monkey_data__" data-payload="${endpointsJsonB64}" style="display:none;"></div>
|
|
308
|
-
<header>
|
|
309
|
-
<h1>🐒 Universal Management Dashboard</h1>
|
|
310
|
-
<div class="nav-links">
|
|
311
|
-
<a href="/api/tester" target="_blank">🛠 Open Tester Sandbox</a>
|
|
312
|
-
<button class="logout-btn" onclick="localStorage.removeItem('__auth_token__'); window.location.href='/login'">Log Out</button>
|
|
313
|
-
</div>
|
|
314
|
-
</header>
|
|
315
|
-
|
|
316
|
-
<div class="selector-banner">
|
|
317
|
-
<label style="margin:0;">Target Data Resource Collection:</label>
|
|
318
|
-
<select id="route-selector" onchange="switchCollection()"></select>
|
|
319
|
-
</div>
|
|
320
|
-
|
|
321
|
-
<div class="grid">
|
|
322
|
-
<div class="panel">
|
|
323
|
-
<h3 id="form-title">Add Entry</h3>
|
|
324
|
-
<div id="dynamic-fields-container"></div>
|
|
325
|
-
<button class="btn" id="btn-submit" onclick="submitDataForm()">Execute Submission</button>
|
|
326
|
-
<button class="btn btn-sm btn-cancel" id="btn-cancel" style="display:none; margin-top:10px;" onclick="resetDataForm()">Cancel Action</button>
|
|
327
|
-
</div>
|
|
272
|
+
// ── Login page ──────────────────────────────────────────────────────────────
|
|
273
|
+
login: () => `<!DOCTYPE html>
|
|
274
|
+
<html>
|
|
275
|
+
<head>
|
|
276
|
+
<title>Sign In</title>
|
|
277
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
278
|
+
<style>
|
|
279
|
+
body { background:#0e0c09; color:#f0e8d8; font-family:'DM Sans',sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; }
|
|
280
|
+
.card { background:#181510; border:1px solid #3a3020; padding:40px; border-radius:12px; width:340px; }
|
|
281
|
+
h2 { color:#e8a838; margin:0 0 24px; text-align:center; }
|
|
282
|
+
.field { margin-bottom:20px; }
|
|
283
|
+
label { display:block; font-size:11px; color:#9a8c78; text-transform:uppercase; margin-bottom:8px; }
|
|
284
|
+
input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
|
|
285
|
+
button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; }
|
|
286
|
+
.footer { text-align:center; margin-top:20px; font-size:13px; color:#9a8c78; }
|
|
287
|
+
a { color:#e8a838; text-decoration:none; }
|
|
288
|
+
#err { color:#d45c3c; font-size:13px; margin-bottom:15px; text-align:center; min-height:18px; }
|
|
289
|
+
</style>
|
|
290
|
+
</head>
|
|
291
|
+
<body>
|
|
292
|
+
<div class="card">
|
|
293
|
+
<h2>Sign In</h2>
|
|
294
|
+
<div id="err"></div>
|
|
295
|
+
<div class="field"><label>Email</label><input type="email" id="email" value="admin@bakery.com"></div>
|
|
296
|
+
<div class="field"><label>Password</label><input type="password" id="password" value="password123"></div>
|
|
297
|
+
<button onclick="handleLogin()">Log In</button>
|
|
298
|
+
<div class="footer">Need an account? <a href="/signup">Sign up</a></div>
|
|
299
|
+
</div>
|
|
300
|
+
<script>
|
|
301
|
+
async function handleLogin() {
|
|
302
|
+
const email = document.getElementById('email').value;
|
|
303
|
+
const password = document.getElementById('password').value;
|
|
304
|
+
const res = await fetch('/api/v1/auth/login', {
|
|
305
|
+
method: 'POST',
|
|
306
|
+
headers: { 'Content-Type': 'application/json' },
|
|
307
|
+
body: JSON.stringify({ email, password })
|
|
308
|
+
});
|
|
309
|
+
const data = await res.json();
|
|
310
|
+
if (res.ok && data.token) {
|
|
311
|
+
localStorage.setItem('__auth_token__', data.token);
|
|
312
|
+
window.location.href = '/dashboard';
|
|
313
|
+
} else {
|
|
314
|
+
document.getElementById('err').textContent = data.error || 'Login failed';
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
</script>
|
|
318
|
+
</body>
|
|
319
|
+
</html>`,
|
|
328
320
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
switchCollection(selector.options[0].value);
|
|
383
|
-
}
|
|
384
|
-
}
|
|
321
|
+
// ── Signup page ─────────────────────────────────────────────────────────────
|
|
322
|
+
signup: () => `<!DOCTYPE html>
|
|
323
|
+
<html>
|
|
324
|
+
<head>
|
|
325
|
+
<title>Create Account</title>
|
|
326
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
327
|
+
<style>
|
|
328
|
+
body { background:#0e0c09; color:#f0e8d8; font-family:'DM Sans',sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; }
|
|
329
|
+
.card { background:#181510; border:1px solid #3a3020; padding:40px; border-radius:12px; width:340px; }
|
|
330
|
+
h2 { color:#e8a838; margin:0 0 24px; text-align:center; }
|
|
331
|
+
.field { margin-bottom:20px; }
|
|
332
|
+
label { display:block; font-size:11px; color:#9a8c78; text-transform:uppercase; margin-bottom:8px; }
|
|
333
|
+
input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
|
|
334
|
+
button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; }
|
|
335
|
+
.footer { text-align:center; margin-top:20px; font-size:13px; color:#9a8c78; }
|
|
336
|
+
a { color:#e8a838; text-decoration:none; }
|
|
337
|
+
#msg { font-size:13px; margin-bottom:15px; text-align:center; min-height:18px; }
|
|
338
|
+
</style>
|
|
339
|
+
</head>
|
|
340
|
+
<body>
|
|
341
|
+
<div class="card">
|
|
342
|
+
<h2>Sign Up</h2>
|
|
343
|
+
<div id="msg"></div>
|
|
344
|
+
<div class="field"><label>Username</label><input type="text" id="username" placeholder="Username"></div>
|
|
345
|
+
<div class="field"><label>Email Address</label><input type="email" id="email" placeholder="Email"></div>
|
|
346
|
+
<div class="field"><label>Password</label><input type="password" id="password"></div>
|
|
347
|
+
<button onclick="handleRegister()">Register Account</button>
|
|
348
|
+
<div class="footer">Have an account? <a href="/login">Sign In</a></div>
|
|
349
|
+
</div>
|
|
350
|
+
<script>
|
|
351
|
+
async function handleRegister() {
|
|
352
|
+
const username = document.getElementById('username').value;
|
|
353
|
+
const email = document.getElementById('email').value;
|
|
354
|
+
const password = document.getElementById('password').value;
|
|
355
|
+
const msgDiv = document.getElementById('msg');
|
|
356
|
+
const res = await fetch('/api/v1/auth/register', {
|
|
357
|
+
method: 'POST',
|
|
358
|
+
headers: { 'Content-Type': 'application/json' },
|
|
359
|
+
body: JSON.stringify({ username, email, password })
|
|
360
|
+
});
|
|
361
|
+
const data = await res.json();
|
|
362
|
+
if (res.ok) {
|
|
363
|
+
msgDiv.style.color = '#6ba05a';
|
|
364
|
+
msgDiv.textContent = 'Registration complete! Redirecting…';
|
|
365
|
+
setTimeout(() => window.location.href = '/login', 1200);
|
|
366
|
+
} else {
|
|
367
|
+
msgDiv.style.color = '#d45c3c';
|
|
368
|
+
msgDiv.textContent = data.error || 'Registration failed';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
</script>
|
|
372
|
+
</body>
|
|
373
|
+
</html>`,
|
|
385
374
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
375
|
+
// ── Dashboard ────────────────────────────────────────────────────────────────
|
|
376
|
+
dashboard: (endpointsJsonB64) => `<!DOCTYPE html>
|
|
377
|
+
<html>
|
|
378
|
+
<head>
|
|
379
|
+
<title>Dynamic Admin Dashboard</title>
|
|
380
|
+
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
|
|
381
|
+
<style>
|
|
382
|
+
:root { --bg:#0e0c09; --surface:#181510; --surface2:#221d14; --border:#3a3020; --accent:#e8a838; --text:#f0e8d8; --text-dim:#9a8c78; --red:#d45c3c; --green:#6ba05a; }
|
|
383
|
+
body { background:var(--bg); color:var(--text); font-family:'DM Sans',sans-serif; padding:40px; margin:0; }
|
|
384
|
+
header { display:flex; justify-content:space-between; align-items:center; border-bottom:1px solid var(--border); padding-bottom:20px; margin-bottom:30px; }
|
|
385
|
+
h1 { color:var(--accent); margin:0; font-size:24px; }
|
|
386
|
+
.nav-links { display:flex; gap:16px; align-items:center; }
|
|
387
|
+
.nav-links a { color:var(--text-dim); text-decoration:none; font-size:14px; }
|
|
388
|
+
.nav-links a:hover { color:var(--accent); }
|
|
389
|
+
.logout-btn { background:#3a1a14; color:var(--red); border:1px solid #5a2014; padding:8px 16px; border-radius:6px; cursor:pointer; font-weight:600; }
|
|
390
|
+
.selector-banner { background:var(--surface); border:1px solid var(--border); padding:12px 20px; border-radius:8px; margin-bottom:24px; display:flex; align-items:center; gap:12px; }
|
|
391
|
+
select { background:var(--surface2); border:1px solid var(--border); color:var(--text); padding:8px 12px; border-radius:6px; outline:none; font-weight:500; }
|
|
392
|
+
.grid { display:grid; grid-template-columns:360px 1fr; gap:30px; }
|
|
393
|
+
.panel { background:var(--surface); border:1px solid var(--border); padding:24px; border-radius:8px; height:fit-content; }
|
|
394
|
+
h3 { color:var(--accent); margin-top:0; margin-bottom:20px; border-bottom:1px solid var(--border); padding-bottom:8px; font-size:14px; text-transform:uppercase; letter-spacing:0.5px; }
|
|
395
|
+
.field { margin-bottom:16px; }
|
|
396
|
+
label { display:block; font-size:11px; color:var(--text-dim); text-transform:uppercase; margin-bottom:6px; font-family:monospace; }
|
|
397
|
+
input { background:var(--surface2); border:1px solid var(--border); color:var(--text); padding:10px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
|
|
398
|
+
.btn { background:var(--accent); color:#0e0c09; border:none; padding:11px; width:100%; border-radius:6px; font-weight:700; cursor:pointer; }
|
|
399
|
+
.btn-cancel { background:var(--surface2); color:var(--text-dim); border:1px solid var(--border); margin-top:8px; }
|
|
400
|
+
.table-wrap { overflow-x:auto; background:var(--surface); border-radius:8px; border:1px solid var(--border); }
|
|
401
|
+
table { width:100%; border-collapse:collapse; }
|
|
402
|
+
th { color:var(--text-dim); text-align:left; padding:14px; border-bottom:2px solid var(--border); font-size:11px; text-transform:uppercase; background:#13100b; }
|
|
403
|
+
td { padding:14px; border-bottom:1px solid var(--surface2); font-size:13px; font-family:monospace; }
|
|
404
|
+
.actions-cell { display:flex; gap:8px; justify-content:flex-end; }
|
|
405
|
+
.btn-sm { padding:4px 8px; border-radius:4px; border:none; font-weight:600; font-size:11px; cursor:pointer; }
|
|
406
|
+
.btn-edit { background:#1a2e3a; color:#5a86c0; border:1px solid #224054; }
|
|
407
|
+
.btn-del { background:#3a1a14; color:var(--red); border:1px solid #5a2014; }
|
|
408
|
+
</style>
|
|
409
|
+
</head>
|
|
410
|
+
<body>
|
|
411
|
+
<div id="__monkey_data__" data-payload="${endpointsJsonB64}" style="display:none;"></div>
|
|
393
412
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
413
|
+
<header>
|
|
414
|
+
<h1>Universal Management Dashboard</h1>
|
|
415
|
+
<div class="nav-links">
|
|
416
|
+
<a href="/api/tester" target="_blank">🛠 Open Tester Sandbox</a>
|
|
417
|
+
<button class="logout-btn" onclick="localStorage.removeItem('__auth_token__'); window.location.href='/login'">Log Out</button>
|
|
418
|
+
</div>
|
|
419
|
+
</header>
|
|
398
420
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
421
|
+
<div class="selector-banner">
|
|
422
|
+
<label style="margin:0;">Target Data Resource Collection:</label>
|
|
423
|
+
<select id="route-selector" onchange="switchCollection()"></select>
|
|
424
|
+
</div>
|
|
403
425
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
426
|
+
<div class="grid">
|
|
427
|
+
<div class="panel">
|
|
428
|
+
<h3 id="form-title">Add Entry</h3>
|
|
429
|
+
<div id="dynamic-fields-container"></div>
|
|
430
|
+
<button class="btn" id="btn-submit" onclick="submitDataForm()">Execute Submission</button>
|
|
431
|
+
<button class="btn btn-sm btn-cancel" id="btn-cancel" style="display:none; margin-top:10px;" onclick="resetDataForm()">Cancel Action</button>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="table-wrap">
|
|
434
|
+
<table id="dynamic-table">
|
|
435
|
+
<thead id="table-head"></thead>
|
|
436
|
+
<tbody id="table-body"></tbody>
|
|
437
|
+
</table>
|
|
438
|
+
</div>
|
|
439
|
+
</div>
|
|
413
440
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
try {
|
|
421
|
-
const res = await fetch(activeCollectionPath, { headers: { 'Authorization': 'Bearer ' + token } });
|
|
422
|
-
const rawData = await res.json();
|
|
423
|
-
|
|
424
|
-
// Flatten standard array wrapping logic safely
|
|
425
|
-
let list = [];
|
|
426
|
-
if (Array.isArray(rawData)) list = rawData;
|
|
427
|
-
else if (rawData && typeof rawData === 'object') {
|
|
428
|
-
const arrayKey = Object.keys(rawData).find(k => Array.isArray(rawData[k]));
|
|
429
|
-
list = arrayKey ? rawData[arrayKey] : [rawData];
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
if (!list || list.length === 0 || list[0] === null) {
|
|
433
|
-
body.innerHTML = '<tr><td style="padding:30px; color:var(--text-dim)">No records found inside this live API container.</td></tr>';
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
// Gather unique keys from database entities dynamically
|
|
438
|
-
let keys = Object.keys(list[0]).filter(k => typeof list[0][k] !== 'object');
|
|
439
|
-
|
|
440
|
-
// Generate headers
|
|
441
|
-
let trHead = '<tr>';
|
|
442
|
-
keys.forEach(k => trHead += \`<th>\${k}</th>\`);
|
|
443
|
-
trHead += '<th style="text-align:right; padding-right:20px;">Actions</th></tr>';
|
|
444
|
-
head.innerHTML = trHead;
|
|
445
|
-
|
|
446
|
-
// Populate row fields
|
|
447
|
-
body.innerHTML = '';
|
|
448
|
-
list.forEach(row => {
|
|
449
|
-
let trBody = '<tr>';
|
|
450
|
-
keys.forEach(k => {
|
|
451
|
-
const val = row[k] !== undefined ? row[k] : '';
|
|
452
|
-
trBody += \`<td>\${val}</td>\`;
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
const targetId = row.id || row._id || list.indexOf(row);
|
|
456
|
-
const rowJson = btoa(JSON.stringify(row));
|
|
457
|
-
|
|
458
|
-
trBody += \`
|
|
459
|
-
<td class="actions-cell" style="padding-right:20px;">
|
|
460
|
-
\${dynamicCollections[activeCollectionPath].put ? \`<button class="btn-sm btn-edit" onclick="startRowEdit('\${targetId}', '\${rowJson}')">Edit</button>\` : ''}
|
|
461
|
-
\${dynamicCollections[activeCollectionPath].del ? \`<button class="btn-sm btn-del" onclick="deleteRow('\${targetId}')">Delete</button>\` : ''}
|
|
462
|
-
</td>
|
|
463
|
-
</tr>\`;
|
|
464
|
-
body.innerHTML += trBody;
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
} catch (e) {
|
|
468
|
-
body.innerHTML = '<tr><td style="padding:20px; color:var(--red)">Failed processing remote endpoint structure data.</td></tr>';
|
|
469
|
-
}
|
|
470
|
-
}
|
|
441
|
+
<script>
|
|
442
|
+
// ── Unicode-safe decode ──────────────────────────────────────────────────────
|
|
443
|
+
function _decode(b64) {
|
|
444
|
+
return JSON.parse(decodeURIComponent(escape(atob(b64.replace(/\\s+/g, '')))));
|
|
445
|
+
}
|
|
471
446
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
if (el) {
|
|
479
|
-
payload[f.name] = f.type === 'number' ? Number(el.value) : el.value;
|
|
480
|
-
}
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
let url = activeCollectionPath;
|
|
484
|
-
let method = 'POST';
|
|
485
|
-
|
|
486
|
-
if (activeEditId !== null) {
|
|
487
|
-
const putTemplate = dynamicCollections[activeCollectionPath].put;
|
|
488
|
-
const paramName = putTemplate.split('/:')[1];
|
|
489
|
-
url = putTemplate.replace(\`:\${paramName}\`, activeEditId);
|
|
490
|
-
method = 'PUT';
|
|
491
|
-
}
|
|
447
|
+
const ENDPOINTS = _decode(
|
|
448
|
+
document.getElementById('__monkey_data__').getAttribute('data-payload')
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
const token = localStorage.getItem('__auth_token__');
|
|
452
|
+
if (!token) window.location.href = '/login';
|
|
492
453
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
body: JSON.stringify(payload)
|
|
497
|
-
});
|
|
454
|
+
let dynamicCollections = {};
|
|
455
|
+
let activeCollectionPath = '';
|
|
456
|
+
let activeEditId = null;
|
|
498
457
|
|
|
499
|
-
|
|
458
|
+
function resolveRoutes() {
|
|
459
|
+
Object.values(ENDPOINTS).forEach(ep => {
|
|
460
|
+
if (!ep.path.includes(':')) {
|
|
461
|
+
if (!dynamicCollections[ep.path]) {
|
|
462
|
+
dynamicCollections[ep.path] = { get: null, post: null, put: null, del: null, modelFields: ep.fields || [] };
|
|
463
|
+
}
|
|
464
|
+
if (ep.method === 'GET') dynamicCollections[ep.path].get = ep.path;
|
|
465
|
+
if (ep.method === 'POST') {
|
|
466
|
+
dynamicCollections[ep.path].post = ep.path;
|
|
467
|
+
if (ep.fields && ep.fields.length) dynamicCollections[ep.path].modelFields = ep.fields;
|
|
500
468
|
}
|
|
469
|
+
} else {
|
|
470
|
+
const basePath = ep.path.split('/:')[0];
|
|
471
|
+
if (!dynamicCollections[basePath]) {
|
|
472
|
+
dynamicCollections[basePath] = { get: null, post: null, put: null, del: null, modelFields: [] };
|
|
473
|
+
}
|
|
474
|
+
if (ep.method === 'PUT') dynamicCollections[basePath].put = ep.path;
|
|
475
|
+
if (ep.method === 'DELETE') dynamicCollections[basePath].del = ep.path;
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
const selector = document.getElementById('route-selector');
|
|
480
|
+
Object.keys(dynamicCollections).forEach(path => {
|
|
481
|
+
if (dynamicCollections[path].get) {
|
|
482
|
+
const opt = document.createElement('option');
|
|
483
|
+
opt.value = path;
|
|
484
|
+
opt.textContent = path + ' (Dynamic Dataset)';
|
|
485
|
+
selector.appendChild(opt);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
501
488
|
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
const delTemplate = dynamicCollections[activeCollectionPath].del;
|
|
505
|
-
const paramName = delTemplate.split('/:')[1];
|
|
506
|
-
const url = delTemplate.replace(\`:\${paramName}\`, id);
|
|
489
|
+
if (selector.options.length > 0) switchCollection(selector.options[0].value);
|
|
490
|
+
}
|
|
507
491
|
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
492
|
+
function switchCollection(targetPath) {
|
|
493
|
+
activeCollectionPath = targetPath || document.getElementById('route-selector').value;
|
|
494
|
+
activeEditId = null;
|
|
495
|
+
resetDataForm();
|
|
496
|
+
renderFormFields();
|
|
497
|
+
fetchData();
|
|
498
|
+
}
|
|
511
499
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
document.getElementById('btn-submit').textContent = 'Commit Changes';
|
|
517
|
-
document.getElementById('btn-cancel').style.display = 'block';
|
|
518
|
-
|
|
519
|
-
const fields = dynamicCollections[activeCollectionPath].modelFields;
|
|
520
|
-
fields.forEach(f => {
|
|
521
|
-
const el = document.getElementById(\`input-\${f.name}\`);
|
|
522
|
-
if (el && data[f.name] !== undefined) el.value = data[f.name];
|
|
523
|
-
});
|
|
524
|
-
}
|
|
500
|
+
function renderFormFields() {
|
|
501
|
+
const container = document.getElementById('dynamic-fields-container');
|
|
502
|
+
container.innerHTML = '';
|
|
503
|
+
const fields = dynamicCollections[activeCollectionPath].modelFields;
|
|
525
504
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
505
|
+
if (!fields || fields.length === 0) {
|
|
506
|
+
container.innerHTML = '<p style="font-size:12px;color:var(--text-dim);">No input properties found.</p>';
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
fields.forEach(f => {
|
|
511
|
+
container.innerHTML += \`
|
|
512
|
+
<div class="field">
|
|
513
|
+
<label>\${f.label}</label>
|
|
514
|
+
<input type="\${f.type || 'text'}" id="input-\${f.name}" placeholder="\${f.placeholder || ''}">
|
|
515
|
+
</div>
|
|
516
|
+
\`;
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
async function fetchData() {
|
|
521
|
+
const head = document.getElementById('table-head');
|
|
522
|
+
const body = document.getElementById('table-body');
|
|
523
|
+
head.innerHTML = '';
|
|
524
|
+
body.innerHTML = '<tr><td style="padding:20px;color:var(--text-dim)">Loading layout schemas…</td></tr>';
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const res = await fetch(activeCollectionPath, { headers: { 'Authorization': 'Bearer ' + token } });
|
|
528
|
+
const rawData = await res.json();
|
|
529
|
+
|
|
530
|
+
let list = [];
|
|
531
|
+
if (Array.isArray(rawData)) {
|
|
532
|
+
list = rawData;
|
|
533
|
+
} else if (rawData && typeof rawData === 'object') {
|
|
534
|
+
const arrayKey = Object.keys(rawData).find(k => Array.isArray(rawData[k]));
|
|
535
|
+
list = arrayKey ? rawData[arrayKey] : [rawData];
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (!list || list.length === 0 || list[0] === null) {
|
|
539
|
+
body.innerHTML = '<tr><td style="padding:30px;color:var(--text-dim)">No records found inside this live API container.</td></tr>';
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const keys = Object.keys(list[0]).filter(k => typeof list[0][k] !== 'object');
|
|
544
|
+
|
|
545
|
+
head.innerHTML = '<tr>' +
|
|
546
|
+
keys.map(k => \`<th>\${k}</th>\`).join('') +
|
|
547
|
+
'<th style="text-align:right;padding-right:20px;">Actions</th></tr>';
|
|
548
|
+
|
|
549
|
+
body.innerHTML = '';
|
|
550
|
+
list.forEach(row => {
|
|
551
|
+
const targetId = row.id || row._id || list.indexOf(row);
|
|
552
|
+
const rowJson = btoa(unescape(encodeURIComponent(JSON.stringify(row))));
|
|
553
|
+
const col = dynamicCollections[activeCollectionPath];
|
|
554
|
+
|
|
555
|
+
body.innerHTML +=
|
|
556
|
+
'<tr>' +
|
|
557
|
+
keys.map(k => \`<td>\${row[k] !== undefined ? row[k] : ''}</td>\`).join('') +
|
|
558
|
+
\`<td class="actions-cell" style="padding-right:20px;">
|
|
559
|
+
\${col.put ? \`<button class="btn-sm btn-edit" onclick="startRowEdit('\${targetId}','\${rowJson}')">Edit</button>\` : ''}
|
|
560
|
+
\${col.del ? \`<button class="btn-sm btn-del" onclick="deleteRow('\${targetId}')">Delete</button>\` : ''}
|
|
561
|
+
</td></tr>\`;
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
} catch (e) {
|
|
565
|
+
body.innerHTML = '<tr><td style="padding:20px;color:var(--red)">Failed processing remote endpoint structure data.</td></tr>';
|
|
566
|
+
console.error(e);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
async function submitDataForm() {
|
|
571
|
+
const fields = dynamicCollections[activeCollectionPath].modelFields;
|
|
572
|
+
const payload = {};
|
|
573
|
+
fields.forEach(f => {
|
|
574
|
+
const el = document.getElementById(\`input-\${f.name}\`);
|
|
575
|
+
if (el) payload[f.name] = f.type === 'number' ? Number(el.value) : el.value;
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
let url = activeCollectionPath;
|
|
579
|
+
let method = 'POST';
|
|
580
|
+
|
|
581
|
+
if (activeEditId !== null) {
|
|
582
|
+
const putTemplate = dynamicCollections[activeCollectionPath].put;
|
|
583
|
+
const paramName = putTemplate.split('/:')[1];
|
|
584
|
+
url = putTemplate.replace(\`:\${paramName}\`, activeEditId);
|
|
585
|
+
method = 'PUT';
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const res = await fetch(url, {
|
|
589
|
+
method,
|
|
590
|
+
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token },
|
|
591
|
+
body: JSON.stringify(payload)
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
if (res.ok) { resetDataForm(); fetchData(); }
|
|
595
|
+
else { alert('Submission declined.'); }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async function deleteRow(id) {
|
|
599
|
+
if (!confirm('Execute atomic entry removal?')) return;
|
|
600
|
+
const delTemplate = dynamicCollections[activeCollectionPath].del;
|
|
601
|
+
const paramName = delTemplate.split('/:')[1];
|
|
602
|
+
const url = delTemplate.replace(\`:\${paramName}\`, id);
|
|
603
|
+
const res = await fetch(url, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
|
|
604
|
+
if (res.ok) fetchData(); else alert('Delete request blocked.');
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function startRowEdit(id, encodedJson) {
|
|
608
|
+
activeEditId = id;
|
|
609
|
+
// Unicode-safe decode for row data encoded above with btoa(unescape(encodeURIComponent(...)))
|
|
610
|
+
const data = JSON.parse(decodeURIComponent(escape(atob(encodedJson))));
|
|
611
|
+
document.getElementById('form-title').textContent = \`Update Row #\${id}\`;
|
|
612
|
+
document.getElementById('btn-submit').textContent = 'Commit Changes';
|
|
613
|
+
document.getElementById('btn-cancel').style.display = 'block';
|
|
614
|
+
const fields = dynamicCollections[activeCollectionPath].modelFields;
|
|
615
|
+
fields.forEach(f => {
|
|
616
|
+
const el = document.getElementById(\`input-\${f.name}\`);
|
|
617
|
+
if (el && data[f.name] !== undefined) el.value = data[f.name];
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function resetDataForm() {
|
|
622
|
+
activeEditId = null;
|
|
623
|
+
document.getElementById('form-title').textContent = 'Add Entry';
|
|
624
|
+
document.getElementById('btn-submit').textContent = 'Execute Submission';
|
|
625
|
+
document.getElementById('btn-cancel').style.display = 'none';
|
|
626
|
+
(dynamicCollections[activeCollectionPath]?.modelFields || []).forEach(f => {
|
|
627
|
+
const el = document.getElementById(\`input-\${f.name}\`);
|
|
628
|
+
if (el) el.value = '';
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
resolveRoutes();
|
|
633
|
+
</script>
|
|
634
|
+
</body>
|
|
635
|
+
</html>`
|
|
537
636
|
|
|
538
|
-
resolveRoutes();
|
|
539
|
-
</script>
|
|
540
|
-
</body></html>`
|
|
541
637
|
};
|
|
542
638
|
|
|
543
|
-
export { UI };
|
|
639
|
+
export { UI, encodePayload };
|
package/monkey.backup.js
CHANGED