@fias/plugin-dev-harness 1.1.0 → 1.1.2

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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * FIAS Dev Harness — Client-side Bridge Host
2
+ * Fias Arche Dev — Client-side Bridge Host
3
3
  *
4
4
  * Ported from the production PluginBridgeHost
5
5
  * (client/src/arche/components/PluginBridge.ts).
@@ -19,10 +19,19 @@
19
19
  var themeToggle = document.getElementById('theme-toggle');
20
20
  var reloadBtn = document.getElementById('reload-btn');
21
21
  var modeBadge = document.getElementById('mode-badge');
22
- var permissionsEl = document.getElementById('permissions');
22
+ var pluginUrlEl = document.getElementById('plugin-url');
23
+ var pluginStatus = document.getElementById('plugin-status');
24
+ var themeBadge = document.getElementById('theme-badge');
25
+ var loginModal = document.getElementById('login-modal');
26
+ var loginInput = document.getElementById('login-input');
27
+ var loginError = document.getElementById('login-error');
28
+ var loginSubmit = document.getElementById('login-submit');
29
+ var loginCancel = document.getElementById('login-cancel');
23
30
 
24
31
  var messageCount = 0;
25
32
  var currentTheme = 'dark';
33
+ var currentMode = 'mock';
34
+ var hasCredentials = false;
26
35
  var cachedConfig = null;
27
36
 
28
37
  /** Permission requirements per bridge call type (matches production) */
@@ -53,24 +62,29 @@
53
62
  fetchConfig().then(function (config) {
54
63
  cachedConfig = config;
55
64
  currentTheme = config.mockTheme || 'dark';
65
+ currentMode = config.mode || 'mock';
66
+ hasCredentials = config.hasCredentials || false;
56
67
 
57
- // Update UI
58
- modeBadge.textContent = config.isLive ? 'LIVE' : 'MOCK';
59
- modeBadge.className = 'mode-badge ' + (config.isLive ? 'mode-live' : 'mode-mock');
68
+ updateThemeBadge();
69
+ updateModeBadge();
60
70
 
61
- permissionsEl.innerHTML = config.permissions
62
- .map(function (p) {
63
- return '<span class="perm-badge">' + escapeHtml(p) + '</span>';
64
- })
65
- .join('');
71
+ pluginUrlEl.textContent = config.pluginUrl;
66
72
 
67
- if (config.isLive) {
73
+ if (currentMode === 'live') {
68
74
  creditBalance.style.display = 'inline';
69
75
  fetchCredits();
70
76
  }
71
77
 
72
- // Load plugin
73
- iframe.src = config.pluginUrl;
78
+ // Check if plugin server is reachable, then load
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
+ });
74
88
  });
75
89
 
76
90
  // ────────────────────────────────────────────────────────────────
@@ -82,11 +96,29 @@
82
96
  });
83
97
 
84
98
  reloadBtn.addEventListener('click', function () {
85
- iframe.src = iframe.src;
99
+ if (cachedConfig) {
100
+ // Re-check reachability on reload
101
+ pluginStatus.classList.remove('hidden', 'error');
102
+ pluginStatus.innerHTML =
103
+ '<div class="status-spinner"></div><p>Connecting to plugin server...</p>';
104
+ iframe.classList.add('hidden');
105
+ checkPluginReachable(cachedConfig.pluginUrl, function (reachable) {
106
+ if (reachable) {
107
+ pluginStatus.classList.add('hidden');
108
+ iframe.classList.remove('hidden');
109
+ iframe.src = cachedConfig.pluginUrl;
110
+ } else {
111
+ showPluginError(cachedConfig.pluginUrl);
112
+ }
113
+ });
114
+ } else {
115
+ iframe.src = iframe.src;
116
+ }
86
117
  });
87
118
 
88
119
  themeToggle.addEventListener('click', function () {
89
120
  currentTheme = currentTheme === 'dark' ? 'light' : 'dark';
121
+ updateThemeBadge();
90
122
  sendToPlugin({
91
123
  type: 'theme_update',
92
124
  messageId: 'theme_' + Date.now(),
@@ -95,6 +127,123 @@
95
127
  logMessage('send', 'theme_update', { mode: currentTheme });
96
128
  });
97
129
 
130
+ // Mode toggle — click the badge to switch between mock/live
131
+ modeBadge.addEventListener('click', function () {
132
+ if (currentMode === 'mock') {
133
+ // Switching to live — need credentials
134
+ if (!hasCredentials) {
135
+ showLoginModal();
136
+ return;
137
+ }
138
+ switchMode('live');
139
+ } else {
140
+ switchMode('mock');
141
+ }
142
+ });
143
+
144
+ function switchMode(newMode) {
145
+ fetch('/api/mode', {
146
+ method: 'POST',
147
+ headers: { 'Content-Type': 'application/json' },
148
+ body: JSON.stringify({ mode: newMode }),
149
+ })
150
+ .then(function (r) {
151
+ if (!r.ok) {
152
+ return r.json().then(function (err) {
153
+ throw new Error(err.error || 'Failed to switch mode');
154
+ });
155
+ }
156
+ return r.json();
157
+ })
158
+ .then(function (data) {
159
+ currentMode = data.mode;
160
+ updateModeBadge();
161
+ logMessage('info', 'Mode switched to ' + currentMode.toUpperCase());
162
+
163
+ if (currentMode === 'live') {
164
+ creditBalance.style.display = 'inline';
165
+ fetchCredits();
166
+ } else {
167
+ creditBalance.style.display = 'none';
168
+ creditBalance.textContent = '';
169
+ }
170
+ })
171
+ .catch(function (err) {
172
+ logMessage('error', err.message);
173
+ });
174
+ }
175
+
176
+ // ────────────────────────────────────────────────────────────────
177
+ // Login Modal
178
+ // ────────────────────────────────────────────────────────────────
179
+
180
+ function showLoginModal() {
181
+ loginModal.style.display = 'flex';
182
+ loginInput.value = '';
183
+ loginError.style.display = 'none';
184
+ loginInput.focus();
185
+ }
186
+
187
+ function hideLoginModal() {
188
+ loginModal.style.display = 'none';
189
+ loginInput.value = '';
190
+ loginError.style.display = 'none';
191
+ }
192
+
193
+ loginCancel.addEventListener('click', hideLoginModal);
194
+
195
+ loginModal.addEventListener('click', function (e) {
196
+ if (e.target === loginModal) hideLoginModal();
197
+ });
198
+
199
+ loginInput.addEventListener('keydown', function (e) {
200
+ if (e.key === 'Enter') submitLogin();
201
+ if (e.key === 'Escape') hideLoginModal();
202
+ });
203
+
204
+ loginSubmit.addEventListener('click', submitLogin);
205
+
206
+ function submitLogin() {
207
+ var apiKey = loginInput.value.trim();
208
+ if (!apiKey) {
209
+ loginError.textContent = 'Please enter an API key.';
210
+ loginError.style.display = 'block';
211
+ return;
212
+ }
213
+
214
+ loginSubmit.disabled = true;
215
+ loginSubmit.textContent = 'Connecting...';
216
+
217
+ fetch('/api/login', {
218
+ method: 'POST',
219
+ headers: { 'Content-Type': 'application/json' },
220
+ body: JSON.stringify({ apiKey: apiKey }),
221
+ })
222
+ .then(function (r) {
223
+ if (!r.ok) {
224
+ return r.json().then(function (err) {
225
+ throw new Error(err.error || 'Login failed');
226
+ });
227
+ }
228
+ return r.json();
229
+ })
230
+ .then(function () {
231
+ hasCredentials = true;
232
+ hideLoginModal();
233
+ logMessage('info', 'API key saved successfully');
234
+ // Automatically switch to live mode after login
235
+ switchMode('live');
236
+ })
237
+ .catch(function (err) {
238
+ loginError.textContent = err.message;
239
+ loginError.style.display = 'block';
240
+ })
241
+ .finally(function () {
242
+ loginSubmit.disabled = false;
243
+ loginSubmit.textContent = 'Save & Connect';
244
+ });
245
+ }
246
+
98
247
  // ────────────────────────────────────────────────────────────────
99
248
  // Message Handling
100
249
  // ────────────────────────────────────────────────────────────────
@@ -228,8 +377,23 @@
228
377
  // Helpers
229
378
  // ────────────────────────────────────────────────────────────────
230
379
 
380
+ function updateModeBadge() {
381
+ modeBadge.textContent = currentMode.toUpperCase();
382
+ modeBadge.className = 'mode-badge ' + (currentMode === 'live' ? 'mode-live' : 'mode-mock');
383
+ modeBadge.title = currentMode === 'live'
384
+ ? 'Live mode (real AI) — click to switch to mock'
385
+ : hasCredentials
386
+ ? 'Mock mode — click to switch to live'
387
+ : 'Mock mode — click to login and enable live mode';
388
+ }
389
+
390
+ function updateThemeBadge() {
391
+ themeBadge.textContent = currentTheme.toUpperCase();
392
+ themeBadge.className = 'theme-badge theme-' + currentTheme;
393
+ }
394
+
231
395
  function sendToPlugin(message) {
232
- // targetOrigin '*' because sandboxed iframes have opaque origins.
396
+ // targetOrigin '*' because the plugin may be on a different port.
233
397
  // Security is enforced by checking event.source on incoming messages.
234
398
  iframe.contentWindow && iframe.contentWindow.postMessage(message, '*');
235
399
  }
@@ -255,32 +419,37 @@
255
419
  bucket.count++;
256
420
  }
257
421
 
422
+ var SYSTEM_FONTS = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
423
+ var MONO_FONTS = '"SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace';
424
+ var SPACING = { xs: '4px', sm: '8px', md: '16px', lg: '24px', xl: '32px' };
425
+ var FONTS = { body: SYSTEM_FONTS, heading: SYSTEM_FONTS, mono: MONO_FONTS };
426
+
258
427
  function getTheme() {
259
428
  if (currentTheme === 'light') {
260
429
  return {
261
430
  mode: 'light',
262
431
  colors: {
263
- background: '#ffffff',
264
- foreground: '#18181b',
265
- primary: '#7c3aed',
266
- secondary: '#ede9fe',
267
- accent: '#8b5cf6',
268
- muted: '#f4f4f5',
432
+ primary: '#7c3aed', secondary: '#ede9fe',
433
+ background: '#ffffff', surface: '#f4f4f5',
434
+ text: '#18181b', textSecondary: '#71717a',
269
435
  border: '#e4e4e7',
436
+ error: '#ef4444', warning: '#f59e0b', success: '#22c55e',
270
437
  },
438
+ spacing: SPACING,
439
+ fonts: FONTS,
271
440
  };
272
441
  }
273
442
  return {
274
443
  mode: 'dark',
275
444
  colors: {
276
- background: '#0a0a0f',
277
- foreground: '#e4e4e7',
278
- primary: '#6d28d9',
279
- secondary: '#1e1b4b',
280
- accent: '#8b5cf6',
281
- muted: '#27272a',
445
+ primary: '#6d28d9', secondary: '#1e1b4b',
446
+ background: '#0a0a0f', surface: '#27272a',
447
+ text: '#e4e4e7', textSecondary: '#a1a1aa',
282
448
  border: '#3f3f46',
449
+ error: '#ef4444', warning: '#f59e0b', success: '#22c55e',
283
450
  },
451
+ spacing: SPACING,
452
+ fonts: FONTS,
284
453
  };
285
454
  }
286
455
 
@@ -296,7 +465,7 @@
296
465
  return r.json();
297
466
  })
298
467
  .then(function (data) {
299
- if (data.balance !== undefined && isFinite(data.balance)) {
468
+ if (data.balance !== undefined && data.balance !== null && isFinite(data.balance)) {
300
469
  creditBalance.textContent = 'Credits: ' + data.balance.toFixed(2);
301
470
  }
302
471
  })
@@ -342,4 +511,45 @@
342
511
  div.appendChild(document.createTextNode(str));
343
512
  return div.innerHTML;
344
513
  }
514
+
515
+ function checkPluginReachable(url, callback) {
516
+ var done = false;
517
+
518
+ // Try fetching through the harness server to avoid CORS issues
519
+ fetch('/api/check-plugin')
520
+ .then(function (r) { return r.json(); })
521
+ .then(function (data) {
522
+ if (!done) {
523
+ done = true;
524
+ callback(data.reachable);
525
+ }
526
+ })
527
+ .catch(function () {
528
+ if (!done) {
529
+ done = true;
530
+ // If the check endpoint doesn't exist, assume reachable and let iframe handle it
531
+ callback(true);
532
+ }
533
+ });
534
+
535
+ // Timeout after 5 seconds
536
+ setTimeout(function () {
537
+ if (!done) {
538
+ done = true;
539
+ callback(false);
540
+ }
541
+ }, 5000);
542
+ }
543
+
544
+ function showPluginError(url) {
545
+ pluginStatus.classList.add('error');
546
+ pluginStatus.innerHTML =
547
+ '<p><strong>Plugin server not reachable</strong></p>' +
548
+ '<p>The harness cannot connect to your plugin at<br/><code>' + escapeHtml(url) + '</code></p>' +
549
+ '<p>Make sure your plugin dev server is running:</p>' +
550
+ '<p><code>npm run dev</code></p>' +
551
+ '<p style="color:#6b7280;font-size:12px;margin-top:8px;">' +
552
+ 'The harness will automatically retry when you click the reload button (&#8635;).</p>';
553
+ iframe.classList.add('hidden');
554
+ }
345
555
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fias/plugin-dev-harness",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Development harness for building and testing FIAS plugin arches locally",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",