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