@miradorlabs/parallax-web 1.0.4 → 1.0.7

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.
@@ -0,0 +1,257 @@
1
+ # Parallax Web Client Demo
2
+
3
+ An interactive web application demonstrating how to use the Parallax SDK to create and manage transaction traces.
4
+
5
+ ## Features
6
+
7
+ - **Builder Pattern Interface**: Use the fluent API to construct traces step-by-step
8
+ - **Real-time Trace Building**: Add attributes, tags, and events dynamically
9
+ - **Transaction Hash Support**: Optionally associate traces with blockchain transactions
10
+ - **Activity Logging**: See all SDK operations in real-time
11
+ - **Client Metadata**: Automatically includes browser, OS, and screen information
12
+
13
+ ## Getting Started
14
+
15
+ ### Quick Start (Recommended)
16
+
17
+ The easiest way to run the demo:
18
+
19
+ ```bash
20
+ cd ./example
21
+ npm install
22
+ npm run dev
23
+ ```
24
+
25
+ This will:
26
+ 1. Install dependencies (Rollup)
27
+ 2. Bundle from TypeScript source
28
+ 3. Compile TypeScript and bundle everything
29
+ 4. Start a local web server
30
+ 5. Open your browser automatically
31
+
32
+ That's it! The demo will be running at http://localhost:8000
33
+
34
+ ### Manual Setup
35
+
36
+ If you prefer to run it manually:
37
+
38
+ ```bash
39
+ # 1. Install dependencies
40
+ npm install
41
+
42
+ # 2. Build the bundle
43
+ npm run build
44
+
45
+ # 3. Start a web server
46
+ npm start # Using Python
47
+ # OR
48
+ npm run serve # Using http-server (auto-opens browser)
49
+ ```
50
+
51
+ ### Alternative: Using VS Code Live Server
52
+ 1. Run `npm run build` to create the bundle
53
+ 2. Install the "Live Server" extension in VS Code
54
+ 3. Right-click on `index.html`
55
+ 4. Select "Open with Live Server"
56
+
57
+ ## How to Use
58
+
59
+ ### Step 1: Create a Trace
60
+ 1. Fill in the trace name (e.g., "swap_execution", "payment_flow")
61
+ 2. Enter transaction details:
62
+ - From address
63
+ - To address
64
+ - Value (in ETH)
65
+ - Network (Ethereum, Polygon, etc.)
66
+ 3. Click "Create Trace"
67
+
68
+ This initializes a trace builder with:
69
+ - Default transaction attributes
70
+ - Network-specific tags
71
+ - Automatic client metadata (browser, OS, screen size, etc.)
72
+
73
+ ### Step 2: Add Custom Attributes (Optional)
74
+ Add any custom key-value pairs to enrich your trace:
75
+ - `slippage_bps`: "50"
76
+ - `gas_limit`: "21000"
77
+ - `dex`: "uniswap"
78
+ - Any other relevant metadata
79
+
80
+ ### Step 3: Add Tags (Optional)
81
+ Add tags to categorize your trace:
82
+ - "dex"
83
+ - "swap"
84
+ - "critical"
85
+ - "production"
86
+
87
+ ### Step 4: Add Events (Optional)
88
+ Track important moments in the transaction lifecycle:
89
+ - Event name: "wallet_connected"
90
+ - Details: `{"wallet": "MetaMask", "version": "10.0.0"}`
91
+
92
+ Or:
93
+ - Event name: "quote_received"
94
+ - Details: (leave empty for timestamp-only events)
95
+
96
+ **JSON Support**: If your details start with `{` or `[`, they'll be parsed as JSON automatically.
97
+
98
+ ### Step 5: Submit the Trace
99
+ Submit your trace to the Parallax Gateway:
100
+
101
+ **Without Transaction Hash:**
102
+ - Just click "Submit Trace"
103
+ - Use this when you don't have a blockchain tx hash yet
104
+
105
+ **With Transaction Hash:**
106
+ - Enter the transaction hash
107
+ - Enter the chain ID (1 for Ethereum, 137 for Polygon, etc.)
108
+ - Click "Submit Trace"
109
+
110
+ ## Example Usage Flow
111
+
112
+ ```javascript
113
+ // The demo app uses the builder pattern like this:
114
+
115
+ // 1. Create trace builder
116
+ const trace = client.trace("swap_execution", true); // includeClientMeta
117
+
118
+ // 2. Add attributes
119
+ trace
120
+ .addAttribute("from", "0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb")
121
+ .addAttribute("to", "0x1234567890123456789012345678901234567890")
122
+ .addAttribute("value", "0.5")
123
+ .addAttribute("network", "ethereum")
124
+ .addAttribute("slippage_bps", "50");
125
+
126
+ // 3. Add tags
127
+ trace
128
+ .addTag("dex")
129
+ .addTag("swap")
130
+ .addTag("uniswap");
131
+
132
+ // 4. Add events
133
+ trace
134
+ .addEvent("wallet_connected", { wallet: "MetaMask" })
135
+ .addEvent("quote_received", { amount: "0.5 ETH", price: "$2000" })
136
+ .addEvent("tx_signed");
137
+
138
+ // 5. Submit with transaction hash
139
+ const response = await trace.submit(
140
+ "0x1234567890abcdef...", // txHash
141
+ "1", // chainId (Ethereum)
142
+ "Swap transaction" // details (optional)
143
+ );
144
+
145
+ console.log("Trace ID:", response.getTraceId());
146
+ ```
147
+
148
+ ## Understanding the Builder Pattern
149
+
150
+ The Parallax SDK uses a **builder pattern** for creating traces:
151
+
152
+ ```javascript
153
+ client.trace(name, includeClientMeta)
154
+ .addAttribute(key, value) // Add single attribute
155
+ .addAttributes({...}) // Add multiple attributes
156
+ .addTag(tag) // Add single tag
157
+ .addTags([...]) // Add multiple tags
158
+ .addEvent(name, details, ts) // Add event
159
+ .setTxHash(hash, chainId) // Set transaction hash
160
+ .submit() // Submit without tx hash
161
+ .submit(hash, chainId) // Submit with tx hash
162
+ ```
163
+
164
+ **Benefits:**
165
+ - ✅ Method chaining for clean, readable code
166
+ - ✅ All data collected before submission
167
+ - ✅ Type-safe with TypeScript
168
+ - ✅ Automatic JSON stringification for objects
169
+ - ✅ Optional client metadata collection
170
+
171
+ ## Activity Log
172
+
173
+ The demo includes a real-time activity log showing:
174
+ - ✅ Successful operations (green)
175
+ - ❌ Errors (red)
176
+ - â„šī¸ Information messages (blue)
177
+ - âš ī¸ Warnings (yellow)
178
+
179
+ All SDK operations are logged with timestamps for debugging.
180
+
181
+ ## API Configuration
182
+
183
+ By default, the demo connects to:
184
+ ```javascript
185
+ const client = new ParallaxClient(
186
+ 'demo-api-key',
187
+ 'https://parallax-gateway.dev.mirador.org:443'
188
+ );
189
+ ```
190
+
191
+ To use a different endpoint, modify `app.js`:
192
+ ```javascript
193
+ const client = new ParallaxClient(
194
+ 'your-api-key',
195
+ 'https://your-gateway-url:port'
196
+ );
197
+ ```
198
+
199
+ ## Client Metadata
200
+
201
+ When `includeClientMeta` is `true`, the SDK automatically includes:
202
+ - Browser name and version
203
+ - Operating system
204
+ - Screen resolution
205
+ - Viewport size
206
+ - User agent
207
+ - Language
208
+ - Timezone
209
+ - Current URL
210
+ - Referrer
211
+
212
+ All metadata is prefixed with `client.*` (e.g., `client.browser`, `client.os`)
213
+
214
+ ## Troubleshooting
215
+
216
+ ### CORS Errors
217
+ If you see CORS errors in the console:
218
+ - Make sure you're serving the files via HTTP (not opening `file://` directly)
219
+ - Check that the Parallax Gateway allows requests from your origin
220
+
221
+ ### Import Errors
222
+ If you see module import errors:
223
+ - Ensure the SDK is built: `npm run build`
224
+ - Check that `dist/index.esm.js` exists
225
+ - Verify you're using a browser that supports ES modules
226
+
227
+ ### Connection Errors
228
+ If trace submission fails:
229
+ - Check that the gateway URL is correct
230
+ - Verify your API key is valid
231
+ - Check the browser console for detailed error messages
232
+ - Look at the Activity Log for specific error details
233
+
234
+ ## Code Structure
235
+
236
+ ```
237
+ example/
238
+ ├── index.html # Main HTML page with styles
239
+ ├── app.js # Application logic using Parallax SDK
240
+ └── README.md # This file
241
+ ```
242
+
243
+ The demo is intentionally simple (vanilla JavaScript + HTML/CSS) to focus on SDK usage without framework complexity.
244
+
245
+ ## Next Steps
246
+
247
+ After exploring the demo:
248
+ 1. Check the Activity Log to see all SDK operations
249
+ 2. Open the browser DevTools to see network requests
250
+ 3. Modify `app.js` to experiment with different trace patterns
251
+ 4. Integrate the builder pattern into your own application
252
+
253
+ ## Learn More
254
+
255
+ - [Parallax SDK Documentation](../README.md)
256
+ - [API Reference](../src/parallax/index.ts)
257
+ - [ParallaxService](../src/parallax/ParallaxService.ts) - Higher-level service wrapper
package/example/app.js ADDED
@@ -0,0 +1,411 @@
1
+ // Import the Parallax SDK
2
+ import { ParallaxClient } from '../dist/index.esm.js';
3
+
4
+ // Initialize the client
5
+ // Use proxy to avoid CORS issues (default)
6
+ // To use direct connection, change to: 'https://parallax-gateway.dev.mirador.org:443'
7
+ const USE_PROXY = true;
8
+ const GATEWAY_URL = USE_PROXY
9
+ ? 'http://localhost:3001' // Proxy server (no CORS issues)
10
+ : 'https://parallax-gateway.dev.mirador.org:443'; // Direct (may have CORS issues)
11
+
12
+ const client = new ParallaxClient('demo-api-key', GATEWAY_URL);
13
+
14
+ // State management
15
+ let currentTrace = null;
16
+ let attributes = {};
17
+ let tags = [];
18
+ let events = [];
19
+
20
+ // DOM elements
21
+ const elements = {
22
+ // Inputs
23
+ traceName: document.getElementById('traceName'),
24
+ fromAddress: document.getElementById('fromAddress'),
25
+ toAddress: document.getElementById('toAddress'),
26
+ value: document.getElementById('value'),
27
+ network: document.getElementById('network'),
28
+ attrKey: document.getElementById('attrKey'),
29
+ attrValue: document.getElementById('attrValue'),
30
+ tagInput: document.getElementById('tagInput'),
31
+ eventName: document.getElementById('eventName'),
32
+ eventDetails: document.getElementById('eventDetails'),
33
+ txHash: document.getElementById('txHash'),
34
+ chainId: document.getElementById('chainId'),
35
+
36
+ // Buttons
37
+ createTraceBtn: document.getElementById('createTraceBtn'),
38
+ addAttributeBtn: document.getElementById('addAttributeBtn'),
39
+ addTagBtn: document.getElementById('addTagBtn'),
40
+ addEventBtn: document.getElementById('addEventBtn'),
41
+ submitBtn: document.getElementById('submitBtn'),
42
+ resetBtn: document.getElementById('resetBtn'),
43
+
44
+ // Sections
45
+ attributesSection: document.getElementById('attributesSection'),
46
+ tagsSection: document.getElementById('tagsSection'),
47
+ eventsSection: document.getElementById('eventsSection'),
48
+ submitSection: document.getElementById('submitSection'),
49
+
50
+ // Display
51
+ traceInfo: document.getElementById('traceInfo'),
52
+ traceId: document.getElementById('traceId'),
53
+ txId: document.getElementById('txId'),
54
+ attributesContainer: document.getElementById('attributesContainer'),
55
+ tagsContainer: document.getElementById('tagsContainer'),
56
+ log: document.getElementById('log'),
57
+ status: document.getElementById('status'),
58
+ };
59
+
60
+ // Logging functions
61
+ function log(message, type = 'info') {
62
+ const time = new Date().toLocaleTimeString();
63
+ const entry = document.createElement('div');
64
+ entry.className = 'log-entry';
65
+ entry.innerHTML = `<span class="log-time">[${time}]</span> <span class="log-${type}">${message}</span>`;
66
+ elements.log.appendChild(entry);
67
+ elements.log.scrollTop = elements.log.scrollHeight;
68
+ }
69
+
70
+ function showStatus(message, type = 'info') {
71
+ elements.status.textContent = message;
72
+ elements.status.className = `status ${type} show`;
73
+ setTimeout(() => {
74
+ elements.status.classList.remove('show');
75
+ }, 5000);
76
+ }
77
+
78
+ // Step 1: Create Trace
79
+ elements.createTraceBtn.addEventListener('click', () => {
80
+ const traceName = elements.traceName.value.trim();
81
+ const from = elements.fromAddress.value.trim();
82
+ const to = elements.toAddress.value.trim();
83
+ const value = elements.value.value.trim();
84
+ const network = elements.network.value;
85
+
86
+ if (!traceName || !from || !to || !value) {
87
+ showStatus('Please fill in all transaction fields', 'error');
88
+ log('❌ Validation failed: Missing required fields', 'error');
89
+ return;
90
+ }
91
+
92
+ try {
93
+ // Initialize a new trace builder
94
+ currentTrace = client.trace(traceName, true); // includeClientMeta = true
95
+
96
+ // Add default transaction attributes
97
+ currentTrace
98
+ .addAttribute('from', from)
99
+ .addAttribute('to', to)
100
+ .addAttribute('value', value)
101
+ .addAttribute('network', network)
102
+ .addAttribute('timestamp', new Date().toISOString())
103
+ .addTags(['demo', 'web-app', network]);
104
+
105
+ // Generate a mock transaction ID for demo purposes
106
+ const mockTxId = `demo_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
107
+
108
+ // Show trace info
109
+ elements.traceInfo.classList.add('show');
110
+ elements.traceId.textContent = 'Pending (will be generated on submit)';
111
+ elements.txId.textContent = mockTxId;
112
+
113
+ // Show next sections
114
+ elements.attributesSection.classList.remove('hidden');
115
+ elements.tagsSection.classList.remove('hidden');
116
+ elements.eventsSection.classList.remove('hidden');
117
+ elements.submitSection.classList.remove('hidden');
118
+
119
+ log(`✅ Trace builder created: "${traceName}"`, 'success');
120
+ log(`📝 Added default attributes: from, to, value, network, timestamp`, 'info');
121
+ log(`đŸˇī¸ Added default tags: demo, web-app, ${network}`, 'info');
122
+ showStatus('Trace builder initialized! Add attributes, tags, and events below.', 'success');
123
+
124
+ // Disable create button
125
+ elements.createTraceBtn.disabled = true;
126
+ elements.createTraceBtn.textContent = 'Trace Created ✓';
127
+ } catch (error) {
128
+ log(`❌ Error creating trace: ${error.message}`, 'error');
129
+ showStatus(`Error: ${error.message}`, 'error');
130
+ }
131
+ });
132
+
133
+ // Step 2: Add Attributes
134
+ elements.addAttributeBtn.addEventListener('click', () => {
135
+ const key = elements.attrKey.value.trim();
136
+ const value = elements.attrValue.value.trim();
137
+
138
+ if (!key || !value) {
139
+ showStatus('Please enter both key and value', 'error');
140
+ return;
141
+ }
142
+
143
+ if (!currentTrace) {
144
+ showStatus('Please create a trace first', 'error');
145
+ return;
146
+ }
147
+
148
+ // Add to trace builder
149
+ currentTrace.addAttribute(key, value);
150
+ attributes[key] = value;
151
+
152
+ // Update UI
153
+ updateAttributesDisplay();
154
+
155
+ // Clear inputs
156
+ elements.attrKey.value = '';
157
+ elements.attrValue.value = '';
158
+
159
+ log(`➕ Added attribute: ${key} = "${value}"`, 'info');
160
+ showStatus('Attribute added successfully', 'success');
161
+ });
162
+
163
+ function updateAttributesDisplay() {
164
+ if (Object.keys(attributes).length === 0) {
165
+ elements.attributesContainer.innerHTML = '<em style="color: #6c757d; font-size: 13px;">No custom attributes added yet</em>';
166
+ return;
167
+ }
168
+
169
+ elements.attributesContainer.innerHTML = '';
170
+ for (const [key, value] of Object.entries(attributes)) {
171
+ const item = document.createElement('div');
172
+ item.className = 'attribute-item';
173
+ item.innerHTML = `
174
+ <span class="attribute-key">${key}:</span>
175
+ <span class="attribute-value">${value}</span>
176
+ `;
177
+ elements.attributesContainer.appendChild(item);
178
+ }
179
+ }
180
+
181
+ // Step 3: Add Tags
182
+ elements.addTagBtn.addEventListener('click', () => {
183
+ const tag = elements.tagInput.value.trim();
184
+
185
+ if (!tag) {
186
+ showStatus('Please enter a tag', 'error');
187
+ return;
188
+ }
189
+
190
+ if (!currentTrace) {
191
+ showStatus('Please create a trace first', 'error');
192
+ return;
193
+ }
194
+
195
+ if (tags.includes(tag)) {
196
+ showStatus('Tag already added', 'error');
197
+ return;
198
+ }
199
+
200
+ // Add to trace builder
201
+ currentTrace.addTag(tag);
202
+ tags.push(tag);
203
+
204
+ // Update UI
205
+ updateTagsDisplay();
206
+
207
+ // Clear input
208
+ elements.tagInput.value = '';
209
+
210
+ log(`đŸˇī¸ Added tag: "${tag}"`, 'info');
211
+ showStatus('Tag added successfully', 'success');
212
+ });
213
+
214
+ function updateTagsDisplay() {
215
+ if (tags.length === 0) {
216
+ elements.tagsContainer.innerHTML = '<em style="color: #6c757d; font-size: 13px;">No tags added yet</em>';
217
+ return;
218
+ }
219
+
220
+ elements.tagsContainer.innerHTML = '';
221
+ tags.forEach(tag => {
222
+ const chip = document.createElement('span');
223
+ chip.className = 'chip';
224
+ chip.textContent = tag;
225
+ elements.tagsContainer.appendChild(chip);
226
+ });
227
+ }
228
+
229
+ // Step 4: Add Events
230
+ elements.addEventBtn.addEventListener('click', () => {
231
+ const eventName = elements.eventName.value.trim();
232
+ const eventDetails = elements.eventDetails.value.trim();
233
+
234
+ if (!eventName) {
235
+ showStatus('Please enter an event name', 'error');
236
+ return;
237
+ }
238
+
239
+ if (!currentTrace) {
240
+ showStatus('Please create a trace first', 'error');
241
+ return;
242
+ }
243
+
244
+ try {
245
+ let details = eventDetails;
246
+
247
+ // Try to parse as JSON if it looks like JSON
248
+ if (eventDetails && (eventDetails.startsWith('{') || eventDetails.startsWith('['))) {
249
+ try {
250
+ details = JSON.parse(eventDetails);
251
+ } catch (e) {
252
+ // If parsing fails, use as string
253
+ details = eventDetails;
254
+ }
255
+ }
256
+
257
+ // Add to trace builder
258
+ currentTrace.addEvent(eventName, details || undefined);
259
+ events.push({ name: eventName, details });
260
+
261
+ // Clear inputs
262
+ elements.eventName.value = '';
263
+ elements.eventDetails.value = '';
264
+
265
+ log(`📌 Added event: "${eventName}"${details ? ` with details` : ''}`, 'info');
266
+ showStatus('Event added successfully', 'success');
267
+ } catch (error) {
268
+ log(`❌ Error adding event: ${error.message}`, 'error');
269
+ showStatus(`Error: ${error.message}`, 'error');
270
+ }
271
+ });
272
+
273
+ // Step 5: Submit Trace
274
+ elements.submitBtn.addEventListener('click', async () => {
275
+ if (!currentTrace) {
276
+ showStatus('Please create a trace first', 'error');
277
+ return;
278
+ }
279
+
280
+ const txHash = elements.txHash.value.trim();
281
+ const chainId = elements.chainId.value.trim();
282
+
283
+ try {
284
+ elements.submitBtn.disabled = true;
285
+ elements.submitBtn.textContent = 'Submitting...';
286
+
287
+ log('🚀 Submitting trace to Parallax Gateway...', 'info');
288
+
289
+ let response;
290
+ if (txHash && chainId) {
291
+ // Submit with transaction hash
292
+ response = await currentTrace.submit(txHash, chainId, 'Demo transaction');
293
+ log(`✅ Trace submitted with tx hash: ${txHash}`, 'success');
294
+ } else {
295
+ // Submit without transaction hash
296
+ response = await currentTrace.submit();
297
+ log('✅ Trace submitted without tx hash', 'success');
298
+ }
299
+
300
+ const traceId = response.getTraceId();
301
+ elements.traceId.textContent = traceId;
302
+
303
+ log(`🎉 Trace ID: ${traceId}`, 'success');
304
+ showStatus('Trace submitted successfully! Check the Activity Log for details.', 'success');
305
+
306
+ // Update button
307
+ elements.submitBtn.textContent = 'Submitted ✓';
308
+ elements.submitBtn.classList.remove('btn-success');
309
+ elements.submitBtn.classList.add('btn-secondary');
310
+
311
+ // Log summary
312
+ log('📊 Trace Summary:', 'info');
313
+ log(` - Name: ${elements.traceName.value}`, 'info');
314
+ log(` - Attributes: ${Object.keys(attributes).length + 5} total`, 'info');
315
+ log(` - Tags: ${tags.length + 3} total`, 'info');
316
+ log(` - Events: ${events.length} total`, 'info');
317
+ if (txHash) {
318
+ log(` - Transaction Hash: ${txHash}`, 'info');
319
+ log(` - Chain ID: ${chainId}`, 'info');
320
+ }
321
+ } catch (error) {
322
+ log(`❌ Error submitting trace: ${error.message}`, 'error');
323
+ showStatus(`Error: ${error.message}`, 'error');
324
+ elements.submitBtn.disabled = false;
325
+ elements.submitBtn.textContent = 'Submit Trace';
326
+ }
327
+ });
328
+
329
+ // Reset functionality
330
+ elements.resetBtn.addEventListener('click', () => {
331
+ if (confirm('Are you sure you want to reset and start over?')) {
332
+ // Reset state
333
+ currentTrace = null;
334
+ attributes = {};
335
+ tags = [];
336
+ events = [];
337
+
338
+ // Reset UI
339
+ elements.traceName.value = 'demo_transaction';
340
+ elements.fromAddress.value = '0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb';
341
+ elements.toAddress.value = '0x1234567890123456789012345678901234567890';
342
+ elements.value.value = '0.5';
343
+ elements.network.value = 'ethereum';
344
+ elements.txHash.value = '';
345
+ elements.chainId.value = '1';
346
+
347
+ // Hide sections
348
+ elements.attributesSection.classList.add('hidden');
349
+ elements.tagsSection.classList.add('hidden');
350
+ elements.eventsSection.classList.add('hidden');
351
+ elements.submitSection.classList.add('hidden');
352
+ elements.traceInfo.classList.remove('show');
353
+
354
+ // Reset buttons
355
+ elements.createTraceBtn.disabled = false;
356
+ elements.createTraceBtn.textContent = 'Create Trace';
357
+ elements.submitBtn.disabled = false;
358
+ elements.submitBtn.textContent = 'Submit Trace';
359
+ elements.submitBtn.classList.remove('btn-secondary');
360
+ elements.submitBtn.classList.add('btn-success');
361
+
362
+ // Clear displays
363
+ updateAttributesDisplay();
364
+ updateTagsDisplay();
365
+
366
+ log('🔄 Reset complete. Ready to create a new trace.', 'warn');
367
+ showStatus('Reset complete! You can create a new trace.', 'info');
368
+ }
369
+ });
370
+
371
+ // Keyboard shortcuts
372
+ document.addEventListener('keydown', (e) => {
373
+ // Ctrl/Cmd + Enter to submit in text areas
374
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
375
+ if (document.activeElement === elements.eventDetails) {
376
+ elements.addEventBtn.click();
377
+ }
378
+ }
379
+ });
380
+
381
+ log('🎨 Demo application loaded', 'success');
382
+ log('â„šī¸ Fill in the transaction details and click "Create Trace" to begin', 'info');
383
+
384
+ // Check if proxy is running
385
+ async function checkProxy() {
386
+ const proxyStatus = document.getElementById('proxyStatus');
387
+ if (!proxyStatus) return;
388
+
389
+ if (!USE_PROXY) {
390
+ proxyStatus.innerHTML = '<span style="color: #856404;">Direct connection (may have CORS issues)</span>';
391
+ return;
392
+ }
393
+
394
+ try {
395
+ const response = await fetch('http://localhost:3001/health');
396
+ const data = await response.json();
397
+ if (data.status === 'ok') {
398
+ proxyStatus.innerHTML = '<span style="color: #155724;">✓ Proxy server running on port 3001</span>';
399
+ log('✓ Proxy server is running', 'success');
400
+ } else {
401
+ proxyStatus.innerHTML = '<span style="color: #721c24;">✗ Proxy server error</span>';
402
+ log('✗ Proxy server returned unexpected response', 'error');
403
+ }
404
+ } catch (error) {
405
+ proxyStatus.innerHTML = '<span style="color: #721c24;">✗ Proxy server not running - run "npm run proxy" in a separate terminal</span>';
406
+ log('✗ Proxy server not running. Start it with: npm run proxy', 'error');
407
+ }
408
+ }
409
+
410
+ // Check proxy status on load
411
+ checkProxy();