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.
@@ -406,6 +406,85 @@ class BaseWidget {
406
406
  console.log(`[${this.constructor.name}] Connection failed after ${status.maxAttempts} attempts`);
407
407
  }
408
408
  }
409
+
410
+ /**
411
+ * Validate initial symbol with the API and handle access/permission errors
412
+ * This is a reusable method for all widgets that need to validate symbols on initialization
413
+ *
414
+ * @param {string} apiMethod - The API method name to call (e.g., 'quotel1', 'quoteOptionl1')
415
+ * @param {string} symbol - The symbol to validate
416
+ * @param {Function} onSuccess - Callback when validation succeeds with data, receives (data, result)
417
+ * @returns {boolean} - Returns false if access denied (stops initialization), true otherwise
418
+ */
419
+ async validateInitialSymbol(apiMethod, symbol) {
420
+ let onSuccess = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
421
+ try {
422
+ const apiService = this.wsManager?.getApiService();
423
+ if (!apiService) {
424
+ if (this.debug) {
425
+ console.log(`[${this.constructor.name}] API service not available for initial validation`);
426
+ }
427
+ return true; // Continue without validation
428
+ }
429
+ if (!apiService[apiMethod]) {
430
+ console.error(`[${this.constructor.name}] API method ${apiMethod} does not exist`);
431
+ return true; // Continue without validation
432
+ }
433
+ const result = await apiService[apiMethod](symbol);
434
+ if (result && result.data && result.data.length > 0) {
435
+ if (this.debug) {
436
+ console.log(`[${this.constructor.name}] Initial symbol validated:`, {
437
+ symbol,
438
+ method: apiMethod,
439
+ dataCount: result.data.length
440
+ });
441
+ }
442
+
443
+ // Call success callback if provided
444
+ if (onSuccess && typeof onSuccess === 'function') {
445
+ onSuccess(result.data, result);
446
+ }
447
+ return true; // Validation successful
448
+ }
449
+
450
+ // No data returned - might be access issue, but continue anyway
451
+ if (this.debug) {
452
+ console.log(`[${this.constructor.name}] No data returned from ${apiMethod} for ${symbol}`);
453
+ }
454
+ return true; // Continue anyway, WebSocket might still work
455
+ } catch (error) {
456
+ if (this.debug) {
457
+ console.warn(`[${this.constructor.name}] Initial symbol validation failed:`, error);
458
+ }
459
+
460
+ // Check if it's an access/permission error
461
+ const errorMessage = (error.message || '').toLowerCase();
462
+ const isAccessError = errorMessage.includes('400') ||
463
+ // Bad Request (often used for access issues)
464
+ errorMessage.includes('401') ||
465
+ // Unauthorized
466
+ errorMessage.includes('403') ||
467
+ // Forbidden
468
+ errorMessage.includes('forbidden') || errorMessage.includes('unauthorized') || errorMessage.includes('no access') || errorMessage.includes('no opra access') ||
469
+ // Specific OPRA access denial
470
+ errorMessage.includes('permission denied') || errorMessage.includes('access denied');
471
+ if (isAccessError) {
472
+ // Hide loading and show access error
473
+ this.hideLoading();
474
+
475
+ // Extract more specific error message if available
476
+ let userMessage = `Access denied: You don't have permission to view data for ${symbol}`;
477
+ if (errorMessage.includes('no opra access')) {
478
+ userMessage = `Access denied: You don't have OPRA access for option ${symbol}`;
479
+ }
480
+ this.showError(userMessage);
481
+ return false; // Stop initialization
482
+ }
483
+
484
+ // For other errors, continue with WebSocket (might still work)
485
+ return true;
486
+ }
487
+ }
409
488
  }
410
489
 
411
490
  // src/models/MarketDataModel.js
@@ -2504,28 +2583,6 @@ const TimeSalesStyles = `
2504
2583
  font-size: 1.71em;
2505
2584
  font-weight: 700;
2506
2585
  color: #111827;
2507
- cursor: pointer;
2508
- transition: all 0.2s ease;
2509
- position: relative;
2510
- }
2511
-
2512
- .time-sales-widget .symbol.editable-symbol:hover {
2513
- opacity: 0.8;
2514
- transform: scale(1.02);
2515
- }
2516
-
2517
- .time-sales-widget .symbol.editable-symbol:hover::after {
2518
- content: "✎";
2519
- position: absolute;
2520
- top: -8px;
2521
- right: -8px;
2522
- background: #3b82f6;
2523
- color: white;
2524
- font-size: 10px;
2525
- padding: 2px 4px;
2526
- border-radius: 4px;
2527
- opacity: 0.8;
2528
- pointer-events: none;
2529
2586
  }
2530
2587
 
2531
2588
  /* ========================================
@@ -4477,99 +4534,46 @@ class MarketDataWidget extends BaseWidget {
4477
4534
  this.showLoading();
4478
4535
 
4479
4536
  // Validate initial symbol via API to get company info
4480
- await this.validateInitialSymbol();
4481
- this.subscribeToData();
4482
- }
4483
- async validateInitialSymbol() {
4484
- try {
4485
- const apiService = this.wsManager.getApiService();
4486
- if (!apiService) {
4487
- if (this.debug) {
4488
- console.log('[MarketDataWidget] API service not available for initial validation');
4489
- }
4490
- return;
4491
- }
4492
- const result = await apiService.quotel1(this.symbol);
4493
- if (result && result.data && result.data[0]) {
4494
- const symbolData = result.data[0];
4495
- // Store for use in updateWidget
4496
- this.initialValidationData = symbolData;
4497
- if (this.debug) {
4498
- console.log('[MarketDataWidget] Initial symbol validated:', {
4499
- symbol: this.symbol,
4500
- companyName: symbolData.comp_name,
4501
- exchangeName: symbolData.market_name
4502
- });
4503
- }
4537
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
4538
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
4539
+ // Store for use in updateWidget
4540
+ if (data && data[0]) {
4541
+ this.initialValidationData = data[0];
4504
4542
  }
4505
- } catch (error) {
4506
- if (this.debug) {
4507
- console.warn('[MarketDataWidget] Initial symbol validation failed:', error);
4508
- }
4509
- // Don't throw - let the widget continue with WebSocket data
4543
+ });
4544
+
4545
+ // If validation failed due to access/permission issues, stop initialization
4546
+ if (validationSuccess === false) {
4547
+ return; // Error is already shown, don't continue
4510
4548
  }
4549
+ this.subscribeToData();
4511
4550
  }
4512
4551
  subscribeToData() {
4513
4552
  // Subscribe with symbol for routing
4514
4553
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryl1'], this.handleMessage.bind(this), this.symbol // Pass symbol for routing
4515
4554
  );
4516
-
4517
- // Send subscription message
4518
- /* this.wsManager.send({
4519
- type: 'queryl1',
4520
- symbol: this.symbol
4521
- }); */
4522
4555
  }
4556
+ handleData(message) {
4557
+ //console.log('DEBUG', message)
4523
4558
 
4524
- /* handleMessage(messageWrapper) {
4525
- if (this.isDestroyed) return;
4526
- try {
4527
- const { event, data } = messageWrapper;
4528
- if (this.debug) {
4529
- console.log('[MarketDataWidget] Received:', event, data);
4530
- }
4531
- if (event === 'connection') {
4532
- this.handleConnectionStatus(data);
4533
- return;
4534
- }
4535
- if (event === 'data') {
4536
- this.handleData(data);
4537
- }
4538
- if (event === 'session_revoked') {
4539
- if (data.status === 'attempting_relogin') {
4540
- this.showLoading();
4541
- } else if (data.status === 'relogin_failed') {
4542
- this.showError(data.error);
4543
- } else if (data.status === 'relogin_successful') {
4544
- this.hideLoading();
4545
- }
4546
- return;
4547
- }
4548
- } catch (error) {
4549
- console.error('[MarketDataWidget] Error handling message:', error);
4550
- this.showError('Error processing data');
4559
+ // Check for error: false and display message if present
4560
+ if (message.error === true) {
4561
+ if (this.debug) {
4562
+ console.log('[MarketDataWidget] Received error response:', message.message);
4551
4563
  }
4552
- } */
4564
+ const errorMsg = message.message || message.Message;
4553
4565
 
4554
- /* handleConnectionStatus(status) {
4555
- if (status.status === 'connected') {
4556
- if (this.debug) {
4557
- console.log('[MarketDataWidget] Connected to WebSocket');
4558
- }
4559
- // Re-send subscription when reconnected
4560
- this.wsManager.send({
4561
- type: 'queryl1',
4562
- symbol: this.symbol
4563
- });
4564
- } else if (status.status === 'disconnected') {
4565
- this.showError('Disconnected from data service');
4566
- } else if (status.status === 'error') {
4567
- this.showError(status.error || 'Connection error');
4566
+ // If there's a message field, show it as an error
4567
+ if (errorMsg) {
4568
+ this.hideLoading();
4569
+ this.showError(errorMsg);
4570
+ return;
4568
4571
  }
4569
- } */
4570
-
4571
- handleData(message) {
4572
- console.log('DEBUG', message);
4572
+ } else if (message.Data && typeof message.Data === 'string') {
4573
+ this.hideLoading();
4574
+ this.showError(message.Message);
4575
+ return;
4576
+ }
4573
4577
  if (this.loadingTimeout) {
4574
4578
  clearTimeout(this.loadingTimeout);
4575
4579
  this.loadingTimeout = null;
@@ -4655,126 +4659,6 @@ class MarketDataWidget extends BaseWidget {
4655
4659
  this.showConnectionQuality(); // Show cache indicator
4656
4660
  }
4657
4661
  }
4658
- showNoDataState() {
4659
- let data = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
4660
- if (this.isDestroyed) return;
4661
- try {
4662
- // Hide loading overlay
4663
- this.hideLoading();
4664
-
4665
- // Clear any existing errors
4666
- this.clearError();
4667
-
4668
- // Update header with dimmed styling and basic info from data
4669
- const symbol = data.Symbol || this.symbol;
4670
-
4671
- // Add null checks for all DOM elements
4672
- const symbolElement = this.container.querySelector('.symbol');
4673
- if (symbolElement) {
4674
- symbolElement.textContent = symbol;
4675
- }
4676
- const companyNameElement = this.container.querySelector('.company-name');
4677
- if (companyNameElement) {
4678
- companyNameElement.textContent = `${symbol} Inc`;
4679
- }
4680
-
4681
- // Set price to $0.00 and add dimmed class
4682
- const currentPriceElement = this.container.querySelector('.current-price');
4683
- if (currentPriceElement) {
4684
- currentPriceElement.textContent = '$0.00';
4685
- }
4686
- const changeElement = this.container.querySelector('.price-change');
4687
- if (changeElement) {
4688
- const changeValueElement = changeElement.querySelector('.change-value');
4689
- const changePercentElement = changeElement.querySelector('.change-percent');
4690
- if (changeValueElement) {
4691
- changeValueElement.textContent = '+0.00';
4692
- }
4693
- if (changePercentElement) {
4694
- changePercentElement.textContent = ' (0.00%)';
4695
- }
4696
- changeElement.classList.remove('positive', 'negative');
4697
- changeElement.classList.add('neutral');
4698
- }
4699
-
4700
- // Add dimmed styling to header
4701
- const widgetHeader = this.container.querySelector('.widget-header');
4702
- if (widgetHeader) {
4703
- widgetHeader.classList.add('dimmed');
4704
- }
4705
-
4706
- // Replace the data grid with no data message
4707
- this.showNoDataMessage(symbol, data);
4708
-
4709
- // Update footer with current timestamp
4710
- const lastUpdateElement = this.container.querySelector('.last-update');
4711
- if (lastUpdateElement) {
4712
- const timestamp = formatTimestampET();
4713
- lastUpdateElement.textContent = `Checked: ${timestamp}`;
4714
- }
4715
- const dataSourceElement = this.container.querySelector('.data-source');
4716
- if (dataSourceElement) {
4717
- dataSourceElement.textContent = 'Source: No data available';
4718
- }
4719
- } catch (error) {
4720
- console.error('Error showing no data state:', error);
4721
- this.showError('Error displaying no data state');
4722
- }
4723
- }
4724
- showNoDataMessage(symbol) {
4725
- const dataGrid = this.container.querySelector('.data-grid');
4726
-
4727
- // Hide the data grid if it exists
4728
- if (dataGrid) {
4729
- dataGrid.style.display = 'none';
4730
- }
4731
-
4732
- // Remove existing no data message if present
4733
- const existingNoData = this.container.querySelector('.no-data-state');
4734
- if (existingNoData) {
4735
- existingNoData.remove();
4736
- }
4737
-
4738
- // Create no data message element - safely without XSS risk
4739
- const noDataElement = createElement('div', '', 'no-data-state');
4740
- const noDataContent = createElement('div', '', 'no-data-content');
4741
-
4742
- // Create icon
4743
- const iconDiv = document.createElement('div');
4744
- iconDiv.className = 'no-data-icon';
4745
- iconDiv.innerHTML = `<svg width="32" height="32" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
4746
- <circle cx="12" cy="12" r="10" stroke="#9ca3af" stroke-width="2"/>
4747
- <path d="M12 8v4" stroke="#9ca3af" stroke-width="2" stroke-linecap="round"/>
4748
- <circle cx="12" cy="16" r="1" fill="#9ca3af"/>
4749
- </svg>`;
4750
- noDataContent.appendChild(iconDiv);
4751
-
4752
- // Create title
4753
- const title = createElement('h3', 'No Market Data', 'no-data-title');
4754
- noDataContent.appendChild(title);
4755
-
4756
- // Create description with sanitized symbol
4757
- const description = createElement('p', '', 'no-data-description');
4758
- description.appendChild(document.createTextNode('Market data for '));
4759
- const symbolStrong = createElement('strong', sanitizeSymbol(symbol));
4760
- description.appendChild(symbolStrong);
4761
- description.appendChild(document.createTextNode(' was not found'));
4762
- noDataContent.appendChild(description);
4763
-
4764
- // Create guidance
4765
- const guidance = createElement('p', 'Please check the symbol spelling or try a different symbol', 'no-data-guidance');
4766
- noDataContent.appendChild(guidance);
4767
- noDataElement.appendChild(noDataContent);
4768
-
4769
- // Insert before footer, with null check
4770
- const footer = this.container.querySelector('.widget-footer');
4771
- if (footer && footer.parentNode) {
4772
- footer.parentNode.insertBefore(noDataElement, footer);
4773
- } else {
4774
- // Fallback: append to container if footer not found
4775
- this.container.appendChild(noDataElement);
4776
- }
4777
- }
4778
4662
  updateWidget(data) {
4779
4663
  if (this.isDestroyed) return;
4780
4664
  try {
@@ -5279,7 +5163,20 @@ class NightSessionWidget extends BaseWidget {
5279
5163
  this.showLoading();
5280
5164
 
5281
5165
  // Validate initial symbol via API to get company info
5282
- await this.validateInitialSymbol();
5166
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
5167
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
5168
+ // Extract company info from first data item
5169
+ if (data && data[0]) {
5170
+ this.companyName = data[0].comp_name || '';
5171
+ this.exchangeName = data[0].market_name || '';
5172
+ this.mic = data[0].mic || '';
5173
+ }
5174
+ });
5175
+
5176
+ // If validation failed due to access/permission issues, stop initialization
5177
+ if (validationSuccess === false) {
5178
+ return; // Error is already shown, don't continue
5179
+ }
5283
5180
 
5284
5181
  // Set timeout to detect no data on initial load
5285
5182
  this.loadingTimeout = setTimeout(() => {
@@ -5296,38 +5193,6 @@ class NightSessionWidget extends BaseWidget {
5296
5193
 
5297
5194
  this.subscribeToData();
5298
5195
  }
5299
- async validateInitialSymbol() {
5300
- try {
5301
- const apiService = this.wsManager.getApiService();
5302
- if (!apiService) {
5303
- if (this.debug) {
5304
- console.log('[NightSessionWidget] API service not available for initial validation');
5305
- }
5306
- return;
5307
- }
5308
- const result = await apiService.quotel1(this.symbol);
5309
- if (result && result.data && result.data[0]) {
5310
- const symbolData = result.data[0];
5311
- // Extract company info
5312
- this.companyName = symbolData.comp_name || '';
5313
- this.exchangeName = symbolData.market_name || '';
5314
- this.mic = symbolData.mic || '';
5315
- if (this.debug) {
5316
- console.log('[NightSessionWidget] Initial symbol validated:', {
5317
- symbol: this.symbol,
5318
- companyName: this.companyName,
5319
- exchangeName: this.exchangeName,
5320
- mic: this.mic
5321
- });
5322
- }
5323
- }
5324
- } catch (error) {
5325
- if (this.debug) {
5326
- console.warn('[NightSessionWidget] Initial symbol validation failed:', error);
5327
- }
5328
- // Don't throw - let the widget continue with WebSocket data
5329
- }
5330
- }
5331
5196
  subscribeToData() {
5332
5197
  let subscriptionType;
5333
5198
  if (this.source === 'bruce') {
@@ -5347,6 +5212,7 @@ class NightSessionWidget extends BaseWidget {
5347
5212
  }
5348
5213
  handleData(message) {
5349
5214
  console.log('DEBUG NIGHT', message);
5215
+
5350
5216
  //message = message.data || message.Data
5351
5217
 
5352
5218
  if (this.loadingTimeout) {
@@ -5354,64 +5220,41 @@ class NightSessionWidget extends BaseWidget {
5354
5220
  this.loadingTimeout = null;
5355
5221
  }
5356
5222
 
5357
- // Handle error messages from server (plain text converted to structured format)
5358
- if (message.type === 'error' || message.error == true) {
5223
+ // Check for error: false and display message if present
5224
+ if (message.error === true) {
5359
5225
  if (this.debug) {
5360
- console.log('[NightSessionWidget] Received no data message:', message.message);
5226
+ console.log('[Nigh] Received error response:', message.message);
5361
5227
  }
5362
- // Only show no data state if we don't have cached data
5363
- if (!this.data) {
5364
- this.showNoDataState({
5365
- Symbol: this.symbol,
5366
- NotFound: true,
5367
- message: message.message
5368
- });
5369
- } else {
5228
+ const errorMsg = message.message || message.Message;
5229
+
5230
+ // If there's a message field, show it as an error
5231
+ if (errorMsg) {
5370
5232
  this.hideLoading();
5371
- if (this.debug) {
5372
- console.log('[NightSessionWidget] No new data, keeping cached data visible');
5373
- }
5233
+ this.showError(errorMsg);
5234
+ return;
5374
5235
  }
5236
+ } else if (message.Data && typeof message.Data === 'string') {
5237
+ this.hideLoading();
5238
+ this.showError(message.Message);
5375
5239
  return;
5376
5240
  }
5377
5241
 
5378
- // Handle general error messages - CHECK IF IT'S ACCESS RELATED
5242
+ // Handle error messages from server (plain text converted to structured format)
5379
5243
  if (message.type === 'error') {
5380
- const errorMsg = message.message || 'Server error';
5381
-
5382
- // If it's an access/permission error, show as no-data state instead of separate error
5383
- if (errorMsg.toLowerCase().includes('access') || errorMsg.toLowerCase().includes('permission') || errorMsg.toLowerCase().includes('denied')) {
5384
- // Only show no data state if we don't have cached data
5385
- if (!this.data) {
5386
- this.showNoDataState({
5387
- Symbol: this.symbol,
5388
- NotFound: true,
5389
- isAccessError: true,
5390
- message: errorMsg
5391
- });
5392
- } else {
5393
- this.hideLoading();
5394
- if (this.debug) {
5395
- console.log('[NightSessionWidget] Access error but keeping cached data');
5396
- }
5397
- }
5244
+ if (this.debug) {
5245
+ console.log('[NightSessionWidget] Received no data message:', message.message);
5246
+ }
5247
+ // Only show no data state if we don't have cached data
5248
+ if (!this.data) {
5249
+ this.showError(errorMsg);
5398
5250
  } else {
5399
- // Only show error if we don't have cached data
5400
- if (!this.data) {
5401
- console.log('errorMsg', errorMsg);
5402
- this.showError(errorMsg);
5403
- } else {
5404
- this.hideLoading();
5405
- if (this.debug) {
5406
- console.log('[NightSessionWidget] Error received but keeping cached data:', errorMsg);
5407
- }
5251
+ this.hideLoading();
5252
+ if (this.debug) {
5253
+ console.log('[MarketDataWidget] Error received but keeping cached data:', errorMsg);
5408
5254
  }
5409
5255
  }
5410
5256
  return;
5411
5257
  }
5412
-
5413
- // Filter for night session data
5414
-
5415
5258
  if (Array.isArray(message.data)) {
5416
5259
  // First, try to find data matching our symbol regardless of MarketName
5417
5260
  const symbolData = message.data.find(item => item.Symbol === this.symbol);
@@ -6312,36 +6155,20 @@ class OptionsWidget extends BaseWidget {
6312
6155
  this.showLoading();
6313
6156
 
6314
6157
  // Validate initial symbol via API to get symbol info
6315
- await this.validateInitialSymbol();
6316
- this.subscribeToData();
6317
- }
6318
- async validateInitialSymbol() {
6319
- try {
6320
- const apiService = this.wsManager.getApiService();
6321
- if (!apiService) {
6322
- if (this.debug) {
6323
- console.log('[OptionsWidget] API service not available for initial validation');
6324
- }
6325
- return;
6158
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting data
6159
+ const validationSuccess = await super.validateInitialSymbol('quoteOptionl1', this.symbol, data => {
6160
+ // Store for use in updateWidget
6161
+ // Note: quoteOptionl1 returns array directly, not wrapped in data field
6162
+ if (data && data[0] && !data[0].error && !data[0].not_found) {
6163
+ this.initialValidationData = data[0];
6326
6164
  }
6327
- const result = await apiService.quoteOptionl1(this.symbol);
6328
- if (result && result[0] && !result[0].error && !result[0].not_found) {
6329
- const symbolData = result[0];
6330
- // Store for use in updateWidget
6331
- this.initialValidationData = symbolData;
6332
- if (this.debug) {
6333
- console.log('[OptionsWidget] Initial symbol validated:', {
6334
- symbol: this.symbol,
6335
- underlying: symbolData.Underlying || symbolData.RootSymbol
6336
- });
6337
- }
6338
- }
6339
- } catch (error) {
6340
- if (this.debug) {
6341
- console.warn('[OptionsWidget] Initial symbol validation failed:', error);
6342
- }
6343
- // Don't throw - let the widget continue with WebSocket data
6165
+ });
6166
+
6167
+ // If validation failed due to access/permission issues, stop initialization
6168
+ if (validationSuccess === false) {
6169
+ return; // Error is already shown, don't continue
6344
6170
  }
6171
+ this.subscribeToData();
6345
6172
  }
6346
6173
  subscribeToData() {
6347
6174
  // Subscribe with symbol for routing
@@ -38698,7 +38525,20 @@ class IntradayChartWidget extends BaseWidget {
38698
38525
  }
38699
38526
  async initialize() {
38700
38527
  // Validate initial symbol via API to get company info
38701
- await this.validateInitialSymbol();
38528
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
38529
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
38530
+ // Extract company info from first data item
38531
+ if (data && data[0]) {
38532
+ this.companyName = data[0].comp_name || '';
38533
+ this.exchangeName = data[0].market_name || '';
38534
+ this.mic = data[0].mic || '';
38535
+ }
38536
+ });
38537
+
38538
+ // If validation failed due to access/permission issues, stop initialization
38539
+ if (validationSuccess === false) {
38540
+ return; // Error is already shown, don't continue
38541
+ }
38702
38542
 
38703
38543
  // Update company name in the header
38704
38544
  this.updateCompanyName();
@@ -38706,38 +38546,6 @@ class IntradayChartWidget extends BaseWidget {
38706
38546
  // Load chart data
38707
38547
  await this.loadChartData();
38708
38548
  }
38709
- async validateInitialSymbol() {
38710
- try {
38711
- const apiService = this.wsManager.getApiService();
38712
- if (!apiService) {
38713
- if (this.debug) {
38714
- console.log('[IntradayChartWidget] API service not available for initial validation');
38715
- }
38716
- return;
38717
- }
38718
- const result = await apiService.quotel1(this.symbol);
38719
- if (result && result.data && result.data[0]) {
38720
- const symbolData = result.data[0];
38721
- // Extract company info
38722
- this.companyName = symbolData.comp_name || '';
38723
- this.exchangeName = symbolData.market_name || '';
38724
- this.mic = symbolData.mic || '';
38725
- if (this.debug) {
38726
- console.log('[IntradayChartWidget] Initial symbol validated:', {
38727
- symbol: this.symbol,
38728
- companyName: this.companyName,
38729
- exchangeName: this.exchangeName,
38730
- mic: this.mic
38731
- });
38732
- }
38733
- }
38734
- } catch (error) {
38735
- if (this.debug) {
38736
- console.warn('[IntradayChartWidget] Initial symbol validation failed:', error);
38737
- }
38738
- // Don't throw - let the widget continue with chart data
38739
- }
38740
- }
38741
38549
  updateCompanyName() {
38742
38550
  const companyNameElement = this.container.querySelector('.intraday-company-name');
38743
38551
  if (companyNameElement) {
@@ -39161,19 +38969,12 @@ class IntradayChartWidget extends BaseWidget {
39161
38969
  // Handle array format (standard night session format)
39162
38970
  if (Array.isArray(message.data)) {
39163
38971
  console.log('[IntradayChartWidget] Processing array format, length:', message.length);
39164
- const symbolData = message.find(item => item.Symbol === this.symbol);
38972
+ const symbolData = message.data.find(item => item.Symbol === this.symbol);
39165
38973
  console.log('[IntradayChartWidget] Found symbol data:', symbolData);
39166
38974
  if (symbolData && !symbolData.NotFound) {
39167
38975
  priceData = symbolData;
39168
38976
  }
39169
38977
  }
39170
- // Handle wrapped format
39171
- else if (message.type === 'queryblueoceanl1' || message.type === 'querybrucel1') {
39172
- console.log('[IntradayChartWidget] Processing wrapped format');
39173
- if (message['0']?.Symbol === this.symbol && !message['0'].NotFound) {
39174
- priceData = message['0'];
39175
- }
39176
- }
39177
38978
  // Handle direct data format
39178
38979
  else if (message.Symbol === this.symbol && !message.NotFound) {
39179
38980
  console.log('[IntradayChartWidget] Processing direct format');
@@ -40276,7 +40077,19 @@ class ONBBOLevel2Widget extends BaseWidget {
40276
40077
  this.showLoading();
40277
40078
 
40278
40079
  // Fetch company info for initial symbol
40279
- await this.validateInitialSymbol();
40080
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
40081
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
40082
+ // Extract company info from first data item
40083
+ if (data && data[0]) {
40084
+ this.companyName = data[0].comp_name || '';
40085
+ this.exchangeName = data[0].market_name || '';
40086
+ }
40087
+ });
40088
+
40089
+ // If validation failed due to access/permission issues, stop initialization
40090
+ if (validationSuccess === false) {
40091
+ return; // Error is already shown, don't continue
40092
+ }
40280
40093
 
40281
40094
  // Set timeout to detect no data on initial load
40282
40095
  this.loadingTimeout = setTimeout(() => {
@@ -40288,36 +40101,6 @@ class ONBBOLevel2Widget extends BaseWidget {
40288
40101
  }, 10000);
40289
40102
  this.subscribeToData();
40290
40103
  }
40291
- async validateInitialSymbol() {
40292
- try {
40293
- const apiService = this.wsManager.getApiService();
40294
- if (!apiService) {
40295
- if (this.debug) {
40296
- console.log('[ONBBO L2] API service not available for initial validation');
40297
- }
40298
- return;
40299
- }
40300
- const result = await apiService.quotel1(this.symbol);
40301
- if (result && result.data && result.data[0]) {
40302
- const symbolData = result.data[0];
40303
- // Extract company info
40304
- this.companyName = symbolData.comp_name || '';
40305
- this.exchangeName = symbolData.market_name || '';
40306
- if (this.debug) {
40307
- console.log('[ONBBO L2] Initial symbol validated:', {
40308
- symbol: this.symbol,
40309
- companyName: this.companyName,
40310
- exchangeName: this.exchangeName
40311
- });
40312
- }
40313
- }
40314
- } catch (error) {
40315
- if (this.debug) {
40316
- console.warn('[ONBBO L2] Initial symbol validation failed:', error);
40317
- }
40318
- // Don't throw - let the widget continue with WebSocket data
40319
- }
40320
- }
40321
40104
  subscribeToData() {
40322
40105
  // Subscribe to ONBBO Level 2 data (order book)
40323
40106
  // Use separate widget IDs to avoid "already active" duplicate detection
@@ -40836,6 +40619,7 @@ class TimeSalesWidget extends BaseWidget {
40836
40619
  this.styled = options.styled !== undefined ? options.styled : true;
40837
40620
  this.maxTrades = options.maxTrades || 20; // Maximum number of trades to display
40838
40621
  this.data = new TimeSalesModel([], this.maxTrades);
40622
+ this.data.symbol = this.symbol; // Set the symbol in the data model
40839
40623
  this.isDestroyed = false;
40840
40624
  this.unsubscribe = null;
40841
40625
  this.symbolEditor = null;
@@ -40847,6 +40631,7 @@ class TimeSalesWidget extends BaseWidget {
40847
40631
 
40848
40632
  // Create widget structure
40849
40633
  this.createWidgetStructure();
40634
+ this.initializeSymbolEditor();
40850
40635
 
40851
40636
  // Initialize the widget
40852
40637
  this.initialize();
@@ -40863,7 +40648,26 @@ class TimeSalesWidget extends BaseWidget {
40863
40648
  }
40864
40649
  }
40865
40650
  this.addStyles();
40866
- this.setupSymbolEditor();
40651
+ //this.setupSymbolEditor();
40652
+ }
40653
+ initializeSymbolEditor() {
40654
+ // Initialize symbol editor with stock symbol validation
40655
+ this.symbolEditor = new SymbolEditor(this, {
40656
+ maxLength: 10,
40657
+ placeholder: 'Enter symbol...',
40658
+ //validator: this.validateStockSymbol.bind(this),
40659
+ onSymbolChange: this.handleSymbolChange.bind(this),
40660
+ debug: this.debug,
40661
+ autoUppercase: true,
40662
+ symbolType: 'quotel1'
40663
+ });
40664
+
40665
+ // Set initial symbol
40666
+ const symbolElement = this.container.querySelector('.symbol');
40667
+ if (symbolElement) {
40668
+ symbolElement.textContent = this.symbol;
40669
+ symbolElement.dataset.originalSymbol = this.symbol;
40670
+ }
40867
40671
  }
40868
40672
  addStyles() {
40869
40673
  // Inject styles from the styles file into document head
@@ -40886,6 +40690,8 @@ class TimeSalesWidget extends BaseWidget {
40886
40690
  // Keep editor open when clicking buttons
40887
40691
  symbolType: 'quotel1' // Use standard quote validation
40888
40692
  });
40693
+
40694
+ //console.log('SETUP COMPLETE')
40889
40695
  }
40890
40696
  async handleSymbolChange(newSymbol, oldSymbol) {
40891
40697
  let validationData = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : null;
@@ -40988,7 +40794,19 @@ class TimeSalesWidget extends BaseWidget {
40988
40794
  this.showLoading();
40989
40795
 
40990
40796
  // Validate initial symbol via API to get company info
40991
- await this.validateInitialSymbol();
40797
+ // Uses BaseWidget's validateInitialSymbol method with callback for extracting company info
40798
+ const validationSuccess = await super.validateInitialSymbol('quotel1', this.symbol, data => {
40799
+ // Extract company info from first data item
40800
+ if (data && data[0]) {
40801
+ this.companyName = data[0].comp_name || '';
40802
+ this.exchangeName = data[0].market_name || '';
40803
+ }
40804
+ });
40805
+
40806
+ // If validation failed due to access/permission issues, stop initialization
40807
+ if (validationSuccess === false) {
40808
+ return; // Error is already shown, don't continue
40809
+ }
40992
40810
 
40993
40811
  // Set timeout to detect no data on initial load
40994
40812
  this.loadingTimeout = setTimeout(() => {
@@ -41001,36 +40819,6 @@ class TimeSalesWidget extends BaseWidget {
41001
40819
 
41002
40820
  this.subscribeToData();
41003
40821
  }
41004
- async validateInitialSymbol() {
41005
- try {
41006
- const apiService = this.wsManager.getApiService();
41007
- if (!apiService) {
41008
- if (this.debug) {
41009
- console.log('[TimeSalesWidget] API service not available for initial validation');
41010
- }
41011
- return;
41012
- }
41013
- const result = await apiService.quotel1(this.symbol);
41014
- if (result && result.data && result.data[0]) {
41015
- const symbolData = result.data[0];
41016
- // Extract company info
41017
- this.companyName = symbolData.comp_name || '';
41018
- this.exchangeName = symbolData.market_name || '';
41019
- if (this.debug) {
41020
- console.log('[TimeSalesWidget] Initial symbol validated:', {
41021
- symbol: this.symbol,
41022
- companyName: this.companyName,
41023
- exchangeName: this.exchangeName
41024
- });
41025
- }
41026
- }
41027
- } catch (error) {
41028
- if (this.debug) {
41029
- console.warn('[TimeSalesWidget] Initial symbol validation failed:', error);
41030
- }
41031
- // Don't throw - let the widget continue with WebSocket data
41032
- }
41033
- }
41034
40822
  subscribeToData() {
41035
40823
  // Subscribe to Time & Sales data (using queryonbbotimesale)
41036
40824
  this.unsubscribe = this.wsManager.subscribe(this.widgetId, ['queryonbbotimesale'],
@@ -41046,6 +40834,9 @@ class TimeSalesWidget extends BaseWidget {
41046
40834
  clearTimeout(this.loadingTimeout);
41047
40835
  this.loadingTimeout = null;
41048
40836
  }
40837
+ if (message.type != 'queryonbbotimesale') {
40838
+ return;
40839
+ }
41049
40840
 
41050
40841
  // Handle error messages
41051
40842
  if (message.type === 'error') {
@@ -41058,30 +40849,16 @@ class TimeSalesWidget extends BaseWidget {
41058
40849
  }
41059
40850
 
41060
40851
  // Handle Time & Sales data - Array of trades
41061
- if (Array.isArray(message)) {
40852
+ if (Array.isArray(message.data)) {
41062
40853
  if (this.debug) {
41063
40854
  console.log('[TimeSalesWidget] Received trades array:', message);
41064
40855
  }
41065
40856
 
41066
40857
  // Add new trades to existing data
41067
- this.data.addTrades(message);
40858
+ this.data.addTrades(message.data);
41068
40859
  this.updateWidget();
41069
40860
  return;
41070
40861
  }
41071
-
41072
- // Handle wrapped format
41073
- if ((message.type === 'queryonbbotimesale' || message.type === 'querytns') && message['0']) {
41074
- if (Array.isArray(message['0'])) {
41075
- if (this.debug) {
41076
- console.log('[TimeSalesWidget] Received wrapped trades array:', message['0']);
41077
- }
41078
-
41079
- // Add new trades to existing data
41080
- this.data.addTrades(message['0']);
41081
- this.updateWidget();
41082
- return;
41083
- }
41084
- }
41085
40862
  if (this.debug) {
41086
40863
  console.log('[TimeSalesWidget] Unexpected message format:', message);
41087
40864
  }
@@ -41308,7 +41085,15 @@ class ApiService {
41308
41085
  try {
41309
41086
  const response = await fetch(url, config);
41310
41087
  if (!response.ok) {
41311
- throw new Error(`HTTP error! status: ${response.status}`);
41088
+ // Try to get response body for error details
41089
+ let errorBody = '';
41090
+ try {
41091
+ errorBody = await response.text();
41092
+ console.error(`[ApiService] HTTP ${response.status} - Response body:`, errorBody);
41093
+ } catch (e) {
41094
+ console.error(`[ApiService] Could not read error response body:`, e);
41095
+ }
41096
+ throw new Error(`HTTP error! status: ${response.status}, body: ${errorBody}`);
41312
41097
  }
41313
41098
  return await response.json();
41314
41099
  } catch (error) {
@@ -42068,8 +41853,8 @@ class WebSocketManager {
42068
41853
  */
42069
41854
  _normalizeMessage(message) {
42070
41855
  // Check for error field (case-insensitive)
42071
- const errorField = message.error || message.Error;
42072
- const typeField = message.type || message.Type;
41856
+ const errorField = message.error;
41857
+ const typeField = message.type;
42073
41858
  const messageField = message.message || message.Message;
42074
41859
 
42075
41860
  // Handle explicit error messages - route based on type field
@@ -42109,54 +41894,16 @@ class WebSocketManager {
42109
41894
  }
42110
41895
  }
42111
41896
 
42112
- // Check for new format: has 'type'/'Type' AND 'data'/'Data' fields
42113
- const dataField = message.data || message.Data;
42114
- if (typeField && dataField !== undefined) {
42115
- if (this.config.debug) {
42116
- console.log(`[WebSocketManager] Detected new message format with type: ${typeField}`);
42117
- }
42118
-
42119
- // Extract the actual data and add the type to it for routing
42120
- let normalizedData;
42121
- if (Array.isArray(dataField)) {
42122
- // If data is an array, process each item
42123
- normalizedData = dataField;
42124
- // Add type info to the structure for routing
42125
- if (normalizedData.length > 0) {
42126
- normalizedData._messageType = typeField;
42127
- }
42128
- } else if (typeof dataField === 'object' && dataField !== null) {
42129
- // If data is an object, use it directly
42130
- normalizedData = dataField;
42131
- // Add type info for routing
42132
- normalizedData._messageType = typeField;
42133
- } else {
42134
- // Primitive value, wrap it
42135
- normalizedData = {
42136
- value: dataField,
42137
- _messageType: typeField
42138
- };
42139
- }
42140
-
42141
- // Only add type field if the normalized data doesn't already have one
42142
- if (typeof normalizedData === 'object' && !normalizedData.type && !normalizedData.Type) {
42143
- normalizedData.type = typeField;
42144
- }
42145
- return normalizedData;
42146
- }
42147
-
42148
41897
  // Old format or no type/data structure, return as is
42149
41898
  return message;
42150
41899
  }
42151
41900
  _routeMessage(message) {
42152
41901
  //console.log('message', message);
42153
41902
 
42154
- if (Array.isArray(message) && message.length === 0) {
42155
- if (this.config.debug) {
42156
- console.log('[WebSocketManager] Received empty array, ignoring');
42157
- }
42158
- return; // Don't route empty arrays
42159
- }
41903
+ // IMPORTANT: Don't filter out empty arrays here - route them based on message.type
41904
+ // Widgets need to receive empty arrays to clear loading states and show "no data" messages
41905
+ // The message structure is: { type: 'queryoptionchain', data: [] }
41906
+ // We need to route based on the 'type' field, not the data content
42160
41907
 
42161
41908
  // Cache the message for later use by new subscribers
42162
41909
  this._cacheMessage(message);
@@ -42255,22 +42002,28 @@ class WebSocketManager {
42255
42002
  _getRelevantWidgets(message) {
42256
42003
  const relevantWidgets = new Set();
42257
42004
 
42005
+ // OPTIMIZATION: Extract message type once from the entire message, not from each data item
42006
+ // Message structure: { type: 'queryoptionchain', data: [] }
42007
+ let dataField = message.Data || message.data;
42008
+ const messageType = message.type || this._extractMessageType(dataField);
42009
+
42258
42010
  // Handle array messages
42259
- this._addRelevantWidgetsForItem(message, relevantWidgets);
42011
+ this._addRelevantWidgetsForItem(message, relevantWidgets, messageType);
42260
42012
  return relevantWidgets;
42261
42013
  }
42262
- _addRelevantWidgetsForItem(item, relevantWidgets) {
42014
+ _addRelevantWidgetsForItem(item, relevantWidgets, messageType) {
42263
42015
  // If item has Data array, process it
42264
42016
  if (item.Data && Array.isArray(item.Data)) {
42265
42017
  //console.log('Data array found with length:', item.Data.length);
42266
42018
  item.Data.forEach(dataItem => {
42267
- this._processDataItem(dataItem, relevantWidgets);
42019
+ // Pass messageType from parent message, don't extract from each data item
42020
+ this._processDataItem(dataItem, relevantWidgets, messageType);
42268
42021
  });
42269
42022
  } else if (item && item[0] && item[0].Strike !== undefined && item[0].Expire && !item[0].underlyingSymbol) {
42270
42023
  this._processOptionChainData(item, relevantWidgets);
42271
42024
  } else {
42272
- // Process single item
42273
- this._processDataItem(item, relevantWidgets);
42025
+ // Process single item - messageType already extracted from message
42026
+ this._processDataItem(item, relevantWidgets, messageType);
42274
42027
  }
42275
42028
  }
42276
42029
 
@@ -42314,12 +42067,11 @@ class WebSocketManager {
42314
42067
  }
42315
42068
  }
42316
42069
  }
42317
- _processDataItem(dataItem, relevantWidgets) {
42070
+ _processDataItem(dataItem, relevantWidgets, messageType) {
42318
42071
  // Process individual data items and route to appropriate widgets
42319
42072
  // Option chain arrays are handled separately by _processOptionChainData
42320
42073
 
42321
42074
  const symbol = this._extractSymbol(dataItem);
42322
- const messageType = this._extractMessageType(dataItem);
42323
42075
  if (this.config.debug) {
42324
42076
  console.log('[WebSocketManager] Processing data item:', {
42325
42077
  symbol,
@@ -42479,25 +42231,15 @@ class WebSocketManager {
42479
42231
  return symbolMappings[extracted] || extracted;
42480
42232
  }
42481
42233
  _extractMessageType(item) {
42482
- // Check for _messageType field added by normalization (from new format)
42483
- if (item._messageType) {
42484
- return item._messageType;
42485
- }
42486
-
42487
- // Determine message type based on content
42488
- if (item.type) return item.type;
42489
-
42490
- // Infer type from data structure
42234
+ // FALLBACK: Infer type from data structure (for legacy/malformed messages)
42491
42235
 
42492
42236
  // IMPORTANT: Check for MMID field FIRST to distinguish ONBBO L2 from L1
42493
42237
  // Level 2 ONBBO data - array of MMID objects or object with MMID field
42494
42238
  if (Array.isArray(item) && item.length > 0 && item[0].MMID !== undefined) {
42495
42239
  return 'queryonbbol2';
42496
42240
  }
42497
-
42498
- // Check for single MMID object
42499
- if (item.MMID !== undefined) {
42500
- return 'queryonbbol2';
42241
+ if (Array.isArray(item) && item.length > 0 && item[0].Strike !== undefined) {
42242
+ return 'queryoptionchain';
42501
42243
  }
42502
42244
 
42503
42245
  // Check for Source field AFTER MMID check