mdas-jsview-sdk 1.0.17-uat.0 → 1.0.23-uat.0

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/mdas-sdk.js CHANGED
@@ -412,6 +412,85 @@
412
412
  console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
413
413
  }
414
414
  }
415
+
416
+ /**
417
+ * Validate initial symbol with the API and handle access/permission errors
418
+ * This is a reusable method for all widgets that need to validate symbols on initialization
419
+ *
420
+ * @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
421
+ * @param {string} symbol - The symbol to validate
422
+ * @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
423
+ * @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
424
+ */
425
+ async validateInitialSymbol(apiMethod, symbol) {
426
+ let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
427
+ try {
428
+ const apiService = this.wsManager?.getApiService();
429
+ if (!apiService) {
430
+ if (this.debug) {
431
+ console.log(`[${this.constructor.name}] API service not available for initial validation`);
432
+ }
433
+ return true; // Continue without validation
434
+ }
435
+ if (!apiService[apiMethod]) {
436
+ console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
437
+ return true; // Continue without validation
438
+ }
439
+ const result = await apiService[apiMethod](symbol);
440
+ if (result && result.data && result.data.length > 0) {
441
+ if (this.debug) {
442
+ console.log(`[${this.constructor.name}] Initial symbol validated:`, {
443
+ symbol,
444
+ method: apiMethod,
445
+ dataCount: result.data.length
446
+ });
447
+ }
448
+
449
+ // Call success callback if provided
450
+ if (onSuccess && typeof onSuccess === 'function') {
451
+ onSuccess(result.data, result);
452
+ }
453
+ return true; // Validation successful
454
+ }
455
+
456
+ // No data returned - might be access issue, but continue anyway
457
+ if (this.debug) {
458
+ console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
459
+ }
460
+ return true; // Continue anyway, WebSocket might still work
461
+ } catch (error) {
462
+ if (this.debug) {
463
+ console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
464
+ }
465
+
466
+ // Check if it's an access/permission error
467
+ const errorMessage = (error.message || '').toLowerCase();
468
+ const isAccessError = errorMessage.includes('400') ||
469
+ // Bad Request (often used for access issues)
470
+ errorMessage.includes('401') ||
471
+ // Unauthorized
472
+ errorMessage.includes('403') ||
473
+ // Forbidden
474
+ errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
475
+ // Specific OPRA access denial
476
+ errorMessage.includes('permission denied') || errorMessage.includes('access denied');
477
+ if (isAccessError) {
478
+ // Hide loading and show access error
479
+ this.hideLoading();
480
+
481
+ // Extract more specific error message if available
482
+ let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
483
+ if (errorMessage.includes('no opra access')) {
484
+ userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
485
+ }
486
+ this.showError(userMessage);
487
+ return false; // Stop initialization
488
+ }
489
+
490
+ // For other errors, continue with WebSocket (might still work)
491
+ return true;
492
+ }
493
+ }
415
494
  }
416
495
 
417
496
  // src/models/MarketDataModel.js
@@ -1283,6 +1362,24 @@
1283
1362
  <span class="l2-market-name"></span>
1284
1363
  </div>
1285
1364
  <span class="symbol editable-symbol">--</span>
1365
+ <div class="level1-info">
1366
+ <div class="l1-item">
1367
+ <span class="l1-label">Last:</span>
1368
+ <span class="l1-value l1-last-px">--</span>
1369
+ </div>
1370
+ <div class="l1-item">
1371
+ <span class="l1-label">Low:</span>
1372
+ <span class="l1-value l1-low-px">--</span>
1373
+ </div>
1374
+ <div class="l1-item">
1375
+ <span class="l1-label">High:</span>
1376
+ <span class="l1-value l1-high-px">--</span>
1377
+ </div>
1378
+ <div class="l1-item">
1379
+ <span class="l1-label">Volume:</span>
1380
+ <span class="l1-value l1-volume">--</span>
1381
+ </div>
1382
+ </div>
1286
1383
  </div>
1287
1384
  </div>
1288
1385
 
@@ -1957,6 +2054,38 @@
1957
2054
  color: #111827;
1958
2055
  }
1959
2056
 
2057
+ /* ========================================
2058
+ LEVEL 1 INFO (INSIDE HEADER)
2059
+ ======================================== */
2060
+
2061
+ .onbbo-level2-widget .level1-info {
2062
+ display: grid;
2063
+ grid-template-columns: repeat(4, 1fr);
2064
+ gap: 12px;
2065
+ margin-top: 10px;
2066
+ }
2067
+
2068
+ .onbbo-level2-widget .l1-item {
2069
+ display: flex;
2070
+ flex-direction: column;
2071
+ gap: 3px;
2072
+ }
2073
+
2074
+ .onbbo-level2-widget .l1-label {
2075
+ font-size: 11px;
2076
+ font-weight: 600;
2077
+ color: #6b7280;
2078
+ text-transform: uppercase;
2079
+ letter-spacing: 0.5px;
2080
+ }
2081
+
2082
+ .onbbo-level2-widget .l1-value {
2083
+ font-size: 15px;
2084
+ font-weight: 700;
2085
+ color: #111827;
2086
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2087
+ }
2088
+
1960
2089
  /* ========================================
1961
2090
  ORDER BOOK CONTAINER
1962
2091
  ======================================== */
@@ -2341,6 +2470,11 @@
2341
2470
  .onbbo-level2-widget .orderbook-panel:last-child {
2342
2471
  border-bottom: none;
2343
2472
  }
2473
+
2474
+ .onbbo-level2-widget .level1-info {
2475
+ grid-template-columns: repeat(2, 1fr);
2476
+ gap: 6px;
2477
+ }
2344
2478
  }
2345
2479
 
2346
2480
  @media (max-width: 480px) {
@@ -2359,6 +2493,19 @@
2359
2493
  text-align: left;
2360
2494
  }
2361
2495
 
2496
+ .onbbo-level2-widget .level1-info {
2497
+ grid-template-columns: repeat(2, 1fr);
2498
+ gap: 4px;
2499
+ }
2500
+
2501
+ .onbbo-level2-widget .l1-label {
2502
+ font-size: 9px;
2503
+ }
2504
+
2505
+ .onbbo-level2-widget .l1-value {
2506
+ font-size: 11px;
2507
+ }
2508
+
2362
2509
  .onbbo-level2-widget .panel-header {
2363
2510
  font-size: 10px;
2364
2511
  padding: 6px 2px;
@@ -2442,28 +2589,6 @@
2442
2589
  font-size: 1.71em;
2443
2590
  font-weight: 700;
2444
2591
  color: #111827;
2445
- cursor: pointer;
2446
- transition: all 0.2s ease;
2447
- position: relative;
2448
- }
2449
-
2450
- .time-sales-widget .symbol.editable-symbol:hover {
2451
- opacity: 0.8;
2452
- transform: scale(1.02);
2453
- }
2454
-
2455
- .time-sales-widget .symbol.editable-symbol:hover::after {
2456
- content: "✎";
2457
- position: absolute;
2458
- top: -8px;
2459
- right: -8px;
2460
- background: #3b82f6;
2461
- color: white;
2462
- font-size: 10px;
2463
- padding: 2px 4px;
2464
- border-radius: 4px;
2465
- opacity: 0.8;
2466
- pointer-events: none;
2467
2592
  }
2468
2593
 
2469
2594
  /* ========================================
@@ -4415,105 +4540,53 @@
4415
4540
  this.showLoading();
4416
4541
 
4417
4542
  // Validate initial symbol via API to get company info
4418
- await this.validateInitialSymbol();
4419
- this.subscribeToData();
4420
- }
4421
- async validateInitialSymbol() {
4422
- try {
4423
- const apiService = this.wsManager.getApiService();
4424
- if (!apiService) {
4425
- if (this.debug) {
4426
- console.log('[MarketDataWidget] API service not available for initial validation');
4427
- }
4428
- return;
4543
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
4544
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
4545
+ // Store for use in updateWidget
4546
+ if (data && data[0]) {
4547
+ this.initialValidationData = data[0];
4429
4548
  }
4430
- const result = await apiService.quotel1(this.symbol);
4431
- if (result && result.data && result.data[0]) {
4432
- const symbolData = result.data[0];
4433
- // Store for use in updateWidget
4434
- this.initialValidationData = symbolData;
4435
- if (this.debug) {
4436
- console.log('[MarketDataWidget] Initial symbol validated:', {
4437
- symbol: this.symbol,
4438
- companyName: symbolData.comp_name,
4439
- exchangeName: symbolData.market_name
4440
- });
4441
- }
4442
- }
4443
- } catch (error) {
4444
- if (this.debug) {
4445
- console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
4446
- }
4447
- // Don't throw - let the widget continue with WebSocket data
4549
+ });
4550
+
4551
+ // If validation failed due to access/permission issues, stop initialization
4552
+ if (validationSuccess === false) {
4553
+ return; // Error is already shown, don't continue
4448
4554
  }
4555
+ this.subscribeToData();
4449
4556
  }
4450
4557
  subscribeToData() {
4451
4558
  // Subscribe with symbol for routing
4452
4559
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
4453
4560
  );
4454
-
4455
- // Send subscription message
4456
- /* this.wsManager.send({
4457
- type: 'queryl1',
4458
- symbol: this.symbol
4459
- }); */
4460
4561
  }
4562
+ handleData(message) {
4563
+ //console.log('DEBUG', message)
4461
4564
 
4462
- /* handleMessage(messageWrapper) {
4463
- if (this.isDestroyed) return;
4464
- try {
4465
- const { event, data } = messageWrapper;
4466
- if (this.debug) {
4467
- console.log('[MarketDataWidget] Received:', event, data);
4468
- }
4469
- if (event === 'connection') {
4470
- this.handleConnectionStatus(data);
4471
- return;
4472
- }
4473
- if (event === 'data') {
4474
- this.handleData(data);
4475
- }
4476
- if (event === 'session_revoked') {
4477
- if (data.status === 'attempting_relogin') {
4478
- this.showLoading();
4479
- } else if (data.status === 'relogin_failed') {
4480
- this.showError(data.error);
4481
- } else if (data.status === 'relogin_successful') {
4482
- this.hideLoading();
4483
- }
4484
- return;
4485
- }
4486
- } catch (error) {
4487
- console.error('[MarketDataWidget] Error handling message:', error);
4488
- this.showError('Error processing data');
4565
+ // Check for error: false and display message if present
4566
+ if (message.error === true) {
4567
+ if (this.debug) {
4568
+ console.log('[MarketDataWidget] Received error response:', message.message);
4489
4569
  }
4490
- } */
4570
+ const errorMsg = message.message || message.Message;
4491
4571
 
4492
- /* handleConnectionStatus(status) {
4493
- if (status.status === 'connected') {
4494
- if (this.debug) {
4495
- console.log('[MarketDataWidget] Connected to WebSocket');
4496
- }
4497
- // Re-send subscription when reconnected
4498
- this.wsManager.send({
4499
- type: 'queryl1',
4500
- symbol: this.symbol
4501
- });
4502
- } else if (status.status === 'disconnected') {
4503
- this.showError('Disconnected from data service');
4504
- } else if (status.status === 'error') {
4505
- this.showError(status.error || 'Connection error');
4572
+ // If there's a message field, show it as an error
4573
+ if (errorMsg) {
4574
+ this.hideLoading();
4575
+ this.showError(errorMsg);
4576
+ return;
4506
4577
  }
4507
- } */
4508
-
4509
- handleData(message) {
4578
+ } else if (message.Data && typeof message.Data === 'string') {
4579
+ this.hideLoading();
4580
+ this.showError(message.Message);
4581
+ return;
4582
+ }
4510
4583
  if (this.loadingTimeout) {
4511
4584
  clearTimeout(this.loadingTimeout);
4512
4585
  this.loadingTimeout = null;
4513
4586
  }
4514
4587
 
4515
4588
  // Handle error messages from server (plain text converted to structured format)
4516
- if (message.type === 'error' && message.noData) {
4589
+ if (message.type === 'error') {
4517
4590
  if (this.debug) {
4518
4591
  console.log('[MarketDataWidget] Received no data message:', message.message);
4519
4592
  }
@@ -4592,126 +4665,6 @@
4592
4665
  this.showConnectionQuality(); // Show cache indicator
4593
4666
  }
4594
4667
  }
4595
- showNoDataState() {
4596
- let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
4597
- if (this.isDestroyed) return;
4598
- try {
4599
- // Hide loading overlay
4600
- this.hideLoading();
4601
-
4602
- // Clear any existing errors
4603
- this.clearError();
4604
-
4605
- // Update header with dimmed styling and basic info from data
4606
- const symbol = data.Symbol || this.symbol;
4607
-
4608
- // Add null checks for all DOM elements
4609
- const symbolElement = this.container.querySelector('.symbol');
4610
- if (symbolElement) {
4611
- symbolElement.textContent = symbol;
4612
- }
4613
- const companyNameElement = this.container.querySelector('.company-name');
4614
- if (companyNameElement) {
4615
- companyNameElement.textContent = `${symbol} Inc`;
4616
- }
4617
-
4618
- // Set price to $0.00 and add dimmed class
4619
- const currentPriceElement = this.container.querySelector('.current-price');
4620
- if (currentPriceElement) {
4621
- currentPriceElement.textContent = '$0.00';
4622
- }
4623
- const changeElement = this.container.querySelector('.price-change');
4624
- if (changeElement) {
4625
- const changeValueElement = changeElement.querySelector('.change-value');
4626
- const changePercentElement = changeElement.querySelector('.change-percent');
4627
- if (changeValueElement) {
4628
- changeValueElement.textContent = '+0.00';
4629
- }
4630
- if (changePercentElement) {
4631
- changePercentElement.textContent = ' (0.00%)';
4632
- }
4633
- changeElement.classList.remove('positive', 'negative');
4634
- changeElement.classList.add('neutral');
4635
- }
4636
-
4637
- // Add dimmed styling to header
4638
- const widgetHeader = this.container.querySelector('.widget-header');
4639
- if (widgetHeader) {
4640
- widgetHeader.classList.add('dimmed');
4641
- }
4642
-
4643
- // Replace the data grid with no data message
4644
- this.showNoDataMessage(symbol, data);
4645
-
4646
- // Update footer with current timestamp
4647
- const lastUpdateElement = this.container.querySelector('.last-update');
4648
- if (lastUpdateElement) {
4649
- const timestamp = formatTimestampET();
4650
- lastUpdateElement.textContent = `Checked: ${timestamp}`;
4651
- }
4652
- const dataSourceElement = this.container.querySelector('.data-source');
4653
- if (dataSourceElement) {
4654
- dataSourceElement.textContent = 'Source: No data available';
4655
- }
4656
- } catch (error) {
4657
- console.error('Error showing no data state:', error);
4658
- this.showError('Error displaying no data state');
4659
- }
4660
- }
4661
- showNoDataMessage(symbol) {
4662
- const dataGrid = this.container.querySelector('.data-grid');
4663
-
4664
- // Hide the data grid if it exists
4665
- if (dataGrid) {
4666
- dataGrid.style.display = 'none';
4667
- }
4668
-
4669
- // Remove existing no data message if present
4670
- const existingNoData = this.container.querySelector('.no-data-state');
4671
- if (existingNoData) {
4672
- existingNoData.remove();
4673
- }
4674
-
4675
- // Create no data message element - safely without XSS risk
4676
- const noDataElement = createElement('div', '', 'no-data-state');
4677
- const noDataContent = createElement('div', '', 'no-data-content');
4678
-
4679
- // Create icon
4680
- const iconDiv = document.createElement('div');
4681
- iconDiv.className = 'no-data-icon';
4682
- iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4683
- <circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
4684
- <path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
4685
- <circle cx="12" cy="16" r="1" fill="#9ca3af"/>
4686
- </svg>`;
4687
- noDataContent.appendChild(iconDiv);
4688
-
4689
- // Create title
4690
- const title = createElement('h3', 'No Market Data', 'no-data-title');
4691
- noDataContent.appendChild(title);
4692
-
4693
- // Create description with sanitized symbol
4694
- const description = createElement('p', '', 'no-data-description');
4695
- description.appendChild(document.createTextNode('Market data for '));
4696
- const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
4697
- description.appendChild(symbolStrong);
4698
- description.appendChild(document.createTextNode(' was not found'));
4699
- noDataContent.appendChild(description);
4700
-
4701
- // Create guidance
4702
- const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
4703
- noDataContent.appendChild(guidance);
4704
- noDataElement.appendChild(noDataContent);
4705
-
4706
- // Insert before footer, with null check
4707
- const footer = this.container.querySelector('.widget-footer');
4708
- if (footer && footer.parentNode) {
4709
- footer.parentNode.insertBefore(noDataElement, footer);
4710
- } else {
4711
- // Fallback: append to container if footer not found
4712
- this.container.appendChild(noDataElement);
4713
- }
4714
- }
4715
4668
  updateWidget(data) {
4716
4669
  if (this.isDestroyed) return;
4717
4670
  try {
@@ -5216,7 +5169,20 @@
5216
5169
  this.showLoading();
5217
5170
 
5218
5171
  // Validate initial symbol via API to get company info
5219
- await this.validateInitialSymbol();
5172
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
5173
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
5174
+ // Extract company info from first data item
5175
+ if (data && data[0]) {
5176
+ this.companyName = data[0].comp_name || '';
5177
+ this.exchangeName = data[0].market_name || '';
5178
+ this.mic = data[0].mic || '';
5179
+ }
5180
+ });
5181
+
5182
+ // If validation failed due to access/permission issues, stop initialization
5183
+ if (validationSuccess === false) {
5184
+ return; // Error is already shown, don't continue
5185
+ }
5220
5186
 
5221
5187
  // Set timeout to detect no data on initial load
5222
5188
  this.loadingTimeout = setTimeout(() => {
@@ -5233,38 +5199,6 @@
5233
5199
 
5234
5200
  this.subscribeToData();
5235
5201
  }
5236
- async validateInitialSymbol() {
5237
- try {
5238
- const apiService = this.wsManager.getApiService();
5239
- if (!apiService) {
5240
- if (this.debug) {
5241
- console.log('[NightSessionWidget] API service not available for initial validation');
5242
- }
5243
- return;
5244
- }
5245
- const result = await apiService.quotel1(this.symbol);
5246
- if (result && result.data && result.data[0]) {
5247
- const symbolData = result.data[0];
5248
- // Extract company info
5249
- this.companyName = symbolData.comp_name || '';
5250
- this.exchangeName = symbolData.market_name || '';
5251
- this.mic = symbolData.mic || '';
5252
- if (this.debug) {
5253
- console.log('[NightSessionWidget] Initial symbol validated:', {
5254
- symbol: this.symbol,
5255
- companyName: this.companyName,
5256
- exchangeName: this.exchangeName,
5257
- mic: this.mic
5258
- });
5259
- }
5260
- }
5261
- } catch (error) {
5262
- if (this.debug) {
5263
- console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
5264
- }
5265
- // Don't throw - let the widget continue with WebSocket data
5266
- }
5267
- }
5268
5202
  subscribeToData() {
5269
5203
  let subscriptionType;
5270
5204
  if (this.source === 'bruce') {
@@ -5283,71 +5217,53 @@
5283
5217
  // to avoid duplicate timeouts
5284
5218
  }
5285
5219
  handleData(message) {
5220
+ console.log('DEBUG NIGHT', message);
5221
+
5222
+ //message = message.data || message.Data
5223
+
5286
5224
  if (this.loadingTimeout) {
5287
5225
  clearTimeout(this.loadingTimeout);
5288
5226
  this.loadingTimeout = null;
5289
5227
  }
5290
5228
 
5291
- // Handle error messages from server (plain text converted to structured format)
5292
- if (message.type === 'error' && message.noData) {
5229
+ // Check for error: false and display message if present
5230
+ if (message.error === true) {
5293
5231
  if (this.debug) {
5294
- console.log('[NightSessionWidget] Received no data message:', message.message);
5232
+ console.log('[Nigh] Received error response:', message.message);
5295
5233
  }
5296
- // Only show no data state if we don't have cached data
5297
- if (!this.data) {
5298
- this.showNoDataState({
5299
- Symbol: this.symbol,
5300
- NotFound: true,
5301
- message: message.message
5302
- });
5303
- } else {
5234
+ const errorMsg = message.message || message.Message;
5235
+
5236
+ // If there's a message field, show it as an error
5237
+ if (errorMsg) {
5304
5238
  this.hideLoading();
5305
- if (this.debug) {
5306
- console.log('[NightSessionWidget] No new data, keeping cached data visible');
5307
- }
5239
+ this.showError(errorMsg);
5240
+ return;
5308
5241
  }
5242
+ } else if (message.Data && typeof message.Data === 'string') {
5243
+ this.hideLoading();
5244
+ this.showError(message.Message);
5309
5245
  return;
5310
5246
  }
5311
5247
 
5312
- // Handle general error messages - CHECK IF IT'S ACCESS RELATED
5248
+ // Handle error messages from server (plain text converted to structured format)
5313
5249
  if (message.type === 'error') {
5314
- const errorMsg = message.message || 'Server error';
5315
-
5316
- // If it's an access/permission error, show as no-data state instead of separate error
5317
- if (errorMsg.toLowerCase().includes('access') || errorMsg.toLowerCase().includes('permission') || errorMsg.toLowerCase().includes('denied')) {
5318
- // Only show no data state if we don't have cached data
5319
- if (!this.data) {
5320
- this.showNoDataState({
5321
- Symbol: this.symbol,
5322
- NotFound: true,
5323
- isAccessError: true,
5324
- message: errorMsg
5325
- });
5326
- } else {
5327
- this.hideLoading();
5328
- if (this.debug) {
5329
- console.log('[NightSessionWidget] Access error but keeping cached data');
5330
- }
5331
- }
5250
+ if (this.debug) {
5251
+ console.log('[NightSessionWidget] Received no data message:', message.message);
5252
+ }
5253
+ // Only show no data state if we don't have cached data
5254
+ if (!this.data) {
5255
+ this.showError(errorMsg);
5332
5256
  } else {
5333
- // Only show error if we don't have cached data
5334
- if (!this.data) {
5335
- console.log('errorMsg', errorMsg);
5336
- this.showError(errorMsg);
5337
- } else {
5338
- this.hideLoading();
5339
- if (this.debug) {
5340
- console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
5341
- }
5257
+ this.hideLoading();
5258
+ if (this.debug) {
5259
+ console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
5342
5260
  }
5343
5261
  }
5344
5262
  return;
5345
5263
  }
5346
-
5347
- // Filter for night session data
5348
- if (Array.isArray(message)) {
5264
+ if (Array.isArray(message.data)) {
5349
5265
  // First, try to find data matching our symbol regardless of MarketName
5350
- const symbolData = message.find(item => item.Symbol === this.symbol);
5266
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
5351
5267
  if (symbolData) {
5352
5268
  if (this.debug) {
5353
5269
  console.log('[NightSessionWidget] Found data for symbol:', symbolData);
@@ -5387,34 +5303,35 @@
5387
5303
  }
5388
5304
  }
5389
5305
  // Handle wrapped format
5390
- else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1' || message.type === 'queryonbbol1') {
5391
- if (message['0']?.Symbol === this.symbol) {
5392
- // For onbbo source, skip MarketName check
5393
- const isOnbbo = message.type === 'queryonbbol1';
5394
- const shouldShowNoData = message['0'].NotFound === true || !isOnbbo && (!message['0'].MarketName || message['0'].MarketName !== 'BLUE');
5395
- if (shouldShowNoData) {
5396
- // Only show no data state if we don't have cached data
5397
- if (!this.data) {
5398
- this.showNoDataState(message['0']);
5399
- } else {
5400
- this.hideLoading();
5401
- if (this.debug) {
5402
- console.log('[NightSessionWidget] No new data, keeping cached data visible');
5306
+ /* else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1' || message.type === 'queryonbbol1') {
5307
+ if (message.data['0']?.Symbol === this.symbol) {
5308
+ // For onbbo source, skip MarketName check
5309
+ const isOnbbo = message.type === 'queryonbbol1';
5310
+ const shouldShowNoData = message['0'].NotFound === true ||
5311
+ (!isOnbbo && (!message['0'].MarketName || message['0'].MarketName !== 'BLUE'));
5312
+ if (shouldShowNoData) {
5313
+ // Only show no data state if we don't have cached data
5314
+ if (!this.data) {
5315
+ this.showNoDataState(message['0']);
5316
+ } else {
5317
+ this.hideLoading();
5318
+ if (this.debug) {
5319
+ console.log('[NightSessionWidget] No new data, keeping cached data visible');
5320
+ }
5321
+ }
5322
+ } else {
5323
+ const model = new NightSessionModel(message['0']);
5324
+ this.data = model; // Store for caching
5325
+ this.updateWidget(model);
5403
5326
  }
5404
- }
5405
5327
  } else {
5406
- const model = new NightSessionModel(message['0']);
5407
- this.data = model; // Store for caching
5408
- this.updateWidget(model);
5409
- }
5410
- } else {
5411
- // No matching symbol - keep cached data if available
5412
- if (this.debug) {
5413
- console.log('[NightSessionWidget] No matching symbol in response, keeping cached data');
5328
+ // No matching symbol - keep cached data if available
5329
+ if (this.debug) {
5330
+ console.log('[NightSessionWidget] No matching symbol in response, keeping cached data');
5331
+ }
5332
+ this.hideLoading();
5414
5333
  }
5415
- this.hideLoading();
5416
- }
5417
- }
5334
+ } */
5418
5335
  if (message._cached) ;
5419
5336
  }
5420
5337
  updateWidget(data) {
@@ -6244,36 +6161,20 @@
6244
6161
  this.showLoading();
6245
6162
 
6246
6163
  // Validate initial symbol via API to get symbol info
6247
- await this.validateInitialSymbol();
6248
- this.subscribeToData();
6249
- }
6250
- async validateInitialSymbol() {
6251
- try {
6252
- const apiService = this.wsManager.getApiService();
6253
- if (!apiService) {
6254
- if (this.debug) {
6255
- console.log('[OptionsWidget] API service not available for initial validation');
6256
- }
6257
- return;
6164
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
6165
+ const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
6166
+ // Store for use in updateWidget
6167
+ // Note: quoteOptionl1 returns array directly, not wrapped in data field
6168
+ if (data && data[0] && !data[0].error && !data[0].not_found) {
6169
+ this.initialValidationData = data[0];
6258
6170
  }
6259
- const result = await apiService.quoteOptionl1(this.symbol);
6260
- if (result && result[0] && !result[0].error && !result[0].not_found) {
6261
- const symbolData = result[0];
6262
- // Store for use in updateWidget
6263
- this.initialValidationData = symbolData;
6264
- if (this.debug) {
6265
- console.log('[OptionsWidget] Initial symbol validated:', {
6266
- symbol: this.symbol,
6267
- underlying: symbolData.Underlying || symbolData.RootSymbol
6268
- });
6269
- }
6270
- }
6271
- } catch (error) {
6272
- if (this.debug) {
6273
- console.warn('[OptionsWidget] Initial symbol validation failed:', error);
6274
- }
6275
- // Don't throw - let the widget continue with WebSocket data
6171
+ });
6172
+
6173
+ // If validation failed due to access/permission issues, stop initialization
6174
+ if (validationSuccess === false) {
6175
+ return; // Error is already shown, don't continue
6276
6176
  }
6177
+ this.subscribeToData();
6277
6178
  }
6278
6179
  subscribeToData() {
6279
6180
  // Subscribe with symbol for routing
@@ -38630,7 +38531,20 @@ ${SharedStyles}
38630
38531
  }
38631
38532
  async initialize() {
38632
38533
  // Validate initial symbol via API to get company info
38633
- await this.validateInitialSymbol();
38534
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
38535
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
38536
+ // Extract company info from first data item
38537
+ if (data && data[0]) {
38538
+ this.companyName = data[0].comp_name || '';
38539
+ this.exchangeName = data[0].market_name || '';
38540
+ this.mic = data[0].mic || '';
38541
+ }
38542
+ });
38543
+
38544
+ // If validation failed due to access/permission issues, stop initialization
38545
+ if (validationSuccess === false) {
38546
+ return; // Error is already shown, don't continue
38547
+ }
38634
38548
 
38635
38549
  // Update company name in the header
38636
38550
  this.updateCompanyName();
@@ -38638,38 +38552,6 @@ ${SharedStyles}
38638
38552
  // Load chart data
38639
38553
  await this.loadChartData();
38640
38554
  }
38641
- async validateInitialSymbol() {
38642
- try {
38643
- const apiService = this.wsManager.getApiService();
38644
- if (!apiService) {
38645
- if (this.debug) {
38646
- console.log('[IntradayChartWidget] API service not available for initial validation');
38647
- }
38648
- return;
38649
- }
38650
- const result = await apiService.quotel1(this.symbol);
38651
- if (result && result.data && result.data[0]) {
38652
- const symbolData = result.data[0];
38653
- // Extract company info
38654
- this.companyName = symbolData.comp_name || '';
38655
- this.exchangeName = symbolData.market_name || '';
38656
- this.mic = symbolData.mic || '';
38657
- if (this.debug) {
38658
- console.log('[IntradayChartWidget] Initial symbol validated:', {
38659
- symbol: this.symbol,
38660
- companyName: this.companyName,
38661
- exchangeName: this.exchangeName,
38662
- mic: this.mic
38663
- });
38664
- }
38665
- }
38666
- } catch (error) {
38667
- if (this.debug) {
38668
- console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
38669
- }
38670
- // Don't throw - let the widget continue with chart data
38671
- }
38672
- }
38673
38555
  updateCompanyName() {
38674
38556
  const companyNameElement = this.container.querySelector('.intraday-company-name');
38675
38557
  if (companyNameElement) {
@@ -39091,21 +38973,14 @@ ${SharedStyles}
39091
38973
  let priceData = null;
39092
38974
 
39093
38975
  // Handle array format (standard night session format)
39094
- if (Array.isArray(message)) {
38976
+ if (Array.isArray(message.data)) {
39095
38977
  console.log('[IntradayChartWidget] Processing array format, length:', message.length);
39096
- const symbolData = message.find(item => item.Symbol === this.symbol);
38978
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
39097
38979
  console.log('[IntradayChartWidget] Found symbol data:', symbolData);
39098
38980
  if (symbolData && !symbolData.NotFound) {
39099
38981
  priceData = symbolData;
39100
38982
  }
39101
38983
  }
39102
- // Handle wrapped format
39103
- else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
39104
- console.log('[IntradayChartWidget] Processing wrapped format');
39105
- if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
39106
- priceData = message['0'];
39107
- }
39108
- }
39109
38984
  // Handle direct data format
39110
38985
  else if (message.Symbol === this.symbol && !message.NotFound) {
39111
38986
  console.log('[IntradayChartWidget] Processing direct format');
@@ -40062,8 +39937,10 @@ ${SharedStyles}
40062
39937
  this.styled = options.styled !== undefined ? options.styled : true;
40063
39938
  this.maxLevels = options.maxLevels || 10; // Number of levels to display
40064
39939
  this.data = null;
39940
+ this.level1Data = null; // Store Level 1 data separately
40065
39941
  this.isDestroyed = false;
40066
- this.unsubscribe = null;
39942
+ this.unsubscribeL2 = null;
39943
+ this.unsubscribeL1 = null;
40067
39944
  this.symbolEditor = null;
40068
39945
  this.loadingTimeout = null;
40069
39946
 
@@ -40164,13 +40041,22 @@ ${SharedStyles}
40164
40041
  this.showError(`No data received for ${upperSymbol}. Please try again.`);
40165
40042
  }, 10000);
40166
40043
 
40167
- // Unsubscribe from old symbol
40168
- if (this.unsubscribe) {
40044
+ // Unsubscribe from old symbol's L2 and L1 data
40045
+ if (this.unsubscribeL2) {
40169
40046
  if (this.debug) {
40170
- console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol}`);
40047
+ console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol} L2`);
40171
40048
  }
40172
- this.unsubscribe();
40173
- this.unsubscribe = null;
40049
+ this.wsManager.sendUnsubscribe('queryonbbol2', this.symbol);
40050
+ this.unsubscribeL2();
40051
+ this.unsubscribeL2 = null;
40052
+ }
40053
+ if (this.unsubscribeL1) {
40054
+ if (this.debug) {
40055
+ console.log(`[ONBBOLevel2Widget] Unsubscribing from ${this.symbol} L1`);
40056
+ }
40057
+ this.wsManager.sendUnsubscribe('queryonbbol1', this.symbol);
40058
+ this.unsubscribeL1();
40059
+ this.unsubscribeL1 = null;
40174
40060
  }
40175
40061
 
40176
40062
  // Update internal symbol
@@ -40197,7 +40083,19 @@ ${SharedStyles}
40197
40083
  this.showLoading();
40198
40084
 
40199
40085
  // Fetch company info for initial symbol
40200
- await this.validateInitialSymbol();
40086
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
40087
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
40088
+ // Extract company info from first data item
40089
+ if (data && data[0]) {
40090
+ this.companyName = data[0].comp_name || '';
40091
+ this.exchangeName = data[0].market_name || '';
40092
+ }
40093
+ });
40094
+
40095
+ // If validation failed due to access/permission issues, stop initialization
40096
+ if (validationSuccess === false) {
40097
+ return; // Error is already shown, don't continue
40098
+ }
40201
40099
 
40202
40100
  // Set timeout to detect no data on initial load
40203
40101
  this.loadingTimeout = setTimeout(() => {
@@ -40209,41 +40107,52 @@ ${SharedStyles}
40209
40107
  }, 10000);
40210
40108
  this.subscribeToData();
40211
40109
  }
40212
- async validateInitialSymbol() {
40213
- try {
40214
- const apiService = this.wsManager.getApiService();
40215
- if (!apiService) {
40216
- if (this.debug) {
40217
- console.log('[ONBBO L2] API service not available for initial validation');
40218
- }
40110
+ subscribeToData() {
40111
+ // Subscribe to ONBBO Level 2 data (order book)
40112
+ // Use separate widget IDs to avoid "already active" duplicate detection
40113
+ this.unsubscribeL2 = this.wsManager.subscribe(`${this.widgetId}-l2`, ['queryonbbol2'], messageWrapper => {
40114
+ const {
40115
+ event,
40116
+ data
40117
+ } = messageWrapper;
40118
+
40119
+ // Handle connection events
40120
+ if (event === 'connection') {
40121
+ this.handleConnectionStatus(data);
40219
40122
  return;
40220
40123
  }
40221
- const result = await apiService.quotel1(this.symbol);
40222
- if (result && result.data && result.data[0]) {
40223
- const symbolData = result.data[0];
40224
- // Extract company info
40225
- this.companyName = symbolData.comp_name || '';
40226
- this.exchangeName = symbolData.market_name || '';
40227
- if (this.debug) {
40228
- console.log('[ONBBO L2] Initial symbol validated:', {
40229
- symbol: this.symbol,
40230
- companyName: this.companyName,
40231
- exchangeName: this.exchangeName
40232
- });
40233
- }
40124
+
40125
+ // For data events, add type context
40126
+ if (event === 'data') {
40127
+ data._dataType = 'level2';
40128
+ this.handleMessage({
40129
+ event,
40130
+ data
40131
+ });
40234
40132
  }
40235
- } catch (error) {
40236
- if (this.debug) {
40237
- console.warn('[ONBBO L2] Initial symbol validation failed:', error);
40133
+ }, this.symbol);
40134
+
40135
+ // Subscribe to ONBBO Level 1 data (statistics: LastPx, LowPx, HighPx, Volume)
40136
+ this.unsubscribeL1 = this.wsManager.subscribe(`${this.widgetId}-l1`, ['queryonbbol1'], messageWrapper => {
40137
+ const {
40138
+ event,
40139
+ data
40140
+ } = messageWrapper;
40141
+
40142
+ // Connection already handled by L2 subscription
40143
+ if (event === 'connection') {
40144
+ return;
40238
40145
  }
40239
- // Don't throw - let the widget continue with WebSocket data
40240
- }
40241
- }
40242
- subscribeToData() {
40243
- // Subscribe to ONBBO Level 2 data
40244
- this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbol2'],
40245
- // ONBBO Level 2 subscription type
40246
- this.handleMessage.bind(this), this.symbol);
40146
+
40147
+ // For data events, add type context
40148
+ if (event === 'data') {
40149
+ data._dataType = 'level1';
40150
+ this.handleMessage({
40151
+ event,
40152
+ data
40153
+ });
40154
+ }
40155
+ }, this.symbol);
40247
40156
  }
40248
40157
  handleData(message) {
40249
40158
  if (this.loadingTimeout) {
@@ -40251,8 +40160,14 @@ ${SharedStyles}
40251
40160
  this.loadingTimeout = null;
40252
40161
  }
40253
40162
 
40163
+ // Extract data type from metadata (added by subscription callback)
40164
+ const dataType = message._dataType;
40165
+ if (this.debug) {
40166
+ console.log(`[ONBBOLevel2Widget] handleData called with type: ${dataType}`, message);
40167
+ }
40168
+
40254
40169
  // Handle error messages
40255
- if (message.type === 'error') {
40170
+ if (message.type === 'error' || message.error == true) {
40256
40171
  const errorMsg = message.message || 'Server error';
40257
40172
  if (this.debug) {
40258
40173
  console.log('[ONBBOLevel2Widget] Error:', errorMsg);
@@ -40261,16 +40176,54 @@ ${SharedStyles}
40261
40176
  return;
40262
40177
  }
40263
40178
 
40179
+ // Handle Level 1 data (statistics: LastPx, LowPx, HighPx, Volume)
40180
+ if (message.type === 'queryonbbol1') {
40181
+ if (this.debug) {
40182
+ console.log('[ONBBOLevel2Widget] Processing Level 1 data:', message);
40183
+ }
40184
+
40185
+ // Handle array format (new format with Data wrapper)
40186
+ if (message.data && Array.isArray(message.data)) {
40187
+ const l1Data = message.data.find(d => d.Symbol === this.symbol);
40188
+ if (l1Data) {
40189
+ this.level1Data = {
40190
+ lastPx: l1Data.LastPx || 0,
40191
+ lowPx: l1Data.LowPx || 0,
40192
+ highPx: l1Data.HighPx || 0,
40193
+ volume: l1Data.Volume || 0
40194
+ };
40195
+ this.updateLevel1Display();
40196
+ }
40197
+ }
40198
+ // Handle direct array format (standard format)
40199
+ else if (Array.isArray(message)) {
40200
+ const l1Data = message.find(d => d.Symbol === this.symbol);
40201
+ if (l1Data) {
40202
+ this.level1Data = {
40203
+ lastPx: l1Data.LastPx || 0,
40204
+ lowPx: l1Data.LowPx || 0,
40205
+ highPx: l1Data.HighPx || 0,
40206
+ volume: l1Data.Volume || 0
40207
+ };
40208
+ this.updateLevel1Display();
40209
+ }
40210
+ }
40211
+
40212
+ // Clean up metadata
40213
+ delete message._dataType;
40214
+ return;
40215
+ }
40216
+
40264
40217
  // Handle Level 2 data - Array of MMID quotes
40265
- if (Array.isArray(message)) {
40218
+ if (Array.isArray(message.data)) {
40266
40219
  // Check if it's an array of MMID objects (ONBBO format)
40267
- if (message.length > 0 && message[0].MMID) {
40220
+ if (message.data.length > 0 && message.data[0].MMID) {
40268
40221
  if (this.debug) {
40269
40222
  console.log('[ONBBOLevel2Widget] Received MMID array:', message);
40270
40223
  }
40271
40224
 
40272
40225
  // Create model from MMID array
40273
- const model = new ONBBOLevel2Model(message);
40226
+ const model = new ONBBOLevel2Model(message.data);
40274
40227
  model.symbol = this.symbol; // Set symbol from widget
40275
40228
  this.data = model;
40276
40229
  this.updateWidget(model);
@@ -40278,7 +40231,7 @@ ${SharedStyles}
40278
40231
  }
40279
40232
 
40280
40233
  // Try to find symbol in array (alternative format)
40281
- const symbolData = message.find(item => item.Symbol === this.symbol);
40234
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
40282
40235
  if (symbolData) {
40283
40236
  if (symbolData.NotFound === true) {
40284
40237
  this.showError(`No Level 2 data available for ${this.symbol}`);
@@ -40292,32 +40245,31 @@ ${SharedStyles}
40292
40245
  this.showError(`No Level 2 data available for ${this.symbol}`);
40293
40246
  }
40294
40247
  // Handle wrapped format
40295
- else if (message.type === 'queryonbbol2') {
40296
- // Check if wrapped data contains MMID array
40297
- if (message['0'] && Array.isArray(message['0'])) {
40298
- if (this.debug) {
40299
- console.log('[ONBBOLevel2Widget] Received wrapped MMID array:', message['0']);
40248
+ /* else if (message.type === 'queryonbbol2') {
40249
+ // Check if wrapped data contains MMID array
40250
+ if (message['0'] && Array.isArray(message['0'])) {
40251
+ if (this.debug) {
40252
+ console.log('[ONBBOLevel2Widget] Received wrapped MMID array:', message['0']);
40253
+ }
40254
+ const model = new ONBBOLevel2Model(message['0']);
40255
+ model.symbol = this.symbol;
40256
+ this.data = model;
40257
+ this.updateWidget(model);
40258
+ return;
40300
40259
  }
40301
- const model = new ONBBOLevel2Model(message['0']);
40302
- model.symbol = this.symbol;
40303
- this.data = model;
40304
- this.updateWidget(model);
40305
- return;
40306
- }
40307
-
40308
- // Check for symbol match in wrapped format
40309
- if (message['0']?.Symbol === this.symbol) {
40310
- if (message['0'].NotFound === true) {
40311
- this.showError(`No Level 2 data available for ${this.symbol}`);
40260
+ // Check for symbol match in wrapped format
40261
+ if (message['0']?.Symbol === this.symbol) {
40262
+ if (message['0'].NotFound === true) {
40263
+ this.showError(`No Level 2 data available for ${this.symbol}`);
40264
+ } else {
40265
+ const model = new ONBBOLevel2Model(message['0']);
40266
+ this.data = model;
40267
+ this.updateWidget(model);
40268
+ }
40312
40269
  } else {
40313
- const model = new ONBBOLevel2Model(message['0']);
40314
- this.data = model;
40315
- this.updateWidget(model);
40270
+ this.showError(`No Level 2 data available for ${this.symbol}`);
40316
40271
  }
40317
- } else {
40318
- this.showError(`No Level 2 data available for ${this.symbol}`);
40319
- }
40320
- }
40272
+ } */
40321
40273
  }
40322
40274
  updateWidget(data) {
40323
40275
  if (this.isDestroyed) return;
@@ -40415,6 +40367,43 @@ ${SharedStyles}
40415
40367
  askBody.appendChild(row);
40416
40368
  });
40417
40369
  }
40370
+ updateLevel1Display() {
40371
+ if (!this.level1Data) return;
40372
+ const formatPrice = price => {
40373
+ return price ? price.toFixed(2) : '--';
40374
+ };
40375
+ const formatVolume = volume => {
40376
+ if (!volume) return '--';
40377
+ return volume.toLocaleString();
40378
+ };
40379
+
40380
+ // Update Last Price
40381
+ const lastPxElement = this.container.querySelector('.l1-last-px');
40382
+ if (lastPxElement) {
40383
+ lastPxElement.textContent = formatPrice(this.level1Data.lastPx);
40384
+ }
40385
+
40386
+ // Update Low Price
40387
+ const lowPxElement = this.container.querySelector('.l1-low-px');
40388
+ if (lowPxElement) {
40389
+ lowPxElement.textContent = formatPrice(this.level1Data.lowPx);
40390
+ }
40391
+
40392
+ // Update High Price
40393
+ const highPxElement = this.container.querySelector('.l1-high-px');
40394
+ if (highPxElement) {
40395
+ highPxElement.textContent = formatPrice(this.level1Data.highPx);
40396
+ }
40397
+
40398
+ // Update Volume
40399
+ const volumeElement = this.container.querySelector('.l1-volume');
40400
+ if (volumeElement) {
40401
+ volumeElement.textContent = formatVolume(this.level1Data.volume);
40402
+ }
40403
+ if (this.debug) {
40404
+ console.log('[ONBBOLevel2Widget] Updated Level 1 display:', this.level1Data);
40405
+ }
40406
+ }
40418
40407
  showLoading() {
40419
40408
  const loadingOverlay = this.container.querySelector('.widget-loading-overlay');
40420
40409
  if (loadingOverlay) {
@@ -40461,9 +40450,17 @@ ${SharedStyles}
40461
40450
  this.clearTimeout(this.loadingTimeout);
40462
40451
  this.loadingTimeout = null;
40463
40452
  }
40464
- if (this.unsubscribe) {
40465
- this.unsubscribe();
40466
- this.unsubscribe = null;
40453
+
40454
+ // Unsubscribe from L2 data
40455
+ if (this.unsubscribeL2) {
40456
+ this.unsubscribeL2();
40457
+ this.unsubscribeL2 = null;
40458
+ }
40459
+
40460
+ // Unsubscribe from L1 data
40461
+ if (this.unsubscribeL1) {
40462
+ this.unsubscribeL1();
40463
+ this.unsubscribeL1 = null;
40467
40464
  }
40468
40465
 
40469
40466
  // Destroy the symbol editor
@@ -40628,6 +40625,7 @@ ${SharedStyles}
40628
40625
  this.styled = options.styled !== undefined ? options.styled : true;
40629
40626
  this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
40630
40627
  this.data = new TimeSalesModel([], this.maxTrades);
40628
+ this.data.symbol = this.symbol; // Set the symbol in the data model
40631
40629
  this.isDestroyed = false;
40632
40630
  this.unsubscribe = null;
40633
40631
  this.symbolEditor = null;
@@ -40639,6 +40637,7 @@ ${SharedStyles}
40639
40637
 
40640
40638
  // Create widget structure
40641
40639
  this.createWidgetStructure();
40640
+ this.initializeSymbolEditor();
40642
40641
 
40643
40642
  // Initialize the widget
40644
40643
  this.initialize();
@@ -40655,7 +40654,26 @@ ${SharedStyles}
40655
40654
  }
40656
40655
  }
40657
40656
  this.addStyles();
40658
- this.setupSymbolEditor();
40657
+ //this.setupSymbolEditor();
40658
+ }
40659
+ initializeSymbolEditor() {
40660
+ // Initialize symbol editor with stock symbol validation
40661
+ this.symbolEditor = new SymbolEditor(this, {
40662
+ maxLength: 10,
40663
+ placeholder: 'Enter symbol...',
40664
+ //validator: this.validateStockSymbol.bind(this),
40665
+ onSymbolChange: this.handleSymbolChange.bind(this),
40666
+ debug: this.debug,
40667
+ autoUppercase: true,
40668
+ symbolType: 'quotel1'
40669
+ });
40670
+
40671
+ // Set initial symbol
40672
+ const symbolElement = this.container.querySelector('.symbol');
40673
+ if (symbolElement) {
40674
+ symbolElement.textContent = this.symbol;
40675
+ symbolElement.dataset.originalSymbol = this.symbol;
40676
+ }
40659
40677
  }
40660
40678
  addStyles() {
40661
40679
  // Inject styles from the styles file into document head
@@ -40678,6 +40696,8 @@ ${SharedStyles}
40678
40696
  // Keep editor open when clicking buttons
40679
40697
  symbolType: 'quotel1' // Use standard quote validation
40680
40698
  });
40699
+
40700
+ //console.log('SETUP COMPLETE')
40681
40701
  }
40682
40702
  async handleSymbolChange(newSymbol, oldSymbol) {
40683
40703
  let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
@@ -40780,7 +40800,19 @@ ${SharedStyles}
40780
40800
  this.showLoading();
40781
40801
 
40782
40802
  // Validate initial symbol via API to get company info
40783
- await this.validateInitialSymbol();
40803
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
40804
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
40805
+ // Extract company info from first data item
40806
+ if (data && data[0]) {
40807
+ this.companyName = data[0].comp_name || '';
40808
+ this.exchangeName = data[0].market_name || '';
40809
+ }
40810
+ });
40811
+
40812
+ // If validation failed due to access/permission issues, stop initialization
40813
+ if (validationSuccess === false) {
40814
+ return; // Error is already shown, don't continue
40815
+ }
40784
40816
 
40785
40817
  // Set timeout to detect no data on initial load
40786
40818
  this.loadingTimeout = setTimeout(() => {
@@ -40793,36 +40825,6 @@ ${SharedStyles}
40793
40825
 
40794
40826
  this.subscribeToData();
40795
40827
  }
40796
- async validateInitialSymbol() {
40797
- try {
40798
- const apiService = this.wsManager.getApiService();
40799
- if (!apiService) {
40800
- if (this.debug) {
40801
- console.log('[TimeSalesWidget] API service not available for initial validation');
40802
- }
40803
- return;
40804
- }
40805
- const result = await apiService.quotel1(this.symbol);
40806
- if (result && result.data && result.data[0]) {
40807
- const symbolData = result.data[0];
40808
- // Extract company info
40809
- this.companyName = symbolData.comp_name || '';
40810
- this.exchangeName = symbolData.market_name || '';
40811
- if (this.debug) {
40812
- console.log('[TimeSalesWidget] Initial symbol validated:', {
40813
- symbol: this.symbol,
40814
- companyName: this.companyName,
40815
- exchangeName: this.exchangeName
40816
- });
40817
- }
40818
- }
40819
- } catch (error) {
40820
- if (this.debug) {
40821
- console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
40822
- }
40823
- // Don't throw - let the widget continue with WebSocket data
40824
- }
40825
- }
40826
40828
  subscribeToData() {
40827
40829
  // Subscribe to Time & Sales data (using queryonbbotimesale)
40828
40830
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
@@ -40838,6 +40840,9 @@ ${SharedStyles}
40838
40840
  clearTimeout(this.loadingTimeout);
40839
40841
  this.loadingTimeout = null;
40840
40842
  }
40843
+ if (message.type != 'queryonbbotimesale') {
40844
+ return;
40845
+ }
40841
40846
 
40842
40847
  // Handle error messages
40843
40848
  if (message.type === 'error') {
@@ -40850,30 +40855,16 @@ ${SharedStyles}
40850
40855
  }
40851
40856
 
40852
40857
  // Handle Time & Sales data - Array of trades
40853
- if (Array.isArray(message)) {
40858
+ if (Array.isArray(message.data)) {
40854
40859
  if (this.debug) {
40855
40860
  console.log('[TimeSalesWidget] Received trades array:', message);
40856
40861
  }
40857
40862
 
40858
40863
  // Add new trades to existing data
40859
- this.data.addTrades(message);
40864
+ this.data.addTrades(message.data);
40860
40865
  this.updateWidget();
40861
40866
  return;
40862
40867
  }
40863
-
40864
- // Handle wrapped format
40865
- if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
40866
- if (Array.isArray(message['0'])) {
40867
- if (this.debug) {
40868
- console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
40869
- }
40870
-
40871
- // Add new trades to existing data
40872
- this.data.addTrades(message['0']);
40873
- this.updateWidget();
40874
- return;
40875
- }
40876
- }
40877
40868
  if (this.debug) {
40878
40869
  console.log('[TimeSalesWidget] Unexpected message format:', message);
40879
40870
  }
@@ -41100,7 +41091,15 @@ ${SharedStyles}
41100
41091
  try {
41101
41092
  const response = await fetch(url, config);
41102
41093
  if (!response.ok) {
41103
- throw new Error(`HTTP error! status: ${response.status}`);
41094
+ // Try to get response body for error details
41095
+ let errorBody = '';
41096
+ try {
41097
+ errorBody = await response.text();
41098
+ console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
41099
+ } catch (e) {
41100
+ console.error(`[ApiService] Could not read error response body:`, e);
41101
+ }
41102
+ throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
41104
41103
  }
41105
41104
  return await response.json();
41106
41105
  } catch (error) {
@@ -41589,6 +41588,7 @@ ${SharedStyles}
41589
41588
  }
41590
41589
  }
41591
41590
  handleMessage(event) {
41591
+ //console.log('EVENT', event)
41592
41592
  try {
41593
41593
  // Update activity tracking - connection is alive!
41594
41594
  this.lastMessageReceived = Date.now();
@@ -41613,22 +41613,20 @@ ${SharedStyles}
41613
41613
  return;
41614
41614
  }
41615
41615
 
41616
- // Determine target type based on error content
41617
- const targetType = this._getTargetTypeFromErrorMessage(textMessage);
41616
+ // Determine type based on error content, default to 'error'
41617
+ const errorType = this._getTargetTypeFromErrorMessage(textMessage) || 'error';
41618
41618
  if (textMessage.toLowerCase().includes('no night session') || textMessage.toLowerCase().includes('no data')) {
41619
41619
  message = {
41620
- type: 'error',
41620
+ type: errorType,
41621
41621
  message: textMessage,
41622
- error: textMessage,
41623
- noData: true,
41624
- targetType: targetType // Will be 'querynightsession' for night session errors
41622
+ error: true,
41623
+ noData: true
41625
41624
  };
41626
41625
  } else {
41627
41626
  message = {
41628
- type: 'error',
41627
+ type: errorType,
41629
41628
  message: textMessage,
41630
- error: textMessage,
41631
- targetType: targetType // Could be null for general errors
41629
+ error: true
41632
41630
  };
41633
41631
  }
41634
41632
  }
@@ -41636,9 +41634,16 @@ ${SharedStyles}
41636
41634
  console.log('[WebSocketManager] Processed message:', message);
41637
41635
  }
41638
41636
 
41639
- // Handle new message format with type and data fields (case-insensitive)
41640
- const normalizedMessage = this._normalizeMessage(message);
41641
- this._routeMessage(normalizedMessage);
41637
+ // Extract data field for night session messages to avoid nested structure
41638
+ // Night session format: { type: 'queryonbbol1', error: false, data: [...] }
41639
+ // We want to send just the data array to widgets
41640
+ let dataToRoute = message;
41641
+ /* if (message.data !== undefined && message.type !== undefined) {
41642
+ // This is night session format - extract the data
41643
+ dataToRoute = message.data;
41644
+ } */
41645
+
41646
+ this._routeMessage(dataToRoute);
41642
41647
  } catch (error) {
41643
41648
  console.error('[WebSocketManager] Error handling message:', error);
41644
41649
  this._notifyWidgets('error', {
@@ -41777,7 +41782,31 @@ ${SharedStyles}
41777
41782
  }
41778
41783
  } else {
41779
41784
  if (this.config.debug) {
41780
- console.log(`[WebSocketManager] Subscription ${subscriptionKey} already active, skipping`);
41785
+ console.log(`[WebSocketManager] Subscription ${subscriptionKey} already active, skipping server subscription`);
41786
+ }
41787
+
41788
+ // Subscription already active, but send cached data to newly subscribing widgets
41789
+ const cachedMessage = this.lastMessageCache.get(subscriptionKey);
41790
+ console.log('LAST', this.lastMessageCache);
41791
+ if (cachedMessage) {
41792
+ if (this.config.debug) {
41793
+ console.log(`[WebSocketManager] Sending cached data to newly subscribing widgets for ${subscriptionKey}`);
41794
+ }
41795
+
41796
+ // Find all widgets subscribed to this type:symbol combination
41797
+ this.subscriptions.forEach((subscription, widgetId) => {
41798
+ if (subscription.types.has(type) && subscription.symbol === symbol) {
41799
+ try {
41800
+ subscription.callback({
41801
+ event: 'data',
41802
+ data: cachedMessage,
41803
+ widgetId
41804
+ });
41805
+ } catch (error) {
41806
+ console.error(`[WebSocketManager] Error sending cached data to widget ${widgetId}:`, error);
41807
+ }
41808
+ }
41809
+ });
41781
41810
  }
41782
41811
  }
41783
41812
  });
@@ -41829,14 +41858,9 @@ ${SharedStyles}
41829
41858
  * The data field structure varies based on the type
41830
41859
  */
41831
41860
  _normalizeMessage(message) {
41832
- // If message already has targetType (error messages), return as is
41833
- if (message.targetType) {
41834
- return message;
41835
- }
41836
-
41837
41861
  // Check for error field (case-insensitive)
41838
- const errorField = message.error || message.Error;
41839
- const typeField = message.type || message.Type;
41862
+ const errorField = message.error;
41863
+ const typeField = message.type;
41840
41864
  const messageField = message.message || message.Message;
41841
41865
 
41842
41866
  // Handle explicit error messages - route based on type field
@@ -41844,13 +41868,12 @@ ${SharedStyles}
41844
41868
  if (this.config.debug) {
41845
41869
  console.log(`[WebSocketManager] Detected error message with type: ${typeField}`);
41846
41870
  }
41847
-
41848
- // Set targetType for routing to specific widget types
41849
41871
  return {
41850
41872
  ...message,
41851
- targetType: typeField,
41852
- type: typeField,
41853
- isError: true
41873
+ // Only add type if message doesn't already have one
41874
+ ...(!message.type && !message.Type && typeField ? {
41875
+ type: typeField
41876
+ } : {})
41854
41877
  };
41855
41878
  }
41856
41879
 
@@ -41868,92 +41891,30 @@ ${SharedStyles}
41868
41891
  // Treat as error and route based on type
41869
41892
  return {
41870
41893
  ...message,
41871
- targetType: typeField,
41872
- type: typeField,
41873
- isError: true,
41874
- error: true // Mark as error even if it was false
41894
+ // Only add type if message doesn't already have one
41895
+ ...(!message.type && !message.Type && typeField ? {
41896
+ type: typeField
41897
+ } : {}),
41898
+ error: true // Mark as error since it was detected as implicit error
41875
41899
  };
41876
41900
  }
41877
41901
  }
41878
41902
 
41879
- // Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
41880
- const dataField = message.data || message.Data;
41881
- if (typeField && dataField !== undefined) {
41882
- if (this.config.debug) {
41883
- console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
41884
- }
41885
-
41886
- // Extract the actual data and add the type to it for routing
41887
- let normalizedData;
41888
- if (Array.isArray(dataField)) {
41889
- // If data is an array, process each item
41890
- normalizedData = dataField;
41891
- // Add type info to the structure for routing
41892
- if (normalizedData.length > 0) {
41893
- normalizedData._messageType = typeField;
41894
- }
41895
- } else if (typeof dataField === 'object' && dataField !== null) {
41896
- // If data is an object, use it directly
41897
- normalizedData = dataField;
41898
- // Add type info for routing
41899
- normalizedData._messageType = typeField;
41900
- } else {
41901
- // Primitive value, wrap it
41902
- normalizedData = {
41903
- value: dataField,
41904
- _messageType: typeField
41905
- };
41906
- }
41907
-
41908
- // Also preserve the original type at the root level for backward compatibility
41909
- if (typeof normalizedData === 'object' && !normalizedData.type) {
41910
- normalizedData.type = typeField;
41911
- }
41912
- return normalizedData;
41913
- }
41914
-
41915
41903
  // Old format or no type/data structure, return as is
41916
41904
  return message;
41917
41905
  }
41918
41906
  _routeMessage(message) {
41919
41907
  //console.log('message', message);
41920
41908
 
41921
- if (Array.isArray(message) && message.length === 0) {
41922
- if (this.config.debug) {
41923
- console.log('[WebSocketManager] Received empty array, ignoring');
41924
- }
41925
- return; // Don't route empty arrays
41926
- }
41909
+ // IMPORTANT: Don't filter out empty arrays here - route them based on message.type
41910
+ // Widgets need to receive empty arrays to clear loading states and show "no data" messages
41911
+ // The message structure is: { type: 'queryoptionchain', data: [] }
41912
+ // We need to route based on the 'type' field, not the data content
41927
41913
 
41928
41914
  // Cache the message for later use by new subscribers
41929
41915
  this._cacheMessage(message);
41930
41916
 
41931
- // Check if message has a specific target type (like night session errors)
41932
- if (message.targetType) {
41933
- const targetWidgets = this.typeSubscriptions.get(message.targetType);
41934
- if (targetWidgets && targetWidgets.size > 0) {
41935
- if (this.config.debug) {
41936
- console.log(`[WebSocketManager] Routing ${message.targetType} error to specific widgets:`, [...targetWidgets]);
41937
- }
41938
- targetWidgets.forEach(widgetId => {
41939
- const subscription = this.subscriptions.get(widgetId);
41940
- if (subscription) {
41941
- try {
41942
- subscription.callback({
41943
- event: 'data',
41944
- data: message,
41945
- widgetId
41946
- });
41947
- } catch (error) {
41948
- console.error(`[WebSocketManager] Error in widget ${widgetId} callback:`, error);
41949
- }
41950
- }
41951
- });
41952
- return; // Don't fall through to broadcast
41953
- }
41954
- }
41955
-
41956
- //console.log('here');
41917
+ // Get relevant widgets based on message type and symbol
41957
41918
  const relevantWidgets = this._getRelevantWidgets(message);
41958
41919
  if (this.config.debug && relevantWidgets.size > 0) {
41959
41920
  console.log(`[WebSocketManager] Routing message to ${relevantWidgets.size} relevant widgets`);
@@ -41996,42 +41957,49 @@ ${SharedStyles}
41996
41957
  _cacheMessage(message) {
41997
41958
  try {
41998
41959
  // Extract symbol and message type to create cache key
41999
- const symbol = this._extractSymbol(message);
42000
- const messageType = this._extractMessageType(message);
41960
+ // This allows new widgets to get instant data when subscribing
41961
+ // check in message.data or message.Data
41962
+ let data;
41963
+ if (message.data) {
41964
+ data = message.data;
41965
+ } else if (message.Data) {
41966
+ data = message.Data;
41967
+ } else {
41968
+ data = message;
41969
+ }
41970
+ const symbol = this._extractSymbol(data);
41971
+ const messageType = message.type;
41972
+ if (this.config.debug) {
41973
+ console.log(`[WebSocketManager] _cacheMessage - extracted symbol: "${symbol}", messageType: "${messageType}"`);
41974
+ }
41975
+
41976
+ // Cache by type:symbol combination
42001
41977
  if (messageType && symbol) {
42002
41978
  const cacheKey = `${messageType}:${symbol}`;
42003
41979
  this.lastMessageCache.set(cacheKey, message);
42004
41980
  if (this.config.debug) {
42005
41981
  console.log(`[WebSocketManager] Cached message for ${cacheKey}`);
42006
41982
  }
42007
- }
42008
-
42009
- // Also handle array messages (for night session / blueocean data)
42010
- if (Array.isArray(message) && message.length > 0 && message[0].Symbol) {
42011
- const firstItem = message[0];
42012
- const symbol = firstItem.Symbol;
42013
- const messageType = this._extractMessageType(message);
42014
- if (messageType && symbol) {
42015
- const cacheKey = `${messageType}:${symbol}`;
42016
- this.lastMessageCache.set(cacheKey, message);
41983
+ } else {
41984
+ // If we can't extract symbol from message, try to cache by type only
41985
+ // This helps with data like ONBBO L2 which doesn't have symbol in the message
41986
+ if (messageType) {
42017
41987
  if (this.config.debug) {
42018
- console.log(`[WebSocketManager] Cached array message for ${cacheKey}`);
41988
+ console.log(`[WebSocketManager] No symbol found in message, attempting to infer from subscriptions for type: ${messageType}`);
42019
41989
  }
42020
- }
42021
- }
42022
41990
 
42023
- // Handle Data array format (for queryl1 data)
42024
- if (message.Data && Array.isArray(message.Data)) {
42025
- message.Data.forEach(dataItem => {
42026
- if (dataItem.Symbol) {
42027
- const cacheKey = `queryl1:${dataItem.Symbol}`;
42028
- // Cache the whole message, not just the item
42029
- this.lastMessageCache.set(cacheKey, message);
42030
- if (this.config.debug) {
42031
- console.log(`[WebSocketManager] Cached data item for ${cacheKey}`);
41991
+ // Find active subscriptions for this message type and cache for each
41992
+ this.activeSubscriptions.forEach(activeKey => {
41993
+ const [type, sym] = activeKey.split(':');
41994
+ if (type === messageType && sym) {
41995
+ const cacheKey = activeKey;
41996
+ this.lastMessageCache.set(cacheKey, message);
41997
+ if (this.config.debug) {
41998
+ console.log(`[WebSocketManager] Cached message for ${cacheKey} (inferred from active subscription)`);
41999
+ }
42032
42000
  }
42033
- }
42034
- });
42001
+ });
42002
+ }
42035
42003
  }
42036
42004
  } catch (error) {
42037
42005
  console.error('[WebSocketManager] Error caching message:', error);
@@ -42040,22 +42008,28 @@ ${SharedStyles}
42040
42008
  _getRelevantWidgets(message) {
42041
42009
  const relevantWidgets = new Set();
42042
42010
 
42011
+ // OPTIMIZATION: Extract message type once from the entire message, not from each data item
42012
+ // Message structure: { type: 'queryoptionchain', data: [] }
42013
+ let dataField = message.Data || message.data;
42014
+ const messageType = message.type || this._extractMessageType(dataField);
42015
+
42043
42016
  // Handle array messages
42044
- this._addRelevantWidgetsForItem(message, relevantWidgets);
42017
+ this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
42045
42018
  return relevantWidgets;
42046
42019
  }
42047
- _addRelevantWidgetsForItem(item, relevantWidgets) {
42020
+ _addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
42048
42021
  // If item has Data array, process it
42049
42022
  if (item.Data && Array.isArray(item.Data)) {
42050
42023
  //console.log('Data array found with length:', item.Data.length);
42051
42024
  item.Data.forEach(dataItem => {
42052
- this._processDataItem(dataItem, relevantWidgets);
42025
+ // Pass messageType from parent message, don't extract from each data item
42026
+ this._processDataItem(dataItem, relevantWidgets, messageType);
42053
42027
  });
42054
42028
  } else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
42055
42029
  this._processOptionChainData(item, relevantWidgets);
42056
42030
  } else {
42057
- // Process single item
42058
- this._processDataItem(item, relevantWidgets);
42031
+ // Process single item - messageType already extracted from message
42032
+ this._processDataItem(item, relevantWidgets, messageType);
42059
42033
  }
42060
42034
  }
42061
42035
 
@@ -42099,12 +42073,11 @@ ${SharedStyles}
42099
42073
  }
42100
42074
  }
42101
42075
  }
42102
- _processDataItem(dataItem, relevantWidgets) {
42076
+ _processDataItem(dataItem, relevantWidgets, messageType) {
42103
42077
  // Process individual data items and route to appropriate widgets
42104
42078
  // Option chain arrays are handled separately by _processOptionChainData
42105
42079
 
42106
42080
  const symbol = this._extractSymbol(dataItem);
42107
- const messageType = this._extractMessageType(dataItem);
42108
42081
  if (this.config.debug) {
42109
42082
  console.log('[WebSocketManager] Processing data item:', {
42110
42083
  symbol,
@@ -42198,7 +42171,7 @@ ${SharedStyles}
42198
42171
  _extractSymbol(item) {
42199
42172
  // Handle new format where symbol might be in nested data structure
42200
42173
  // Check if this is an array with data that has _messageType (normalized new format)
42201
- if (Array.isArray(item) && item._messageType) {
42174
+ if (Array.isArray(item)) {
42202
42175
  // The data is an array, check first item for symbol
42203
42176
  if (item.length > 0 && item[0]) {
42204
42177
  return this._extractSymbolFromItem(item[0]);
@@ -42264,25 +42237,15 @@ ${SharedStyles}
42264
42237
  return symbolMappings[extracted] || extracted;
42265
42238
  }
42266
42239
  _extractMessageType(item) {
42267
- // Check for _messageType field added by normalization (from new format)
42268
- if (item._messageType) {
42269
- return item._messageType;
42270
- }
42271
-
42272
- // Determine message type based on content
42273
- if (item.type) return item.type;
42274
-
42275
- // Infer type from data structure
42240
+ // FALLBACK: Infer type from data structure (for legacy/malformed messages)
42276
42241
 
42277
42242
  // IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
42278
42243
  // Level 2 ONBBO data - array of MMID objects or object with MMID field
42279
42244
  if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
42280
42245
  return 'queryonbbol2';
42281
42246
  }
42282
-
42283
- // Check for single MMID object
42284
- if (item.MMID !== undefined) {
42285
- return 'queryonbbol2';
42247
+ if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
42248
+ return 'queryoptionchain';
42286
42249
  }
42287
42250
 
42288
42251
  // Check for Source field AFTER MMID check