mdas-jsview-sdk 1.0.21-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
@@ -2510,28 +2589,6 @@
2510
2589
  font-size: 1.71em;
2511
2590
  font-weight: 700;
2512
2591
  color: #111827;
2513
- cursor: pointer;
2514
- transition: all 0.2s ease;
2515
- position: relative;
2516
- }
2517
-
2518
- .time-sales-widget .symbol.editable-symbol:hover {
2519
- opacity: 0.8;
2520
- transform: scale(1.02);
2521
- }
2522
-
2523
- .time-sales-widget .symbol.editable-symbol:hover::after {
2524
- content: "✎";
2525
- position: absolute;
2526
- top: -8px;
2527
- right: -8px;
2528
- background: #3b82f6;
2529
- color: white;
2530
- font-size: 10px;
2531
- padding: 2px 4px;
2532
- border-radius: 4px;
2533
- opacity: 0.8;
2534
- pointer-events: none;
2535
2592
  }
2536
2593
 
2537
2594
  /* ========================================
@@ -4483,99 +4540,46 @@
4483
4540
  this.showLoading();
4484
4541
 
4485
4542
  // Validate initial symbol via API to get company info
4486
- await this.validateInitialSymbol();
4487
- this.subscribeToData();
4488
- }
4489
- async validateInitialSymbol() {
4490
- try {
4491
- const apiService = this.wsManager.getApiService();
4492
- if (!apiService) {
4493
- if (this.debug) {
4494
- console.log('[MarketDataWidget] API service not available for initial validation');
4495
- }
4496
- return;
4497
- }
4498
- const result = await apiService.quotel1(this.symbol);
4499
- if (result && result.data && result.data[0]) {
4500
- const symbolData = result.data[0];
4501
- // Store for use in updateWidget
4502
- this.initialValidationData = symbolData;
4503
- if (this.debug) {
4504
- console.log('[MarketDataWidget] Initial symbol validated:', {
4505
- symbol: this.symbol,
4506
- companyName: symbolData.comp_name,
4507
- exchangeName: symbolData.market_name
4508
- });
4509
- }
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];
4510
4548
  }
4511
- } catch (error) {
4512
- if (this.debug) {
4513
- console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
4514
- }
4515
- // 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
4516
4554
  }
4555
+ this.subscribeToData();
4517
4556
  }
4518
4557
  subscribeToData() {
4519
4558
  // Subscribe with symbol for routing
4520
4559
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
4521
4560
  );
4522
-
4523
- // Send subscription message
4524
- /* this.wsManager.send({
4525
- type: 'queryl1',
4526
- symbol: this.symbol
4527
- }); */
4528
4561
  }
4562
+ handleData(message) {
4563
+ //console.log('DEBUG', message)
4529
4564
 
4530
- /* handleMessage(messageWrapper) {
4531
- if (this.isDestroyed) return;
4532
- try {
4533
- const { event, data } = messageWrapper;
4534
- if (this.debug) {
4535
- console.log('[MarketDataWidget] Received:', event, data);
4536
- }
4537
- if (event === 'connection') {
4538
- this.handleConnectionStatus(data);
4539
- return;
4540
- }
4541
- if (event === 'data') {
4542
- this.handleData(data);
4543
- }
4544
- if (event === 'session_revoked') {
4545
- if (data.status === 'attempting_relogin') {
4546
- this.showLoading();
4547
- } else if (data.status === 'relogin_failed') {
4548
- this.showError(data.error);
4549
- } else if (data.status === 'relogin_successful') {
4550
- this.hideLoading();
4551
- }
4552
- return;
4553
- }
4554
- } catch (error) {
4555
- console.error('[MarketDataWidget] Error handling message:', error);
4556
- 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);
4557
4569
  }
4558
- } */
4570
+ const errorMsg = message.message || message.Message;
4559
4571
 
4560
- /* handleConnectionStatus(status) {
4561
- if (status.status === 'connected') {
4562
- if (this.debug) {
4563
- console.log('[MarketDataWidget] Connected to WebSocket');
4564
- }
4565
- // Re-send subscription when reconnected
4566
- this.wsManager.send({
4567
- type: 'queryl1',
4568
- symbol: this.symbol
4569
- });
4570
- } else if (status.status === 'disconnected') {
4571
- this.showError('Disconnected from data service');
4572
- } else if (status.status === 'error') {
4573
- 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;
4574
4577
  }
4575
- } */
4576
-
4577
- handleData(message) {
4578
- console.log('DEBUG', message);
4578
+ } else if (message.Data && typeof message.Data === 'string') {
4579
+ this.hideLoading();
4580
+ this.showError(message.Message);
4581
+ return;
4582
+ }
4579
4583
  if (this.loadingTimeout) {
4580
4584
  clearTimeout(this.loadingTimeout);
4581
4585
  this.loadingTimeout = null;
@@ -4661,126 +4665,6 @@
4661
4665
  this.showConnectionQuality(); // Show cache indicator
4662
4666
  }
4663
4667
  }
4664
- showNoDataState() {
4665
- let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
4666
- if (this.isDestroyed) return;
4667
- try {
4668
- // Hide loading overlay
4669
- this.hideLoading();
4670
-
4671
- // Clear any existing errors
4672
- this.clearError();
4673
-
4674
- // Update header with dimmed styling and basic info from data
4675
- const symbol = data.Symbol || this.symbol;
4676
-
4677
- // Add null checks for all DOM elements
4678
- const symbolElement = this.container.querySelector('.symbol');
4679
- if (symbolElement) {
4680
- symbolElement.textContent = symbol;
4681
- }
4682
- const companyNameElement = this.container.querySelector('.company-name');
4683
- if (companyNameElement) {
4684
- companyNameElement.textContent = `${symbol} Inc`;
4685
- }
4686
-
4687
- // Set price to $0.00 and add dimmed class
4688
- const currentPriceElement = this.container.querySelector('.current-price');
4689
- if (currentPriceElement) {
4690
- currentPriceElement.textContent = '$0.00';
4691
- }
4692
- const changeElement = this.container.querySelector('.price-change');
4693
- if (changeElement) {
4694
- const changeValueElement = changeElement.querySelector('.change-value');
4695
- const changePercentElement = changeElement.querySelector('.change-percent');
4696
- if (changeValueElement) {
4697
- changeValueElement.textContent = '+0.00';
4698
- }
4699
- if (changePercentElement) {
4700
- changePercentElement.textContent = ' (0.00%)';
4701
- }
4702
- changeElement.classList.remove('positive', 'negative');
4703
- changeElement.classList.add('neutral');
4704
- }
4705
-
4706
- // Add dimmed styling to header
4707
- const widgetHeader = this.container.querySelector('.widget-header');
4708
- if (widgetHeader) {
4709
- widgetHeader.classList.add('dimmed');
4710
- }
4711
-
4712
- // Replace the data grid with no data message
4713
- this.showNoDataMessage(symbol, data);
4714
-
4715
- // Update footer with current timestamp
4716
- const lastUpdateElement = this.container.querySelector('.last-update');
4717
- if (lastUpdateElement) {
4718
- const timestamp = formatTimestampET();
4719
- lastUpdateElement.textContent = `Checked: ${timestamp}`;
4720
- }
4721
- const dataSourceElement = this.container.querySelector('.data-source');
4722
- if (dataSourceElement) {
4723
- dataSourceElement.textContent = 'Source: No data available';
4724
- }
4725
- } catch (error) {
4726
- console.error('Error showing no data state:', error);
4727
- this.showError('Error displaying no data state');
4728
- }
4729
- }
4730
- showNoDataMessage(symbol) {
4731
- const dataGrid = this.container.querySelector('.data-grid');
4732
-
4733
- // Hide the data grid if it exists
4734
- if (dataGrid) {
4735
- dataGrid.style.display = 'none';
4736
- }
4737
-
4738
- // Remove existing no data message if present
4739
- const existingNoData = this.container.querySelector('.no-data-state');
4740
- if (existingNoData) {
4741
- existingNoData.remove();
4742
- }
4743
-
4744
- // Create no data message element - safely without XSS risk
4745
- const noDataElement = createElement('div', '', 'no-data-state');
4746
- const noDataContent = createElement('div', '', 'no-data-content');
4747
-
4748
- // Create icon
4749
- const iconDiv = document.createElement('div');
4750
- iconDiv.className = 'no-data-icon';
4751
- iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4752
- <circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
4753
- <path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
4754
- <circle cx="12" cy="16" r="1" fill="#9ca3af"/>
4755
- </svg>`;
4756
- noDataContent.appendChild(iconDiv);
4757
-
4758
- // Create title
4759
- const title = createElement('h3', 'No Market Data', 'no-data-title');
4760
- noDataContent.appendChild(title);
4761
-
4762
- // Create description with sanitized symbol
4763
- const description = createElement('p', '', 'no-data-description');
4764
- description.appendChild(document.createTextNode('Market data for '));
4765
- const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
4766
- description.appendChild(symbolStrong);
4767
- description.appendChild(document.createTextNode(' was not found'));
4768
- noDataContent.appendChild(description);
4769
-
4770
- // Create guidance
4771
- const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
4772
- noDataContent.appendChild(guidance);
4773
- noDataElement.appendChild(noDataContent);
4774
-
4775
- // Insert before footer, with null check
4776
- const footer = this.container.querySelector('.widget-footer');
4777
- if (footer && footer.parentNode) {
4778
- footer.parentNode.insertBefore(noDataElement, footer);
4779
- } else {
4780
- // Fallback: append to container if footer not found
4781
- this.container.appendChild(noDataElement);
4782
- }
4783
- }
4784
4668
  updateWidget(data) {
4785
4669
  if (this.isDestroyed) return;
4786
4670
  try {
@@ -5285,7 +5169,20 @@
5285
5169
  this.showLoading();
5286
5170
 
5287
5171
  // Validate initial symbol via API to get company info
5288
- 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
+ }
5289
5186
 
5290
5187
  // Set timeout to detect no data on initial load
5291
5188
  this.loadingTimeout = setTimeout(() => {
@@ -5302,38 +5199,6 @@
5302
5199
 
5303
5200
  this.subscribeToData();
5304
5201
  }
5305
- async validateInitialSymbol() {
5306
- try {
5307
- const apiService = this.wsManager.getApiService();
5308
- if (!apiService) {
5309
- if (this.debug) {
5310
- console.log('[NightSessionWidget] API service not available for initial validation');
5311
- }
5312
- return;
5313
- }
5314
- const result = await apiService.quotel1(this.symbol);
5315
- if (result && result.data && result.data[0]) {
5316
- const symbolData = result.data[0];
5317
- // Extract company info
5318
- this.companyName = symbolData.comp_name || '';
5319
- this.exchangeName = symbolData.market_name || '';
5320
- this.mic = symbolData.mic || '';
5321
- if (this.debug) {
5322
- console.log('[NightSessionWidget] Initial symbol validated:', {
5323
- symbol: this.symbol,
5324
- companyName: this.companyName,
5325
- exchangeName: this.exchangeName,
5326
- mic: this.mic
5327
- });
5328
- }
5329
- }
5330
- } catch (error) {
5331
- if (this.debug) {
5332
- console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
5333
- }
5334
- // Don't throw - let the widget continue with WebSocket data
5335
- }
5336
- }
5337
5202
  subscribeToData() {
5338
5203
  let subscriptionType;
5339
5204
  if (this.source === 'bruce') {
@@ -5353,6 +5218,7 @@
5353
5218
  }
5354
5219
  handleData(message) {
5355
5220
  console.log('DEBUG NIGHT', message);
5221
+
5356
5222
  //message = message.data || message.Data
5357
5223
 
5358
5224
  if (this.loadingTimeout) {
@@ -5360,64 +5226,41 @@
5360
5226
  this.loadingTimeout = null;
5361
5227
  }
5362
5228
 
5363
- // Handle error messages from server (plain text converted to structured format)
5364
- if (message.type === 'error' || message.error == true) {
5229
+ // Check for error: false and display message if present
5230
+ if (message.error === true) {
5365
5231
  if (this.debug) {
5366
- console.log('[NightSessionWidget] Received no data message:', message.message);
5232
+ console.log('[Nigh] Received error response:', message.message);
5367
5233
  }
5368
- // Only show no data state if we don't have cached data
5369
- if (!this.data) {
5370
- this.showNoDataState({
5371
- Symbol: this.symbol,
5372
- NotFound: true,
5373
- message: message.message
5374
- });
5375
- } else {
5234
+ const errorMsg = message.message || message.Message;
5235
+
5236
+ // If there's a message field, show it as an error
5237
+ if (errorMsg) {
5376
5238
  this.hideLoading();
5377
- if (this.debug) {
5378
- console.log('[NightSessionWidget] No new data, keeping cached data visible');
5379
- }
5239
+ this.showError(errorMsg);
5240
+ return;
5380
5241
  }
5242
+ } else if (message.Data && typeof message.Data === 'string') {
5243
+ this.hideLoading();
5244
+ this.showError(message.Message);
5381
5245
  return;
5382
5246
  }
5383
5247
 
5384
- // Handle general error messages - CHECK IF IT'S ACCESS RELATED
5248
+ // Handle error messages from server (plain text converted to structured format)
5385
5249
  if (message.type === 'error') {
5386
- const errorMsg = message.message || 'Server error';
5387
-
5388
- // If it's an access/permission error, show as no-data state instead of separate error
5389
- if (errorMsg.toLowerCase().includes('access') || errorMsg.toLowerCase().includes('permission') || errorMsg.toLowerCase().includes('denied')) {
5390
- // Only show no data state if we don't have cached data
5391
- if (!this.data) {
5392
- this.showNoDataState({
5393
- Symbol: this.symbol,
5394
- NotFound: true,
5395
- isAccessError: true,
5396
- message: errorMsg
5397
- });
5398
- } else {
5399
- this.hideLoading();
5400
- if (this.debug) {
5401
- console.log('[NightSessionWidget] Access error but keeping cached data');
5402
- }
5403
- }
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);
5404
5256
  } else {
5405
- // Only show error if we don't have cached data
5406
- if (!this.data) {
5407
- console.log('errorMsg', errorMsg);
5408
- this.showError(errorMsg);
5409
- } else {
5410
- this.hideLoading();
5411
- if (this.debug) {
5412
- console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
5413
- }
5257
+ this.hideLoading();
5258
+ if (this.debug) {
5259
+ console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
5414
5260
  }
5415
5261
  }
5416
5262
  return;
5417
5263
  }
5418
-
5419
- // Filter for night session data
5420
-
5421
5264
  if (Array.isArray(message.data)) {
5422
5265
  // First, try to find data matching our symbol regardless of MarketName
5423
5266
  const symbolData = message.data.find(item => item.Symbol === this.symbol);
@@ -6318,36 +6161,20 @@
6318
6161
  this.showLoading();
6319
6162
 
6320
6163
  // Validate initial symbol via API to get symbol info
6321
- await this.validateInitialSymbol();
6322
- this.subscribeToData();
6323
- }
6324
- async validateInitialSymbol() {
6325
- try {
6326
- const apiService = this.wsManager.getApiService();
6327
- if (!apiService) {
6328
- if (this.debug) {
6329
- console.log('[OptionsWidget] API service not available for initial validation');
6330
- }
6331
- 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];
6332
6170
  }
6333
- const result = await apiService.quoteOptionl1(this.symbol);
6334
- if (result && result[0] && !result[0].error && !result[0].not_found) {
6335
- const symbolData = result[0];
6336
- // Store for use in updateWidget
6337
- this.initialValidationData = symbolData;
6338
- if (this.debug) {
6339
- console.log('[OptionsWidget] Initial symbol validated:', {
6340
- symbol: this.symbol,
6341
- underlying: symbolData.Underlying || symbolData.RootSymbol
6342
- });
6343
- }
6344
- }
6345
- } catch (error) {
6346
- if (this.debug) {
6347
- console.warn('[OptionsWidget] Initial symbol validation failed:', error);
6348
- }
6349
- // 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
6350
6176
  }
6177
+ this.subscribeToData();
6351
6178
  }
6352
6179
  subscribeToData() {
6353
6180
  // Subscribe with symbol for routing
@@ -38704,7 +38531,20 @@ ${SharedStyles}
38704
38531
  }
38705
38532
  async initialize() {
38706
38533
  // Validate initial symbol via API to get company info
38707
- 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
+ }
38708
38548
 
38709
38549
  // Update company name in the header
38710
38550
  this.updateCompanyName();
@@ -38712,38 +38552,6 @@ ${SharedStyles}
38712
38552
  // Load chart data
38713
38553
  await this.loadChartData();
38714
38554
  }
38715
- async validateInitialSymbol() {
38716
- try {
38717
- const apiService = this.wsManager.getApiService();
38718
- if (!apiService) {
38719
- if (this.debug) {
38720
- console.log('[IntradayChartWidget] API service not available for initial validation');
38721
- }
38722
- return;
38723
- }
38724
- const result = await apiService.quotel1(this.symbol);
38725
- if (result && result.data && result.data[0]) {
38726
- const symbolData = result.data[0];
38727
- // Extract company info
38728
- this.companyName = symbolData.comp_name || '';
38729
- this.exchangeName = symbolData.market_name || '';
38730
- this.mic = symbolData.mic || '';
38731
- if (this.debug) {
38732
- console.log('[IntradayChartWidget] Initial symbol validated:', {
38733
- symbol: this.symbol,
38734
- companyName: this.companyName,
38735
- exchangeName: this.exchangeName,
38736
- mic: this.mic
38737
- });
38738
- }
38739
- }
38740
- } catch (error) {
38741
- if (this.debug) {
38742
- console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
38743
- }
38744
- // Don't throw - let the widget continue with chart data
38745
- }
38746
- }
38747
38555
  updateCompanyName() {
38748
38556
  const companyNameElement = this.container.querySelector('.intraday-company-name');
38749
38557
  if (companyNameElement) {
@@ -39167,19 +38975,12 @@ ${SharedStyles}
39167
38975
  // Handle array format (standard night session format)
39168
38976
  if (Array.isArray(message.data)) {
39169
38977
  console.log('[IntradayChartWidget] Processing array format, length:', message.length);
39170
- const symbolData = message.find(item => item.Symbol === this.symbol);
38978
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
39171
38979
  console.log('[IntradayChartWidget] Found symbol data:', symbolData);
39172
38980
  if (symbolData && !symbolData.NotFound) {
39173
38981
  priceData = symbolData;
39174
38982
  }
39175
38983
  }
39176
- // Handle wrapped format
39177
- else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
39178
- console.log('[IntradayChartWidget] Processing wrapped format');
39179
- if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
39180
- priceData = message['0'];
39181
- }
39182
- }
39183
38984
  // Handle direct data format
39184
38985
  else if (message.Symbol === this.symbol && !message.NotFound) {
39185
38986
  console.log('[IntradayChartWidget] Processing direct format');
@@ -40282,7 +40083,19 @@ ${SharedStyles}
40282
40083
  this.showLoading();
40283
40084
 
40284
40085
  // Fetch company info for initial symbol
40285
- 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
+ }
40286
40099
 
40287
40100
  // Set timeout to detect no data on initial load
40288
40101
  this.loadingTimeout = setTimeout(() => {
@@ -40294,36 +40107,6 @@ ${SharedStyles}
40294
40107
  }, 10000);
40295
40108
  this.subscribeToData();
40296
40109
  }
40297
- async validateInitialSymbol() {
40298
- try {
40299
- const apiService = this.wsManager.getApiService();
40300
- if (!apiService) {
40301
- if (this.debug) {
40302
- console.log('[ONBBO L2] API service not available for initial validation');
40303
- }
40304
- return;
40305
- }
40306
- const result = await apiService.quotel1(this.symbol);
40307
- if (result && result.data && result.data[0]) {
40308
- const symbolData = result.data[0];
40309
- // Extract company info
40310
- this.companyName = symbolData.comp_name || '';
40311
- this.exchangeName = symbolData.market_name || '';
40312
- if (this.debug) {
40313
- console.log('[ONBBO L2] Initial symbol validated:', {
40314
- symbol: this.symbol,
40315
- companyName: this.companyName,
40316
- exchangeName: this.exchangeName
40317
- });
40318
- }
40319
- }
40320
- } catch (error) {
40321
- if (this.debug) {
40322
- console.warn('[ONBBO L2] Initial symbol validation failed:', error);
40323
- }
40324
- // Don't throw - let the widget continue with WebSocket data
40325
- }
40326
- }
40327
40110
  subscribeToData() {
40328
40111
  // Subscribe to ONBBO Level 2 data (order book)
40329
40112
  // Use separate widget IDs to avoid "already active" duplicate detection
@@ -40842,6 +40625,7 @@ ${SharedStyles}
40842
40625
  this.styled = options.styled !== undefined ? options.styled : true;
40843
40626
  this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
40844
40627
  this.data = new TimeSalesModel([], this.maxTrades);
40628
+ this.data.symbol = this.symbol; // Set the symbol in the data model
40845
40629
  this.isDestroyed = false;
40846
40630
  this.unsubscribe = null;
40847
40631
  this.symbolEditor = null;
@@ -40853,6 +40637,7 @@ ${SharedStyles}
40853
40637
 
40854
40638
  // Create widget structure
40855
40639
  this.createWidgetStructure();
40640
+ this.initializeSymbolEditor();
40856
40641
 
40857
40642
  // Initialize the widget
40858
40643
  this.initialize();
@@ -40869,7 +40654,26 @@ ${SharedStyles}
40869
40654
  }
40870
40655
  }
40871
40656
  this.addStyles();
40872
- 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
+ }
40873
40677
  }
40874
40678
  addStyles() {
40875
40679
  // Inject styles from the styles file into document head
@@ -40892,6 +40696,8 @@ ${SharedStyles}
40892
40696
  // Keep editor open when clicking buttons
40893
40697
  symbolType: 'quotel1' // Use standard quote validation
40894
40698
  });
40699
+
40700
+ //console.log('SETUP COMPLETE')
40895
40701
  }
40896
40702
  async handleSymbolChange(newSymbol, oldSymbol) {
40897
40703
  let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
@@ -40994,7 +40800,19 @@ ${SharedStyles}
40994
40800
  this.showLoading();
40995
40801
 
40996
40802
  // Validate initial symbol via API to get company info
40997
- 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
+ }
40998
40816
 
40999
40817
  // Set timeout to detect no data on initial load
41000
40818
  this.loadingTimeout = setTimeout(() => {
@@ -41007,36 +40825,6 @@ ${SharedStyles}
41007
40825
 
41008
40826
  this.subscribeToData();
41009
40827
  }
41010
- async validateInitialSymbol() {
41011
- try {
41012
- const apiService = this.wsManager.getApiService();
41013
- if (!apiService) {
41014
- if (this.debug) {
41015
- console.log('[TimeSalesWidget] API service not available for initial validation');
41016
- }
41017
- return;
41018
- }
41019
- const result = await apiService.quotel1(this.symbol);
41020
- if (result && result.data && result.data[0]) {
41021
- const symbolData = result.data[0];
41022
- // Extract company info
41023
- this.companyName = symbolData.comp_name || '';
41024
- this.exchangeName = symbolData.market_name || '';
41025
- if (this.debug) {
41026
- console.log('[TimeSalesWidget] Initial symbol validated:', {
41027
- symbol: this.symbol,
41028
- companyName: this.companyName,
41029
- exchangeName: this.exchangeName
41030
- });
41031
- }
41032
- }
41033
- } catch (error) {
41034
- if (this.debug) {
41035
- console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
41036
- }
41037
- // Don't throw - let the widget continue with WebSocket data
41038
- }
41039
- }
41040
40828
  subscribeToData() {
41041
40829
  // Subscribe to Time & Sales data (using queryonbbotimesale)
41042
40830
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
@@ -41052,6 +40840,9 @@ ${SharedStyles}
41052
40840
  clearTimeout(this.loadingTimeout);
41053
40841
  this.loadingTimeout = null;
41054
40842
  }
40843
+ if (message.type != 'queryonbbotimesale') {
40844
+ return;
40845
+ }
41055
40846
 
41056
40847
  // Handle error messages
41057
40848
  if (message.type === 'error') {
@@ -41064,30 +40855,16 @@ ${SharedStyles}
41064
40855
  }
41065
40856
 
41066
40857
  // Handle Time & Sales data - Array of trades
41067
- if (Array.isArray(message)) {
40858
+ if (Array.isArray(message.data)) {
41068
40859
  if (this.debug) {
41069
40860
  console.log('[TimeSalesWidget] Received trades array:', message);
41070
40861
  }
41071
40862
 
41072
40863
  // Add new trades to existing data
41073
- this.data.addTrades(message);
40864
+ this.data.addTrades(message.data);
41074
40865
  this.updateWidget();
41075
40866
  return;
41076
40867
  }
41077
-
41078
- // Handle wrapped format
41079
- if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
41080
- if (Array.isArray(message['0'])) {
41081
- if (this.debug) {
41082
- console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
41083
- }
41084
-
41085
- // Add new trades to existing data
41086
- this.data.addTrades(message['0']);
41087
- this.updateWidget();
41088
- return;
41089
- }
41090
- }
41091
40868
  if (this.debug) {
41092
40869
  console.log('[TimeSalesWidget] Unexpected message format:', message);
41093
40870
  }
@@ -41314,7 +41091,15 @@ ${SharedStyles}
41314
41091
  try {
41315
41092
  const response = await fetch(url, config);
41316
41093
  if (!response.ok) {
41317
- 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}`);
41318
41103
  }
41319
41104
  return await response.json();
41320
41105
  } catch (error) {
@@ -42074,8 +41859,8 @@ ${SharedStyles}
42074
41859
  */
42075
41860
  _normalizeMessage(message) {
42076
41861
  // Check for error field (case-insensitive)
42077
- const errorField = message.error || message.Error;
42078
- const typeField = message.type || message.Type;
41862
+ const errorField = message.error;
41863
+ const typeField = message.type;
42079
41864
  const messageField = message.message || message.Message;
42080
41865
 
42081
41866
  // Handle explicit error messages - route based on type field
@@ -42115,54 +41900,16 @@ ${SharedStyles}
42115
41900
  }
42116
41901
  }
42117
41902
 
42118
- // Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
42119
- const dataField = message.data || message.Data;
42120
- if (typeField && dataField !== undefined) {
42121
- if (this.config.debug) {
42122
- console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
42123
- }
42124
-
42125
- // Extract the actual data and add the type to it for routing
42126
- let normalizedData;
42127
- if (Array.isArray(dataField)) {
42128
- // If data is an array, process each item
42129
- normalizedData = dataField;
42130
- // Add type info to the structure for routing
42131
- if (normalizedData.length > 0) {
42132
- normalizedData._messageType = typeField;
42133
- }
42134
- } else if (typeof dataField === 'object' && dataField !== null) {
42135
- // If data is an object, use it directly
42136
- normalizedData = dataField;
42137
- // Add type info for routing
42138
- normalizedData._messageType = typeField;
42139
- } else {
42140
- // Primitive value, wrap it
42141
- normalizedData = {
42142
- value: dataField,
42143
- _messageType: typeField
42144
- };
42145
- }
42146
-
42147
- // Only add type field if the normalized data doesn't already have one
42148
- if (typeof normalizedData === 'object' && !normalizedData.type && !normalizedData.Type) {
42149
- normalizedData.type = typeField;
42150
- }
42151
- return normalizedData;
42152
- }
42153
-
42154
41903
  // Old format or no type/data structure, return as is
42155
41904
  return message;
42156
41905
  }
42157
41906
  _routeMessage(message) {
42158
41907
  //console.log('message', message);
42159
41908
 
42160
- if (Array.isArray(message) && message.length === 0) {
42161
- if (this.config.debug) {
42162
- console.log('[WebSocketManager] Received empty array, ignoring');
42163
- }
42164
- return; // Don't route empty arrays
42165
- }
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
42166
41913
 
42167
41914
  // Cache the message for later use by new subscribers
42168
41915
  this._cacheMessage(message);
@@ -42261,22 +42008,28 @@ ${SharedStyles}
42261
42008
  _getRelevantWidgets(message) {
42262
42009
  const relevantWidgets = new Set();
42263
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
+
42264
42016
  // Handle array messages
42265
- this._addRelevantWidgetsForItem(message, relevantWidgets);
42017
+ this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
42266
42018
  return relevantWidgets;
42267
42019
  }
42268
- _addRelevantWidgetsForItem(item, relevantWidgets) {
42020
+ _addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
42269
42021
  // If item has Data array, process it
42270
42022
  if (item.Data && Array.isArray(item.Data)) {
42271
42023
  //console.log('Data array found with length:', item.Data.length);
42272
42024
  item.Data.forEach(dataItem => {
42273
- this._processDataItem(dataItem, relevantWidgets);
42025
+ // Pass messageType from parent message, don't extract from each data item
42026
+ this._processDataItem(dataItem, relevantWidgets, messageType);
42274
42027
  });
42275
42028
  } else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
42276
42029
  this._processOptionChainData(item, relevantWidgets);
42277
42030
  } else {
42278
- // Process single item
42279
- this._processDataItem(item, relevantWidgets);
42031
+ // Process single item - messageType already extracted from message
42032
+ this._processDataItem(item, relevantWidgets, messageType);
42280
42033
  }
42281
42034
  }
42282
42035
 
@@ -42320,12 +42073,11 @@ ${SharedStyles}
42320
42073
  }
42321
42074
  }
42322
42075
  }
42323
- _processDataItem(dataItem, relevantWidgets) {
42076
+ _processDataItem(dataItem, relevantWidgets, messageType) {
42324
42077
  // Process individual data items and route to appropriate widgets
42325
42078
  // Option chain arrays are handled separately by _processOptionChainData
42326
42079
 
42327
42080
  const symbol = this._extractSymbol(dataItem);
42328
- const messageType = this._extractMessageType(dataItem);
42329
42081
  if (this.config.debug) {
42330
42082
  console.log('[WebSocketManager] Processing data item:', {
42331
42083
  symbol,
@@ -42485,25 +42237,15 @@ ${SharedStyles}
42485
42237
  return symbolMappings[extracted] || extracted;
42486
42238
  }
42487
42239
  _extractMessageType(item) {
42488
- // Check for _messageType field added by normalization (from new format)
42489
- if (item._messageType) {
42490
- return item._messageType;
42491
- }
42492
-
42493
- // Determine message type based on content
42494
- if (item.type) return item.type;
42495
-
42496
- // Infer type from data structure
42240
+ // FALLBACK: Infer type from data structure (for legacy/malformed messages)
42497
42241
 
42498
42242
  // IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
42499
42243
  // Level 2 ONBBO data - array of MMID objects or object with MMID field
42500
42244
  if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
42501
42245
  return 'queryonbbol2';
42502
42246
  }
42503
-
42504
- // Check for single MMID object
42505
- if (item.MMID !== undefined) {
42506
- return 'queryonbbol2';
42247
+ if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
42248
+ return 'queryoptionchain';
42507
42249
  }
42508
42250
 
42509
42251
  // Check for Source field AFTER MMID check