@fias/plugin-dev-harness 1.5.2 → 1.5.4
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/dist/server/harness-server.d.ts.map +1 -1
- package/dist/server/harness-server.js +7 -1
- package/dist/server/harness-server.js.map +1 -1
- package/dist/server/static/harness.css +255 -8
- package/dist/server/static/harness.html +80 -11
- package/dist/server/static/harness.js +390 -93
- package/package.json +1 -1
- package/dist/server/static/static/harness.css +0 -642
- package/dist/server/static/static/harness.html +0 -139
- package/dist/server/static/static/harness.js +0 -1090
|
@@ -1,1090 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Fias Arche Dev — Client-side Bridge Host
|
|
3
|
-
*
|
|
4
|
-
* Handles postMessage communication with the plugin iframe,
|
|
5
|
-
* enforcing permissions and rate limits, and proxying server-side
|
|
6
|
-
* operations through the harness Express server.
|
|
7
|
-
*/
|
|
8
|
-
(function () {
|
|
9
|
-
'use strict';
|
|
10
|
-
|
|
11
|
-
var iframe = document.getElementById('plugin-iframe');
|
|
12
|
-
var consoleBody = document.getElementById('console-body');
|
|
13
|
-
var consoleCount = document.getElementById('console-count');
|
|
14
|
-
var consoleToggle = document.getElementById('console-toggle');
|
|
15
|
-
var creditBalance = document.getElementById('credit-balance');
|
|
16
|
-
var themeToggle = document.getElementById('theme-toggle');
|
|
17
|
-
var reloadBtn = document.getElementById('reload-btn');
|
|
18
|
-
var modeBadge = document.getElementById('mode-badge');
|
|
19
|
-
var pluginStatus = document.getElementById('plugin-status');
|
|
20
|
-
var themeBadge = document.getElementById('theme-badge');
|
|
21
|
-
var loginModal = document.getElementById('login-modal');
|
|
22
|
-
var loginEmail = document.getElementById('login-email');
|
|
23
|
-
var loginPassword = document.getElementById('login-password');
|
|
24
|
-
var loginError = document.getElementById('login-error');
|
|
25
|
-
var loginSubmit = document.getElementById('login-submit');
|
|
26
|
-
var loginCancel = document.getElementById('login-cancel');
|
|
27
|
-
var envSelector = document.getElementById('env-selector');
|
|
28
|
-
|
|
29
|
-
var messageCount = 0;
|
|
30
|
-
var currentTheme = 'dark';
|
|
31
|
-
var currentMode = 'mock';
|
|
32
|
-
var currentEnvironment = 'staging';
|
|
33
|
-
var hasCredentials = false;
|
|
34
|
-
var cachedConfig = null;
|
|
35
|
-
|
|
36
|
-
/** Permission requirements per bridge call type (matches production) */
|
|
37
|
-
var PERMISSION_MAP = {
|
|
38
|
-
get_user: 'user:profile:read',
|
|
39
|
-
get_theme: 'theme:read',
|
|
40
|
-
entity_invoke: 'entities:invoke',
|
|
41
|
-
image_generate: 'entities:image_generate',
|
|
42
|
-
storage_read: 'storage:sandbox',
|
|
43
|
-
storage_write: 'storage:sandbox',
|
|
44
|
-
storage_list: 'storage:sandbox',
|
|
45
|
-
storage_delete: 'storage:sandbox',
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/** Rate limits per message type (matches production) */
|
|
49
|
-
var RATE_LIMITS = {
|
|
50
|
-
entity_invoke: { maxPerMinute: 60 },
|
|
51
|
-
image_generate: { maxPerMinute: 10 },
|
|
52
|
-
storage_write: { maxPerMinute: 120 },
|
|
53
|
-
storage_read: { maxPerMinute: 300 },
|
|
54
|
-
storage_list: { maxPerMinute: 60 },
|
|
55
|
-
storage_delete: { maxPerMinute: 60 },
|
|
56
|
-
};
|
|
57
|
-
var rateBuckets = {};
|
|
58
|
-
|
|
59
|
-
// ────────────────────────────────────────────────────────────────
|
|
60
|
-
// Initialization
|
|
61
|
-
// ────────────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
fetchConfig().then(function (config) {
|
|
64
|
-
cachedConfig = config;
|
|
65
|
-
currentTheme = config.mockTheme || 'dark';
|
|
66
|
-
currentMode = config.mode || 'mock';
|
|
67
|
-
currentEnvironment = config.environment || 'staging';
|
|
68
|
-
hasCredentials = config.hasCredentials || false;
|
|
69
|
-
|
|
70
|
-
envSelector.value = currentEnvironment;
|
|
71
|
-
updateThemeBadge();
|
|
72
|
-
updateModeBadge();
|
|
73
|
-
|
|
74
|
-
if (currentMode === 'live') {
|
|
75
|
-
creditBalance.style.display = 'inline';
|
|
76
|
-
fetchCredits();
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
checkPluginReachable(config.pluginUrl, function (reachable) {
|
|
80
|
-
if (reachable) {
|
|
81
|
-
pluginStatus.classList.add('hidden');
|
|
82
|
-
iframe.classList.remove('hidden');
|
|
83
|
-
iframe.src = config.pluginUrl;
|
|
84
|
-
} else {
|
|
85
|
-
showPluginError(config.pluginUrl);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
// ────────────────────────────────────────────────────────────────
|
|
91
|
-
// UI Controls
|
|
92
|
-
// ────────────────────────────────────────────────────────────────
|
|
93
|
-
|
|
94
|
-
consoleToggle.addEventListener('click', function () {
|
|
95
|
-
consoleBody.classList.toggle('open');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
reloadBtn.addEventListener('click', function () {
|
|
99
|
-
if (cachedConfig) {
|
|
100
|
-
pluginStatus.classList.remove('hidden', 'error');
|
|
101
|
-
pluginStatus.innerHTML =
|
|
102
|
-
'<div class="status-spinner"></div><p>Connecting to plugin server...</p>';
|
|
103
|
-
iframe.classList.add('hidden');
|
|
104
|
-
checkPluginReachable(cachedConfig.pluginUrl, function (reachable) {
|
|
105
|
-
if (reachable) {
|
|
106
|
-
pluginStatus.classList.add('hidden');
|
|
107
|
-
iframe.classList.remove('hidden');
|
|
108
|
-
iframe.src = cachedConfig.pluginUrl;
|
|
109
|
-
} else {
|
|
110
|
-
showPluginError(cachedConfig.pluginUrl);
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
} else {
|
|
114
|
-
iframe.src = iframe.src;
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
themeToggle.addEventListener('click', function () {
|
|
119
|
-
currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
120
|
-
updateThemeBadge();
|
|
121
|
-
sendToPlugin({
|
|
122
|
-
type: 'theme_update',
|
|
123
|
-
messageId: 'theme_' + Date.now(),
|
|
124
|
-
payload: getTheme(),
|
|
125
|
-
});
|
|
126
|
-
logMessage('send', 'theme_update', { mode: currentTheme });
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
// Mode toggle
|
|
130
|
-
modeBadge.addEventListener('click', function () {
|
|
131
|
-
if (currentMode === 'mock') {
|
|
132
|
-
if (!hasCredentials) {
|
|
133
|
-
showLoginModal();
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
switchMode('live');
|
|
137
|
-
} else {
|
|
138
|
-
switchMode('mock');
|
|
139
|
-
}
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// Environment selector
|
|
143
|
-
envSelector.addEventListener('change', function () {
|
|
144
|
-
fetch('/api/environment', {
|
|
145
|
-
method: 'POST',
|
|
146
|
-
headers: { 'Content-Type': 'application/json' },
|
|
147
|
-
body: JSON.stringify({ environment: envSelector.value }),
|
|
148
|
-
})
|
|
149
|
-
.then(function (r) {
|
|
150
|
-
return r.json();
|
|
151
|
-
})
|
|
152
|
-
.then(function (data) {
|
|
153
|
-
currentEnvironment = data.environment;
|
|
154
|
-
currentMode = data.mode;
|
|
155
|
-
hasCredentials = data.hasCredentials;
|
|
156
|
-
updateModeBadge();
|
|
157
|
-
|
|
158
|
-
if (currentMode === 'live') {
|
|
159
|
-
creditBalance.style.display = 'inline';
|
|
160
|
-
fetchCredits();
|
|
161
|
-
} else {
|
|
162
|
-
creditBalance.style.display = 'none';
|
|
163
|
-
creditBalance.textContent = '';
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
logMessage(
|
|
167
|
-
'info',
|
|
168
|
-
'Environment: ' +
|
|
169
|
-
currentEnvironment.toUpperCase() +
|
|
170
|
-
(hasCredentials ? '' : ' (not authenticated)'),
|
|
171
|
-
);
|
|
172
|
-
})
|
|
173
|
-
.catch(function (err) {
|
|
174
|
-
logMessage('error', err.message);
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
function switchMode(newMode) {
|
|
179
|
-
fetch('/api/mode', {
|
|
180
|
-
method: 'POST',
|
|
181
|
-
headers: { 'Content-Type': 'application/json' },
|
|
182
|
-
body: JSON.stringify({ mode: newMode }),
|
|
183
|
-
})
|
|
184
|
-
.then(function (r) {
|
|
185
|
-
if (!r.ok) {
|
|
186
|
-
return r.json().then(function (err) {
|
|
187
|
-
throw new Error(err.error || 'Failed to switch mode');
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
return r.json();
|
|
191
|
-
})
|
|
192
|
-
.then(function (data) {
|
|
193
|
-
currentMode = data.mode;
|
|
194
|
-
updateModeBadge();
|
|
195
|
-
logMessage('info', 'Mode switched to ' + currentMode.toUpperCase());
|
|
196
|
-
|
|
197
|
-
if (currentMode === 'live') {
|
|
198
|
-
creditBalance.style.display = 'inline';
|
|
199
|
-
fetchCredits();
|
|
200
|
-
} else {
|
|
201
|
-
creditBalance.style.display = 'none';
|
|
202
|
-
creditBalance.textContent = '';
|
|
203
|
-
}
|
|
204
|
-
})
|
|
205
|
-
.catch(function (err) {
|
|
206
|
-
logMessage('error', err.message);
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ────────────────────────────────────────────────────────────────
|
|
211
|
-
// Login Modal
|
|
212
|
-
// ────────────────────────────────────────────────────────────────
|
|
213
|
-
|
|
214
|
-
var loginTarget = document.getElementById('login-target');
|
|
215
|
-
|
|
216
|
-
function showLoginModal() {
|
|
217
|
-
loginModal.style.display = 'flex';
|
|
218
|
-
loginEmail.value = '';
|
|
219
|
-
loginPassword.value = '';
|
|
220
|
-
loginError.style.display = 'none';
|
|
221
|
-
loginSubmit.disabled = false;
|
|
222
|
-
loginSubmit.textContent = 'Sign in';
|
|
223
|
-
loginTarget.textContent = currentEnvironment === 'production' ? 'fias.io' : 'staging.fias.io';
|
|
224
|
-
loginEmail.focus();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function hideLoginModal() {
|
|
228
|
-
loginModal.style.display = 'none';
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
loginCancel.addEventListener('click', hideLoginModal);
|
|
232
|
-
|
|
233
|
-
loginModal.addEventListener('click', function (e) {
|
|
234
|
-
if (e.target === loginModal) hideLoginModal();
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
loginEmail.addEventListener('keydown', function (e) {
|
|
238
|
-
if (e.key === 'Enter') loginPassword.focus();
|
|
239
|
-
if (e.key === 'Escape') hideLoginModal();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
loginPassword.addEventListener('keydown', function (e) {
|
|
243
|
-
if (e.key === 'Enter') submitLogin();
|
|
244
|
-
if (e.key === 'Escape') hideLoginModal();
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
loginSubmit.addEventListener('click', submitLogin);
|
|
248
|
-
|
|
249
|
-
function submitLogin() {
|
|
250
|
-
var email = loginEmail.value.trim();
|
|
251
|
-
var password = loginPassword.value;
|
|
252
|
-
|
|
253
|
-
if (!email || !password) {
|
|
254
|
-
loginError.textContent = 'Email and password are required.';
|
|
255
|
-
loginError.style.display = 'block';
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
loginSubmit.disabled = true;
|
|
260
|
-
loginSubmit.textContent = 'Signing in...';
|
|
261
|
-
loginError.style.display = 'none';
|
|
262
|
-
|
|
263
|
-
fetch('/api/auth/login', {
|
|
264
|
-
method: 'POST',
|
|
265
|
-
headers: { 'Content-Type': 'application/json' },
|
|
266
|
-
body: JSON.stringify({ email: email, password: password }),
|
|
267
|
-
})
|
|
268
|
-
.then(function (r) {
|
|
269
|
-
if (!r.ok) {
|
|
270
|
-
return r.json().then(function (err) {
|
|
271
|
-
throw new Error(err.error || 'Sign in failed');
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
return r.json();
|
|
275
|
-
})
|
|
276
|
-
.then(function () {
|
|
277
|
-
hasCredentials = true;
|
|
278
|
-
hideLoginModal();
|
|
279
|
-
logMessage('info', 'Signed in for ' + currentEnvironment.toUpperCase());
|
|
280
|
-
switchMode('live');
|
|
281
|
-
})
|
|
282
|
-
.catch(function (err) {
|
|
283
|
-
loginError.textContent = err.message;
|
|
284
|
-
loginError.style.display = 'block';
|
|
285
|
-
})
|
|
286
|
-
.finally(function () {
|
|
287
|
-
loginSubmit.disabled = false;
|
|
288
|
-
loginSubmit.textContent = 'Sign in';
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// ────────────────────────────────────────────────────────────────
|
|
293
|
-
// Message Handling
|
|
294
|
-
// ────────────────────────────────────────────────────────────────
|
|
295
|
-
|
|
296
|
-
window.addEventListener('message', function (event) {
|
|
297
|
-
if (event.source !== iframe.contentWindow) return;
|
|
298
|
-
|
|
299
|
-
var data = event.data;
|
|
300
|
-
if (!data || typeof data !== 'object' || !data.type || !data.messageId) return;
|
|
301
|
-
|
|
302
|
-
logMessage('recv', data.type, data.payload);
|
|
303
|
-
|
|
304
|
-
if (data.type === 'ready') return;
|
|
305
|
-
|
|
306
|
-
if (data.type === 'resize') {
|
|
307
|
-
var height = data.payload && data.payload.height;
|
|
308
|
-
if (typeof height === 'number' && height > 0) {
|
|
309
|
-
iframe.style.height = height + 'px';
|
|
310
|
-
iframe.style.flex = 'none';
|
|
311
|
-
}
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (data.type === 'toast') {
|
|
316
|
-
var msg = data.payload && data.payload.message;
|
|
317
|
-
if (typeof msg === 'string') {
|
|
318
|
-
logMessage('toast', msg, data.payload);
|
|
319
|
-
}
|
|
320
|
-
return;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if (data.type === 'navigate') {
|
|
324
|
-
var navPath = data.payload && data.payload.path;
|
|
325
|
-
if (typeof navPath === 'string') {
|
|
326
|
-
logMessage('nav', navPath);
|
|
327
|
-
}
|
|
328
|
-
return;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
handleRequest(data);
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
function handleRequest(data) {
|
|
335
|
-
try {
|
|
336
|
-
var requiredPerm = PERMISSION_MAP[data.type];
|
|
337
|
-
if (requiredPerm && cachedConfig && cachedConfig.permissions.indexOf(requiredPerm) === -1) {
|
|
338
|
-
throw new Error('Permission denied: ' + requiredPerm + ' not granted');
|
|
339
|
-
}
|
|
340
|
-
checkRateLimit(data.type);
|
|
341
|
-
} catch (err) {
|
|
342
|
-
logMessage('error', err.message);
|
|
343
|
-
sendToPlugin({
|
|
344
|
-
type: 'response',
|
|
345
|
-
messageId: data.messageId,
|
|
346
|
-
payload: null,
|
|
347
|
-
error: err.message,
|
|
348
|
-
});
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Streaming path for entity_invoke with stream: true
|
|
353
|
-
if (data.type === 'entity_invoke' && data.payload && data.payload.stream) {
|
|
354
|
-
handleStreamingRequest(data);
|
|
355
|
-
return;
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
fetch('/api/bridge', {
|
|
359
|
-
method: 'POST',
|
|
360
|
-
headers: { 'Content-Type': 'application/json' },
|
|
361
|
-
body: JSON.stringify({ type: data.type, payload: data.payload }),
|
|
362
|
-
})
|
|
363
|
-
.then(function (response) {
|
|
364
|
-
if (!response.ok) {
|
|
365
|
-
return response.json().then(function (err) {
|
|
366
|
-
throw new Error(err.error || 'Bridge call failed');
|
|
367
|
-
});
|
|
368
|
-
}
|
|
369
|
-
return response.json();
|
|
370
|
-
})
|
|
371
|
-
.then(function (result) {
|
|
372
|
-
logMessage('send', 'response', result);
|
|
373
|
-
|
|
374
|
-
if (
|
|
375
|
-
data.type === 'entity_invoke' &&
|
|
376
|
-
result.metadata &&
|
|
377
|
-
result.metadata.cost > 0
|
|
378
|
-
) {
|
|
379
|
-
logMessage('cost', 'Credits used: ' + result.metadata.cost.toFixed(4));
|
|
380
|
-
fetchCredits();
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
sendToPlugin({
|
|
384
|
-
type: 'response',
|
|
385
|
-
messageId: data.messageId,
|
|
386
|
-
payload: result,
|
|
387
|
-
});
|
|
388
|
-
})
|
|
389
|
-
.catch(function (err) {
|
|
390
|
-
logMessage('error', err.message);
|
|
391
|
-
sendToPlugin({
|
|
392
|
-
type: 'response',
|
|
393
|
-
messageId: data.messageId,
|
|
394
|
-
payload: null,
|
|
395
|
-
error: err.message,
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
function handleStreamingRequest(data) {
|
|
401
|
-
fetch('/api/bridge/stream', {
|
|
402
|
-
method: 'POST',
|
|
403
|
-
headers: { 'Content-Type': 'application/json' },
|
|
404
|
-
body: JSON.stringify({ type: data.type, payload: data.payload }),
|
|
405
|
-
})
|
|
406
|
-
.then(function (response) {
|
|
407
|
-
if (!response.ok) {
|
|
408
|
-
return response.json().then(function (err) {
|
|
409
|
-
throw new Error(err.error || 'Bridge streaming call failed');
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
var contentType = response.headers.get('content-type') || '';
|
|
414
|
-
if (contentType.indexOf('text/event-stream') === -1) {
|
|
415
|
-
// Fallback: not SSE, parse as JSON
|
|
416
|
-
return response.json().then(function (result) {
|
|
417
|
-
sendToPlugin({ type: 'response', messageId: data.messageId, payload: result });
|
|
418
|
-
});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
// Parse SSE stream
|
|
422
|
-
var reader = response.body.getReader();
|
|
423
|
-
var decoder = new TextDecoder();
|
|
424
|
-
var buffer = '';
|
|
425
|
-
var finalPayload = null;
|
|
426
|
-
|
|
427
|
-
function pump() {
|
|
428
|
-
return reader.read().then(function (chunk) {
|
|
429
|
-
if (chunk.done) {
|
|
430
|
-
if (finalPayload) {
|
|
431
|
-
logMessage('send', 'response', finalPayload);
|
|
432
|
-
sendToPlugin({ type: 'response', messageId: data.messageId, payload: finalPayload });
|
|
433
|
-
}
|
|
434
|
-
return;
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
438
|
-
var lines = buffer.split('\n');
|
|
439
|
-
buffer = lines.pop() || '';
|
|
440
|
-
|
|
441
|
-
for (var i = 0; i < lines.length; i++) {
|
|
442
|
-
var line = lines[i];
|
|
443
|
-
if (line.indexOf('data: ') !== 0) continue;
|
|
444
|
-
try {
|
|
445
|
-
var parsed = JSON.parse(line.slice(6));
|
|
446
|
-
if (parsed.error) throw new Error(parsed.error);
|
|
447
|
-
if (parsed.text) {
|
|
448
|
-
sendToPlugin({ type: 'stream_token', messageId: data.messageId, text: parsed.text });
|
|
449
|
-
}
|
|
450
|
-
if (parsed.done) {
|
|
451
|
-
finalPayload = { output: parsed.output, metadata: parsed.metadata };
|
|
452
|
-
if (parsed.metadata && parsed.metadata.cost > 0) {
|
|
453
|
-
logMessage('cost', 'Credits used: ' + parsed.metadata.cost.toFixed(4));
|
|
454
|
-
fetchCredits();
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
} catch (e) {
|
|
458
|
-
if (e instanceof SyntaxError) continue;
|
|
459
|
-
throw e;
|
|
460
|
-
}
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
return pump();
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
return pump();
|
|
468
|
-
})
|
|
469
|
-
.catch(function (err) {
|
|
470
|
-
logMessage('error', err.message);
|
|
471
|
-
sendToPlugin({
|
|
472
|
-
type: 'response',
|
|
473
|
-
messageId: data.messageId,
|
|
474
|
-
payload: null,
|
|
475
|
-
error: err.message,
|
|
476
|
-
});
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
// ────────────────────────────────────────────────────────────────
|
|
481
|
-
// Publish Wizard
|
|
482
|
-
// ────────────────────────────────────────────────────────────────
|
|
483
|
-
|
|
484
|
-
var publishBtn = document.getElementById('publish-btn');
|
|
485
|
-
var publishModal = document.getElementById('publish-modal');
|
|
486
|
-
var pubCancel = document.getElementById('pub-cancel');
|
|
487
|
-
var pubBack = document.getElementById('pub-back');
|
|
488
|
-
var pubNext = document.getElementById('pub-next');
|
|
489
|
-
var pubError = document.getElementById('pub-error');
|
|
490
|
-
var pubStep = 1;
|
|
491
|
-
var pubManifest = null;
|
|
492
|
-
var pubSubmissionId = null;
|
|
493
|
-
var pubPollTimer = null;
|
|
494
|
-
var pubValidationPassed = false;
|
|
495
|
-
|
|
496
|
-
publishBtn.addEventListener('click', function () {
|
|
497
|
-
if (currentMode !== 'live' || !hasCredentials) {
|
|
498
|
-
pubError.textContent = 'Switch to live mode and sign in to publish.';
|
|
499
|
-
pubError.style.display = 'block';
|
|
500
|
-
publishModal.style.display = 'flex';
|
|
501
|
-
showPublishStep(1);
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
pubError.style.display = 'none';
|
|
505
|
-
fetch('/api/manifest')
|
|
506
|
-
.then(function (r) {
|
|
507
|
-
if (!r.ok) throw new Error('fias-plugin.json not found');
|
|
508
|
-
return r.json();
|
|
509
|
-
})
|
|
510
|
-
.then(function (manifest) {
|
|
511
|
-
pubManifest = manifest;
|
|
512
|
-
populatePublishForm(manifest);
|
|
513
|
-
publishModal.style.display = 'flex';
|
|
514
|
-
showPublishStep(1);
|
|
515
|
-
})
|
|
516
|
-
.catch(function (err) {
|
|
517
|
-
pubError.textContent = err.message;
|
|
518
|
-
pubError.style.display = 'block';
|
|
519
|
-
publishModal.style.display = 'flex';
|
|
520
|
-
showPublishStep(1);
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
pubCancel.addEventListener('click', closePublishModal);
|
|
525
|
-
publishModal.addEventListener('click', function (e) {
|
|
526
|
-
if (e.target === publishModal) closePublishModal();
|
|
527
|
-
});
|
|
528
|
-
|
|
529
|
-
function closePublishModal() {
|
|
530
|
-
publishModal.style.display = 'none';
|
|
531
|
-
if (pubPollTimer) { clearInterval(pubPollTimer); pubPollTimer = null; }
|
|
532
|
-
pubStep = 1;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
pubBack.addEventListener('click', function () {
|
|
536
|
-
if (pubStep > 1) showPublishStep(pubStep - 1);
|
|
537
|
-
});
|
|
538
|
-
|
|
539
|
-
pubNext.addEventListener('click', function () {
|
|
540
|
-
pubError.style.display = 'none';
|
|
541
|
-
if (pubStep === 1) {
|
|
542
|
-
collectAndSaveManifest().then(function () { showPublishStep(2); startValidation(); });
|
|
543
|
-
} else if (pubStep === 2 && pubValidationPassed) {
|
|
544
|
-
showPublishStep(3);
|
|
545
|
-
showCostInfo();
|
|
546
|
-
} else if (pubStep === 3) {
|
|
547
|
-
startBuildAndSubmit();
|
|
548
|
-
} else if (pubStep === 4) {
|
|
549
|
-
closePublishModal();
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
function showPublishStep(step) {
|
|
554
|
-
pubStep = step;
|
|
555
|
-
for (var i = 1; i <= 4; i++) {
|
|
556
|
-
var el = document.getElementById('publish-step-' + i);
|
|
557
|
-
if (el) el.style.display = i === step ? 'block' : 'none';
|
|
558
|
-
}
|
|
559
|
-
var steps = document.querySelectorAll('.publish-step');
|
|
560
|
-
steps.forEach(function (s, idx) {
|
|
561
|
-
s.className = 'publish-step' + (idx + 1 === step ? ' active' : idx + 1 < step ? ' completed' : '');
|
|
562
|
-
});
|
|
563
|
-
pubBack.style.display = step > 1 && step < 4 ? 'inline-block' : 'none';
|
|
564
|
-
if (step === 1) { pubNext.textContent = 'Validate'; pubNext.style.display = 'inline-block'; }
|
|
565
|
-
else if (step === 2) { pubNext.textContent = 'Build & Submit'; pubNext.style.display = 'inline-block'; pubNext.disabled = !pubValidationPassed; }
|
|
566
|
-
else if (step === 3) { pubNext.textContent = 'Confirm & Build'; pubNext.style.display = 'inline-block'; }
|
|
567
|
-
else if (step === 4) { pubNext.textContent = 'Close'; pubNext.style.display = 'inline-block'; }
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
function populatePublishForm(m) {
|
|
571
|
-
document.getElementById('pub-name').value = m.name || '';
|
|
572
|
-
document.getElementById('pub-version').value = m.version || '1.0.0';
|
|
573
|
-
document.getElementById('pub-description').value = m.description || '';
|
|
574
|
-
document.getElementById('pub-expanded-desc').value = m.expandedDescription || '';
|
|
575
|
-
document.getElementById('pub-archetype').value = m.archeType || 'tool';
|
|
576
|
-
document.getElementById('pub-pricing-model').value = m.pricing?.model || 'free';
|
|
577
|
-
document.getElementById('pub-price').value = m.pricing?.priceCents || '';
|
|
578
|
-
document.getElementById('pub-price-row').style.display = m.pricing?.model === 'free' ? 'none' : 'block';
|
|
579
|
-
|
|
580
|
-
// Tags
|
|
581
|
-
var tagsList = document.getElementById('pub-tags-list');
|
|
582
|
-
tagsList.innerHTML = '';
|
|
583
|
-
(m.tags || []).forEach(function (t) { addPublishTag(t); });
|
|
584
|
-
|
|
585
|
-
// Permissions
|
|
586
|
-
var checkboxes = document.querySelectorAll('#pub-permissions input[type="checkbox"]');
|
|
587
|
-
var perms = m.permissions || [];
|
|
588
|
-
checkboxes.forEach(function (cb) { cb.checked = perms.indexOf(cb.value) !== -1; });
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
// Tag management
|
|
592
|
-
document.getElementById('pub-tags-input').addEventListener('keydown', function (e) {
|
|
593
|
-
if (e.key === 'Enter' || e.key === ',') {
|
|
594
|
-
e.preventDefault();
|
|
595
|
-
var val = this.value.trim().replace(/,/g, '');
|
|
596
|
-
if (val) { addPublishTag(val); this.value = ''; }
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
|
|
600
|
-
function addPublishTag(text) {
|
|
601
|
-
var tag = document.createElement('span');
|
|
602
|
-
tag.className = 'pub-tag';
|
|
603
|
-
tag.innerHTML = escapeHtml(text) + ' <button class="pub-tag-remove">×</button>';
|
|
604
|
-
tag.querySelector('.pub-tag-remove').addEventListener('click', function () { tag.remove(); });
|
|
605
|
-
document.getElementById('pub-tags-list').appendChild(tag);
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
// Pricing model change
|
|
609
|
-
document.getElementById('pub-pricing-model').addEventListener('change', function () {
|
|
610
|
-
document.getElementById('pub-price-row').style.display = this.value === 'free' ? 'none' : 'block';
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
function collectManifest() {
|
|
614
|
-
var tags = [];
|
|
615
|
-
document.querySelectorAll('#pub-tags-list .pub-tag').forEach(function (t) {
|
|
616
|
-
tags.push(t.textContent.replace('\u00d7', '').trim());
|
|
617
|
-
});
|
|
618
|
-
var perms = [];
|
|
619
|
-
document.querySelectorAll('#pub-permissions input:checked').forEach(function (cb) { perms.push(cb.value); });
|
|
620
|
-
var pricingModel = document.getElementById('pub-pricing-model').value;
|
|
621
|
-
var pricing = { model: pricingModel, currency: 'usd' };
|
|
622
|
-
if (pricingModel !== 'free') pricing.priceCents = parseInt(document.getElementById('pub-price').value, 10) || 0;
|
|
623
|
-
|
|
624
|
-
return {
|
|
625
|
-
name: document.getElementById('pub-name').value.trim(),
|
|
626
|
-
version: document.getElementById('pub-version').value.trim(),
|
|
627
|
-
description: document.getElementById('pub-description').value.trim(),
|
|
628
|
-
expandedDescription: document.getElementById('pub-expanded-desc').value.trim() || undefined,
|
|
629
|
-
main: (pubManifest && pubManifest.main) || 'src/index.tsx',
|
|
630
|
-
archeType: document.getElementById('pub-archetype').value,
|
|
631
|
-
tags: tags,
|
|
632
|
-
pricing: pricing,
|
|
633
|
-
permissions: perms,
|
|
634
|
-
sdk: (pubManifest && pubManifest.sdk) || '^1.0.0',
|
|
635
|
-
dependencies: pubManifest && pubManifest.dependencies,
|
|
636
|
-
};
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function collectAndSaveManifest() {
|
|
640
|
-
var manifest = collectManifest();
|
|
641
|
-
pubManifest = manifest;
|
|
642
|
-
return fetch('/api/manifest', {
|
|
643
|
-
method: 'PUT',
|
|
644
|
-
headers: { 'Content-Type': 'application/json' },
|
|
645
|
-
body: JSON.stringify(manifest),
|
|
646
|
-
}).then(function (r) {
|
|
647
|
-
if (!r.ok) throw new Error('Failed to save manifest');
|
|
648
|
-
});
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function startValidation() {
|
|
652
|
-
pubValidationPassed = false;
|
|
653
|
-
pubNext.disabled = true;
|
|
654
|
-
document.getElementById('pub-validate-loading').style.display = 'flex';
|
|
655
|
-
document.getElementById('pub-validate-results').style.display = 'none';
|
|
656
|
-
|
|
657
|
-
fetch('/api/publish/validate', { method: 'POST' })
|
|
658
|
-
.then(function (r) { return r.json(); })
|
|
659
|
-
.then(function (data) {
|
|
660
|
-
document.getElementById('pub-validate-loading').style.display = 'none';
|
|
661
|
-
document.getElementById('pub-validate-results').style.display = 'block';
|
|
662
|
-
|
|
663
|
-
var statusEl = document.getElementById('pub-validate-status');
|
|
664
|
-
var errorsEl = document.getElementById('pub-validate-errors');
|
|
665
|
-
var warningsEl = document.getElementById('pub-validate-warnings');
|
|
666
|
-
errorsEl.innerHTML = '';
|
|
667
|
-
warningsEl.innerHTML = '';
|
|
668
|
-
|
|
669
|
-
if (data.valid && (!data.errors || data.errors.length === 0)) {
|
|
670
|
-
statusEl.innerHTML = '<div class="pub-valid">Manifest is valid</div>';
|
|
671
|
-
pubValidationPassed = true;
|
|
672
|
-
pubNext.disabled = false;
|
|
673
|
-
} else {
|
|
674
|
-
statusEl.innerHTML = '<div style="color:#fca5a5;font-weight:500;margin-bottom:8px">Validation failed</div>';
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
if (data.errors) {
|
|
678
|
-
data.errors.forEach(function (e) {
|
|
679
|
-
var div = document.createElement('div');
|
|
680
|
-
div.className = 'pub-error-item';
|
|
681
|
-
div.textContent = e;
|
|
682
|
-
errorsEl.appendChild(div);
|
|
683
|
-
});
|
|
684
|
-
}
|
|
685
|
-
if (data.warnings) {
|
|
686
|
-
data.warnings.forEach(function (w) {
|
|
687
|
-
var div = document.createElement('div');
|
|
688
|
-
div.className = 'pub-warn-item';
|
|
689
|
-
div.textContent = w;
|
|
690
|
-
warningsEl.appendChild(div);
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
// Listing preview
|
|
695
|
-
var preview = document.getElementById('pub-listing-preview');
|
|
696
|
-
var m = pubManifest;
|
|
697
|
-
preview.innerHTML =
|
|
698
|
-
'<div class="pub-listing-name">' + escapeHtml(m.name) + '</div>' +
|
|
699
|
-
'<div class="pub-listing-desc">' + escapeHtml(m.description) + '</div>' +
|
|
700
|
-
'<div class="pub-listing-meta">' +
|
|
701
|
-
'<span>v' + escapeHtml(m.version) + '</span>' +
|
|
702
|
-
'<span>' + escapeHtml(m.archeType) + '</span>' +
|
|
703
|
-
'<span>' + escapeHtml(m.pricing.model) + '</span>' +
|
|
704
|
-
'</div>';
|
|
705
|
-
})
|
|
706
|
-
.catch(function (err) {
|
|
707
|
-
document.getElementById('pub-validate-loading').style.display = 'none';
|
|
708
|
-
pubError.textContent = err.message;
|
|
709
|
-
pubError.style.display = 'block';
|
|
710
|
-
});
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function showCostInfo() {
|
|
714
|
-
var costEl = document.getElementById('pub-cost-info');
|
|
715
|
-
costEl.innerHTML = '<p>Submission cost: <strong>5,000 credits ($50.00)</strong> for first listing, <strong>100 credits ($1.00)</strong> for updates.</p>';
|
|
716
|
-
fetch('/api/credits').then(function (r) { return r.json(); }).then(function (d) {
|
|
717
|
-
if (d.balance != null && isFinite(d.balance)) {
|
|
718
|
-
costEl.innerHTML += '<p style="margin-top:8px">Your balance: <strong>' + d.balance.toFixed(2) + ' credits</strong></p>';
|
|
719
|
-
}
|
|
720
|
-
}).catch(function () {});
|
|
721
|
-
document.getElementById('pub-build-output').style.display = 'none';
|
|
722
|
-
document.getElementById('pub-build-output').textContent = '';
|
|
723
|
-
document.getElementById('pub-submit-progress').innerHTML = '';
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function startBuildAndSubmit() {
|
|
727
|
-
pubNext.style.display = 'none';
|
|
728
|
-
pubBack.style.display = 'none';
|
|
729
|
-
var outputEl = document.getElementById('pub-build-output');
|
|
730
|
-
var progressEl = document.getElementById('pub-submit-progress');
|
|
731
|
-
outputEl.style.display = 'block';
|
|
732
|
-
outputEl.textContent = '';
|
|
733
|
-
progressEl.innerHTML = '<div class="pub-phase active" id="phase-build">Building...</div>' +
|
|
734
|
-
'<div class="pub-phase" id="phase-package">Packaging...</div>' +
|
|
735
|
-
'<div class="pub-phase" id="phase-submit">Uploading & submitting...</div>';
|
|
736
|
-
|
|
737
|
-
// SSE build
|
|
738
|
-
fetch('/api/publish/build', { method: 'POST' })
|
|
739
|
-
.then(function (response) {
|
|
740
|
-
var reader = response.body.getReader();
|
|
741
|
-
var decoder = new TextDecoder();
|
|
742
|
-
var buffer = '';
|
|
743
|
-
|
|
744
|
-
function pump() {
|
|
745
|
-
return reader.read().then(function (chunk) {
|
|
746
|
-
if (chunk.done) return;
|
|
747
|
-
buffer += decoder.decode(chunk.value, { stream: true });
|
|
748
|
-
var lines = buffer.split('\n');
|
|
749
|
-
buffer = lines.pop() || '';
|
|
750
|
-
for (var i = 0; i < lines.length; i++) {
|
|
751
|
-
if (lines[i].indexOf('data: ') !== 0) continue;
|
|
752
|
-
try {
|
|
753
|
-
var parsed = JSON.parse(lines[i].slice(6));
|
|
754
|
-
if (parsed.line) {
|
|
755
|
-
outputEl.textContent += parsed.line + '\n';
|
|
756
|
-
outputEl.scrollTop = outputEl.scrollHeight;
|
|
757
|
-
}
|
|
758
|
-
if (parsed.done) {
|
|
759
|
-
if (parsed.code !== 0) throw new Error('Build failed (exit code ' + parsed.code + ')');
|
|
760
|
-
document.getElementById('phase-build').className = 'pub-phase done';
|
|
761
|
-
document.getElementById('phase-build').textContent = 'Build complete';
|
|
762
|
-
return doPackageAndSubmit();
|
|
763
|
-
}
|
|
764
|
-
} catch (e) {
|
|
765
|
-
if (e instanceof SyntaxError) continue;
|
|
766
|
-
throw e;
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return pump();
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
return pump();
|
|
773
|
-
})
|
|
774
|
-
.catch(function (err) {
|
|
775
|
-
document.getElementById('phase-build').className = 'pub-phase error';
|
|
776
|
-
document.getElementById('phase-build').textContent = 'Build failed: ' + err.message;
|
|
777
|
-
pubBack.style.display = 'inline-block';
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
function doPackageAndSubmit() {
|
|
782
|
-
var progressPhase = document.getElementById('phase-package');
|
|
783
|
-
progressPhase.className = 'pub-phase active';
|
|
784
|
-
|
|
785
|
-
return fetch('/api/publish/package', { method: 'POST' })
|
|
786
|
-
.then(function (r) {
|
|
787
|
-
if (!r.ok) return r.json().then(function (e) { throw new Error(e.error); });
|
|
788
|
-
return r.json();
|
|
789
|
-
})
|
|
790
|
-
.then(function (data) {
|
|
791
|
-
progressPhase.className = 'pub-phase done';
|
|
792
|
-
progressPhase.textContent = 'Packaged (' + Math.round(data.sizeBytes / 1024) + ' KB)';
|
|
793
|
-
document.getElementById('phase-submit').className = 'pub-phase active';
|
|
794
|
-
|
|
795
|
-
return fetch('/api/publish/submit', {
|
|
796
|
-
method: 'POST',
|
|
797
|
-
headers: { 'Content-Type': 'application/json' },
|
|
798
|
-
body: JSON.stringify({}),
|
|
799
|
-
});
|
|
800
|
-
})
|
|
801
|
-
.then(function (r) {
|
|
802
|
-
if (!r.ok) return r.json().then(function (e) { throw new Error(e.error); });
|
|
803
|
-
return r.json();
|
|
804
|
-
})
|
|
805
|
-
.then(function (data) {
|
|
806
|
-
document.getElementById('phase-submit').className = 'pub-phase done';
|
|
807
|
-
document.getElementById('phase-submit').textContent = 'Submitted for review';
|
|
808
|
-
pubSubmissionId = data.submissionId;
|
|
809
|
-
logMessage('info', 'Submitted: ' + pubSubmissionId);
|
|
810
|
-
setTimeout(function () { showPublishStep(4); startStatusPolling(); }, 1000);
|
|
811
|
-
})
|
|
812
|
-
.catch(function (err) {
|
|
813
|
-
var active = document.querySelector('.pub-phase.active');
|
|
814
|
-
if (active) { active.className = 'pub-phase error'; active.textContent += ' — ' + err.message; }
|
|
815
|
-
pubBack.style.display = 'inline-block';
|
|
816
|
-
});
|
|
817
|
-
}
|
|
818
|
-
|
|
819
|
-
function startStatusPolling() {
|
|
820
|
-
if (pubPollTimer) clearInterval(pubPollTimer);
|
|
821
|
-
document.getElementById('pub-review-status').style.display = 'block';
|
|
822
|
-
document.getElementById('pub-review-result').style.display = 'none';
|
|
823
|
-
|
|
824
|
-
pubPollTimer = setInterval(function () {
|
|
825
|
-
fetch('/api/publish/status/' + pubSubmissionId)
|
|
826
|
-
.then(function (r) { return r.json(); })
|
|
827
|
-
.then(function (data) {
|
|
828
|
-
var sub = data.submission || data;
|
|
829
|
-
var status = sub.status;
|
|
830
|
-
var statusEl = document.getElementById('pub-review-status');
|
|
831
|
-
|
|
832
|
-
if (status === 'reviewing') {
|
|
833
|
-
statusEl.innerHTML = '<div class="status-spinner"></div><p>AI is reviewing your plugin...</p>';
|
|
834
|
-
} else if (status === 'building') {
|
|
835
|
-
statusEl.innerHTML = '<div class="status-spinner"></div><p>Building your plugin...</p>';
|
|
836
|
-
} else if (status === 'published') {
|
|
837
|
-
clearInterval(pubPollTimer);
|
|
838
|
-
pubPollTimer = null;
|
|
839
|
-
statusEl.style.display = 'none';
|
|
840
|
-
document.getElementById('pub-review-result').style.display = 'block';
|
|
841
|
-
document.getElementById('pub-review-result').innerHTML =
|
|
842
|
-
'<div class="pub-congrats">Published!</div>' +
|
|
843
|
-
'<p style="text-align:center;color:#a1a1aa;font-size:13px">Your plugin is now live on the Fias marketplace.</p>';
|
|
844
|
-
logMessage('info', 'Plugin published!');
|
|
845
|
-
} else if (status === 'rejected') {
|
|
846
|
-
clearInterval(pubPollTimer);
|
|
847
|
-
pubPollTimer = null;
|
|
848
|
-
statusEl.style.display = 'none';
|
|
849
|
-
document.getElementById('pub-review-result').style.display = 'block';
|
|
850
|
-
var html = '<div class="pub-rejected">Submission Rejected</div>';
|
|
851
|
-
if (sub.errorMessage) html += '<p style="color:#a1a1aa;font-size:12px;margin-bottom:8px">' + escapeHtml(sub.errorMessage) + '</p>';
|
|
852
|
-
if (data.review) {
|
|
853
|
-
html += '<p style="color:#6b7280;font-size:11px">Risk score: ' + (data.review.riskScore || 'N/A') + '</p>';
|
|
854
|
-
}
|
|
855
|
-
document.getElementById('pub-review-result').innerHTML = html;
|
|
856
|
-
}
|
|
857
|
-
})
|
|
858
|
-
.catch(function () {});
|
|
859
|
-
}, 3000);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
// ────────────────────────────────────────────────────────────────
|
|
863
|
-
// Iframe Init
|
|
864
|
-
// ────────────────────────────────────────────────────────────────
|
|
865
|
-
|
|
866
|
-
iframe.addEventListener('load', function () {
|
|
867
|
-
if (!cachedConfig) return;
|
|
868
|
-
|
|
869
|
-
sendToPlugin({
|
|
870
|
-
type: 'init',
|
|
871
|
-
messageId: 'init_0',
|
|
872
|
-
payload: {
|
|
873
|
-
archId: 'dev_harness',
|
|
874
|
-
permissions: cachedConfig.permissions,
|
|
875
|
-
theme: getTheme(),
|
|
876
|
-
currentPath: '/',
|
|
877
|
-
},
|
|
878
|
-
});
|
|
879
|
-
logMessage('send', 'init');
|
|
880
|
-
});
|
|
881
|
-
|
|
882
|
-
// ────────────────────────────────────────────────────────────────
|
|
883
|
-
// Helpers
|
|
884
|
-
// ────────────────────────────────────────────────────────────────
|
|
885
|
-
|
|
886
|
-
function updateModeBadge() {
|
|
887
|
-
var opposite = currentMode === 'live' ? 'Mock' : 'Live';
|
|
888
|
-
modeBadge.textContent = currentMode.toUpperCase() + ' \u21C6';
|
|
889
|
-
modeBadge.className = 'mode-badge ' + (currentMode === 'live' ? 'mode-live' : 'mode-mock');
|
|
890
|
-
modeBadge.title = 'Click to switch to ' + opposite + ' mode';
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
function updateThemeBadge() {
|
|
894
|
-
themeBadge.textContent = currentTheme.toUpperCase();
|
|
895
|
-
themeBadge.className = 'theme-badge theme-' + currentTheme;
|
|
896
|
-
document.body.style.background = currentTheme === 'light' ? '#ffffff' : '#0a0a0a';
|
|
897
|
-
|
|
898
|
-
// Update toolbar to match theme
|
|
899
|
-
var toolbar = document.querySelector('.toolbar');
|
|
900
|
-
var logo = document.querySelector('.logo');
|
|
901
|
-
var icons = document.querySelectorAll('.btn-icon');
|
|
902
|
-
var env = document.getElementById('env-selector');
|
|
903
|
-
if (currentTheme === 'light') {
|
|
904
|
-
if (toolbar) { toolbar.style.background = '#f5f5f5'; toolbar.style.borderBottomColor = '#e5e5e5'; }
|
|
905
|
-
if (logo) { logo.style.color = '#171717'; }
|
|
906
|
-
if (env) { env.style.background = '#e5e5e5'; env.style.color = '#171717'; env.style.borderColor = '#d4d4d4'; }
|
|
907
|
-
icons.forEach(function (btn) { btn.style.background = '#e5e5e5'; btn.style.color = '#171717'; btn.style.borderColor = '#d4d4d4'; });
|
|
908
|
-
} else {
|
|
909
|
-
if (toolbar) { toolbar.style.background = '#18181b'; toolbar.style.borderBottomColor = '#3f3f46'; }
|
|
910
|
-
if (logo) { logo.style.color = '#e4e4e7'; }
|
|
911
|
-
if (env) { env.style.background = '#27272a'; env.style.color = '#a1a1aa'; env.style.borderColor = '#3f3f46'; }
|
|
912
|
-
icons.forEach(function (btn) { btn.style.background = '#27272a'; btn.style.color = '#e4e4e7'; btn.style.borderColor = '#3f3f46'; });
|
|
913
|
-
}
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function sendToPlugin(message) {
|
|
917
|
-
iframe.contentWindow && iframe.contentWindow.postMessage(message, '*');
|
|
918
|
-
}
|
|
919
|
-
|
|
920
|
-
function checkRateLimit(type) {
|
|
921
|
-
var limit = RATE_LIMITS[type];
|
|
922
|
-
if (!limit) return;
|
|
923
|
-
|
|
924
|
-
var now = Date.now();
|
|
925
|
-
var bucket = rateBuckets[type];
|
|
926
|
-
|
|
927
|
-
if (!bucket || now - bucket.windowStart > 60000) {
|
|
928
|
-
rateBuckets[type] = { count: 1, windowStart: now };
|
|
929
|
-
return;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
if (bucket.count >= limit.maxPerMinute) {
|
|
933
|
-
throw new Error(
|
|
934
|
-
'Rate limit exceeded for ' + type + ': max ' + limit.maxPerMinute + '/minute',
|
|
935
|
-
);
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
bucket.count++;
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
var SYSTEM_FONTS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
942
|
-
var MONO_FONTS = '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
|
|
943
|
-
var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
|
|
944
|
-
var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
|
|
945
|
-
|
|
946
|
-
var COMPONENTS = {
|
|
947
|
-
borderRadius: '0.5rem',
|
|
948
|
-
buttonRadius: '0.375rem',
|
|
949
|
-
cardRadius: '0.5rem',
|
|
950
|
-
inputRadius: '0.375rem',
|
|
951
|
-
shadowSm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
|
|
952
|
-
shadowMd: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -2px rgba(0, 0, 0, 0.1)',
|
|
953
|
-
shadowLg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1)',
|
|
954
|
-
borderWidth: '1px',
|
|
955
|
-
};
|
|
956
|
-
|
|
957
|
-
function getTheme() {
|
|
958
|
-
if (currentTheme === 'light') {
|
|
959
|
-
return {
|
|
960
|
-
mode: 'light',
|
|
961
|
-
colors: {
|
|
962
|
-
primary: '#171717', primaryText: '#ffffff',
|
|
963
|
-
secondary: '#e5e5e5', accent: '#2563eb',
|
|
964
|
-
background: '#ffffff', surface: '#fafafa',
|
|
965
|
-
card: '#ffffff', cardText: '#0a0a0a',
|
|
966
|
-
text: '#0a0a0a', textSecondary: '#737373',
|
|
967
|
-
muted: '#f5f5f5', mutedText: '#a3a3a3',
|
|
968
|
-
border: '#e5e5e5',
|
|
969
|
-
error: '#dc2626', warning: '#d97706', success: '#16a34a', info: '#2563eb',
|
|
970
|
-
},
|
|
971
|
-
spacing: SPACING,
|
|
972
|
-
fonts: FONTS,
|
|
973
|
-
components: COMPONENTS,
|
|
974
|
-
};
|
|
975
|
-
}
|
|
976
|
-
return {
|
|
977
|
-
mode: 'dark',
|
|
978
|
-
colors: {
|
|
979
|
-
primary: '#ffffff', primaryText: '#0a0a0a',
|
|
980
|
-
secondary: '#1f1f1f', accent: '#3b82f6',
|
|
981
|
-
background: '#0a0a0a', surface: '#171717',
|
|
982
|
-
card: '#141414', cardText: '#ffffff',
|
|
983
|
-
text: '#ffffff', textSecondary: '#a6a6a6',
|
|
984
|
-
muted: '#1e1e1e', mutedText: '#737373',
|
|
985
|
-
border: '#2e2e2e',
|
|
986
|
-
error: '#ef4444', warning: '#f59e0b', success: '#22c55e', info: '#3b82f6',
|
|
987
|
-
},
|
|
988
|
-
spacing: SPACING,
|
|
989
|
-
fonts: FONTS,
|
|
990
|
-
components: COMPONENTS,
|
|
991
|
-
};
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
function fetchConfig() {
|
|
995
|
-
return fetch('/api/config').then(function (r) {
|
|
996
|
-
return r.json();
|
|
997
|
-
});
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
|
-
function fetchCredits() {
|
|
1001
|
-
fetch('/api/credits')
|
|
1002
|
-
.then(function (r) {
|
|
1003
|
-
return r.json();
|
|
1004
|
-
})
|
|
1005
|
-
.then(function (data) {
|
|
1006
|
-
if (data.balance !== undefined && data.balance !== null && isFinite(data.balance)) {
|
|
1007
|
-
creditBalance.textContent = 'Credits: ' + data.balance.toFixed(2);
|
|
1008
|
-
}
|
|
1009
|
-
})
|
|
1010
|
-
.catch(function () {});
|
|
1011
|
-
}
|
|
1012
|
-
|
|
1013
|
-
function logMessage(direction, type, payload) {
|
|
1014
|
-
messageCount++;
|
|
1015
|
-
consoleCount.textContent = messageCount + ' messages';
|
|
1016
|
-
|
|
1017
|
-
var entry = document.createElement('div');
|
|
1018
|
-
entry.className = 'log-entry';
|
|
1019
|
-
|
|
1020
|
-
var time = new Date().toLocaleTimeString();
|
|
1021
|
-
var cls = 'log-info';
|
|
1022
|
-
if (direction === 'error') cls = 'log-error';
|
|
1023
|
-
if (direction === 'warn' || direction === 'cost') cls = 'log-warn';
|
|
1024
|
-
if (direction === 'toast') cls = 'log-warn';
|
|
1025
|
-
|
|
1026
|
-
var text = '<span class="log-time">' + escapeHtml(time) + '</span>';
|
|
1027
|
-
text +=
|
|
1028
|
-
'<span class="' +
|
|
1029
|
-
cls +
|
|
1030
|
-
'">[' +
|
|
1031
|
-
escapeHtml(direction.toUpperCase()) +
|
|
1032
|
-
'] ' +
|
|
1033
|
-
escapeHtml(String(type)) +
|
|
1034
|
-
'</span>';
|
|
1035
|
-
if (payload && typeof payload === 'object') {
|
|
1036
|
-
text +=
|
|
1037
|
-
' <span style="color:#6b7280">' +
|
|
1038
|
-
escapeHtml(JSON.stringify(payload).substring(0, 120)) +
|
|
1039
|
-
'</span>';
|
|
1040
|
-
}
|
|
1041
|
-
|
|
1042
|
-
entry.innerHTML = text;
|
|
1043
|
-
consoleBody.appendChild(entry);
|
|
1044
|
-
consoleBody.scrollTop = consoleBody.scrollHeight;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
function escapeHtml(str) {
|
|
1048
|
-
var div = document.createElement('div');
|
|
1049
|
-
div.appendChild(document.createTextNode(str));
|
|
1050
|
-
return div.innerHTML;
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
function checkPluginReachable(url, callback) {
|
|
1054
|
-
var done = false;
|
|
1055
|
-
|
|
1056
|
-
fetch('/api/check-plugin')
|
|
1057
|
-
.then(function (r) { return r.json(); })
|
|
1058
|
-
.then(function (data) {
|
|
1059
|
-
if (!done) {
|
|
1060
|
-
done = true;
|
|
1061
|
-
callback(data.reachable);
|
|
1062
|
-
}
|
|
1063
|
-
})
|
|
1064
|
-
.catch(function () {
|
|
1065
|
-
if (!done) {
|
|
1066
|
-
done = true;
|
|
1067
|
-
callback(true);
|
|
1068
|
-
}
|
|
1069
|
-
});
|
|
1070
|
-
|
|
1071
|
-
setTimeout(function () {
|
|
1072
|
-
if (!done) {
|
|
1073
|
-
done = true;
|
|
1074
|
-
callback(false);
|
|
1075
|
-
}
|
|
1076
|
-
}, 5000);
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
function showPluginError(url) {
|
|
1080
|
-
pluginStatus.classList.add('error');
|
|
1081
|
-
pluginStatus.innerHTML =
|
|
1082
|
-
'<p><strong>Plugin server not reachable</strong></p>' +
|
|
1083
|
-
'<p>The harness cannot connect to your plugin at<br/><code>' + escapeHtml(url) + '</code></p>' +
|
|
1084
|
-
'<p>Make sure your plugin dev server is running:</p>' +
|
|
1085
|
-
'<p><code>npm run dev</code></p>' +
|
|
1086
|
-
'<p style="color:#6b7280;font-size:12px;margin-top:8px;">' +
|
|
1087
|
-
'The harness will automatically retry when you click the reload button (↻).</p>';
|
|
1088
|
-
iframe.classList.add('hidden');
|
|
1089
|
-
}
|
|
1090
|
-
})();
|